git-core-*/?*
test-date
test-delta
+test-dump-cache-tree
common-cmds.h
*.tar.gz
*.dsc
man7: $(DOC_MAN7)
install: man
- $(INSTALL) -d -m755 $(DESTDIR)/$(man1) $(DESTDIR)/$(man7)
- $(INSTALL) $(DOC_MAN1) $(DESTDIR)/$(man1)
- $(INSTALL) $(DOC_MAN7) $(DESTDIR)/$(man7)
+ $(INSTALL) -d -m755 $(DESTDIR)$(man1) $(DESTDIR)$(man7)
+ $(INSTALL) $(DOC_MAN1) $(DESTDIR)$(man1)
+ $(INSTALL) $(DOC_MAN7) $(DESTDIR)$(man7)
#
------------------
The git configuration file contains a number of variables that affect
-the git commands behaviour. They can be used by both the git plumbing
-and the porcelains. The variables are divided to sections, where
+the git command's behavior. They can be used by both the git plumbing
+and the porcelains. The variables are divided into sections, where
in the fully qualified variable name the variable itself is the last
dot-separated segment and the section name is everything before the last
dot. The variable names are case-insensitive and only alphanumeric
characters are allowed. Some variables may appear multiple times.
The syntax is fairly flexible and permissive; whitespaces are mostly
-ignored. The '#' and ';' characters begin commends to the end of line,
+ignored. The '#' and ';' characters begin comments to the end of line,
blank lines are ignored, lines containing strings enclosed in square
brackets start sections and all the other lines are recognized
as setting variables, in the form 'name = value'. If there is no equal
~~~~~~~~~
Note that this list is non-comprehensive and not necessarily complete.
-For command-specific variables, you will find more detailed description
-in the appropriate manual page. You will find description of non-core
+For command-specific variables, you will find a more detailed description
+in the appropriate manual page. You will find a description of non-core
porcelain configuration variables in the respective porcelain documentation.
core.fileMode::
on hostnames ending with the specified domain string. This variable
may be set multiple times and is matched in the given order;
the first match wins.
-
- Can be overriden by the 'GIT_PROXY_COMMAND' environment variable
- (which always applies universally, without the special "for"
- handling).
++
+Can be overridden by the 'GIT_PROXY_COMMAND' environment variable
+(which always applies universally, without the special "for"
+handling).
core.ignoreStat::
The working copy files are assumed to stay unchanged until you
This is sometimes needed to work with old scripts that
expect HEAD to be a symbolic link.
+core.logAllRefUpdates::
+ If true, `git-update-ref` will append a line to
+ "$GIT_DIR/logs/<ref>" listing the new SHA1 and the date/time
+ of the update. If the file does not exist it will be
+ created automatically. This information can be used to
+ determine what commit was the tip of a branch "2 days ago".
+ This value is false by default (no logging).
+
core.repositoryFormatVersion::
Internal variable identifying the repository format and layout
version.
If true, git will warn you if the ref name you passed it is ambiguous
and might match multiple refs in the .git/refs/ tree. True by default.
+alias.*::
+ Command aliases for the gitlink:git[1] command wrapper - e.g.
+ after defining "alias.last = cat-file commit HEAD", the invocation
+ "git last" is equivalent to "git cat-file commit HEAD". To avoid
+ confusion and troubles with script usage, aliases that
+ hide existing git commands are ignored. Arguments are split by
+ spaces, the usual shell quoting and escaping is supported.
+ quote pair and a backslash can be used to quote them.
+
apply.whitespace::
Tells `git-apply` how to handle whitespaces, in the same way
as the '--whitespace' option. See gitlink:git-apply[1].
http.sslVerify::
Whether to verify the SSL certificate when fetching or pushing
- over HTTPS. Can be overriden by the 'GIT_SSL_NO_VERIFY' environment
+ over HTTPS. Can be overridden by the 'GIT_SSL_NO_VERIFY' environment
variable.
http.sslCert::
File containing the SSL certificate when fetching or pushing
- over HTTPS. Can be overriden by the 'GIT_SSL_CERT' environment
+ over HTTPS. Can be overridden by the 'GIT_SSL_CERT' environment
variable.
http.sslKey::
File containing the SSL private key when fetching or pushing
- over HTTPS. Can be overriden by the 'GIT_SSL_KEY' environment
+ over HTTPS. Can be overridden by the 'GIT_SSL_KEY' environment
variable.
http.sslCAInfo::
File containing the certificates to verify the peer with when
- fetching or pushing over HTTPS. Can be overriden by the
+ fetching or pushing over HTTPS. Can be overridden by the
'GIT_SSL_CAINFO' environment variable.
http.sslCAPath::
Path containing files with the CA certificates to verify the peer
- with when fetching or pushing over HTTPS. Can be overriden
+ with when fetching or pushing over HTTPS. Can be overridden
by the 'GIT_SSL_CAPATH' environment variable.
http.maxRequests::
- How many HTTP requests to launch in parallel. Can be overriden
+ How many HTTP requests to launch in parallel. Can be overridden
by the 'GIT_HTTP_MAX_REQUESTS' environment variable. Default is 5.
http.lowSpeedLimit, http.lowSpeedTime::
If the HTTP transfer speed is less than 'http.lowSpeedLimit'
for longer than 'http.lowSpeedTime' seconds, the transfer is aborted.
- Can be overriden by the 'GIT_HTTP_LOW_SPEED_LIMIT' and
+ Can be overridden by the 'GIT_HTTP_LOW_SPEED_LIMIT' and
'GIT_HTTP_LOW_SPEED_TIME' environment variables.
i18n.commitEncoding::
user.email::
Your email address to be recorded in any newly created commits.
- Can be overriden by the 'GIT_AUTHOR_EMAIL' and 'GIT_COMMITTER_EMAIL'
+ Can be overridden by the 'GIT_AUTHOR_EMAIL' and 'GIT_COMMITTER_EMAIL'
environment variables. See gitlink:git-commit-tree[1].
user.name::
Your full name to be recorded in any newly created commits.
- Can be overriden by the 'GIT_AUTHOR_NAME' and 'GIT_COMMITTER_NAME'
+ Can be overridden by the 'GIT_AUTHOR_NAME' and 'GIT_COMMITTER_NAME'
environment variables. See gitlink:git-commit-tree[1].
whatchanged.difftree::
-A short git tutorial
-====================
+A git core tutorial for developers
+==================================
Introduction
------------
----------------
where the `-t` tells `git-cat-file` to tell you what the "type" of the
-object is. git will tell you that you have a "blob" object (ie just a
+object is. git will tell you that you have a "blob" object (i.e., just a
regular file), and you can see the contents with
----------------
----------------
which will sign the current `HEAD` (but you can also give it another
-argument that specifies the thing to tag, ie you could have tagged the
+argument that specifies the thing to tag, i.e., you could have tagged the
current `mybranch` point by using `git tag <tagname> mybranch`).
You normally only do signed tags for major releases or things
using the object name of that commit object. Then it reads the
commit object to find out its parent commits and the associate
tree object; it repeats this process until it gets all the
-necessary objects. Because of this behaviour, they are
+necessary objects. Because of this behavior, they are
sometimes also called 'commit walkers'.
+
The 'commit walkers' are sometimes also called 'dumb
git for CVS users
=================
-So you're a CVS user. That's ok, it's a treatable condition. The job of
+So you're a CVS user. That's OK, it's a treatable condition. The job of
this document is to put you on the road to recovery, by helping you
convert an existing cvs repository to git, and by showing you how to use a
git repository in a cvs-like fashion.
they create are writable and searchable by other group members.
Suppose this repository is now set up in /pub/repo.git on the host
-foo.com. Then as an individual commiter you can clone the shared
+foo.com. Then as an individual committer you can clone the shared
repository:
------------------------------------------------
[NOTE]
============
-Because of this behaviour, if the shared repository and the developer's
+Because of this behavior, if the shared repository and the developer's
repository both have branches named `origin`, then a push like the above
attempts to update the `origin` branch in the shared repository from the
developer's `origin` branch. The results may be unexpected, so it's
* gitlink:git-fsck-objects[1] to validate the repository.
- * gitlink:git-prune[1] to garbage collect crufts in the
+ * gitlink:git-prune[1] to garbage collect cruft in the
repository.
* gitlink:git-repack[1] to pack loose objects for efficiency.
<1> running without "--full" is usually cheap and assures the
repository health reasonably well.
<2> check how many loose objects there are and how much
-diskspace is wasted by not repacking.
+disk space is wasted by not repacking.
<3> without "-a" repacks incrementally. repacking every 4-5MB
of loose objects accumulation may be a good rule of thumb.
<4> after repack, prune removes the duplicate loose objects.
----------------------------------------------------------------------
A standalone individual developer does not exchange patches with
-other poeple, and works alone in a single repository, using the
+other people, and works alone in a single repository, using the
following commands.
* gitlink:git-show-branch[1] to see where you are.
<11> make sure I did not accidentally rewind master beyond what I
already pushed out. "ko" shorthand points at the repository I have
at kernel.org, and looks like this:
- $ cat .git/remotes/ko
- URL: kernel.org:/pub/scm/git/git.git
- Pull: master:refs/tags/ko-master
- Pull: maint:refs/tags/ko-maint
- Push: master
- Push: +pu
- Push: maint
++
+------------
+$ cat .git/remotes/ko
+URL: kernel.org:/pub/scm/git/git.git
+Pull: master:refs/tags/ko-master
+Pull: maint:refs/tags/ko-maint
+Push: master
+Push: +pu
+Push: maint
+------------
++
In the output from "git show-branch", "master" should have
everything "ko-master" has.
+
<12> push out the bleeding edge.
<13> push the tag out, too.
Run git-daemon to serve /pub/scm from inetd.::
+
------------
-$ grep git /etc/inet.conf
+$ grep git /etc/inetd.conf
git stream tcp nowait nobody \
/usr/bin/git-daemon git-daemon --inetd --syslog --export-all /pub/scm
------------
+
The actual configuration line should be on one line.
+Run git-daemon to serve /pub/scm from xinetd.::
++
+------------
+$ cat /etc/xinetd.d/git-daemon
+# default: off
+# description: The git server offers access to git repositories
+service git
+{
+ disable = no
+ type = UNLISTED
+ port = 9418
+ socket_type = stream
+ wait = no
+ user = nobody
+ server = /usr/bin/git-daemon
+ server_args = --inetd --syslog --export-all --base-path=/pub/scm
+ log_on_failure += USERID
+}
+------------
++
+Check your xinetd(8) documentation and setup, this is from a Fedora system.
+Others might be different.
+
Give push/pull only access to developers.::
+
------------
-C<n>::
Ensure at least <n> lines of surrounding context match before
and after each change. When fewer lines of surrounding
- context exist they all most match. By default no context is
+ context exist they all must match. By default no context is
ever ignored.
--apply::
When `git-apply` is used for statistics and not applying a
patch, it defaults to `nowarn`.
You can use different `<option>` to control this
- behaviour:
+ behavior:
+
* `nowarn` turns off the trailing whitespace warning.
* `warn` outputs warnings for a few such errors, but applies the
OPTIONS
-------
--c, --compability::
+-c, --compatibility::
Use the same output mode as git-annotate (Default: off).
-l, --long::
- Show long rev (Defaults off).
+ Show long rev (Default: off).
+
+-t, --time::
+ Show raw timestamp (Default: off).
-S, --rev-file <revs-file>::
Use revs from revs-file instead of calling git-rev-list.
--------
[verse]
'git-branch' [-r]
-'git-branch' [-f] <branchname> [<start-point>]
+'git-branch' [-l] [-f] <branchname> [<start-point>]
'git-branch' (-d | -D) <branchname>...
DESCRIPTION
equal to that of the currently checked out branch.
With a `-d` or `-D` option, `<branchname>` will be deleted. You may
-specify more than one branch for deletion.
+specify more than one branch for deletion. If the branch currently
+has a ref log then the ref log will also be deleted.
OPTIONS
-D::
Delete a branch irrespective of its index status.
+-l::
+ Create the branch's ref log. This activates recording of
+ all changes to made the branch ref, enabling use of date
+ based sha1 expressions such as "<branchname>@{yesterday}".
+
-f::
Force the creation of a new branch even if it means deleting
a branch that already exists with the same name.
a tag is stored under `$GIT_DIR/refs/tags` directory. git
imposes the following rules on how refs are named:
-. It could be named hierarchically (i.e. separated with slash
- `/`), but each of its component cannot begin with a dot `.`;
+. It can include slash `/` for hierarchical (directory)
+ grouping, but no slash-separated component can begin with a
+ dot `.`;
. It cannot have two consecutive dots `..` anywhere;
SYNOPSIS
--------
[verse]
-'git-checkout' [-f] [-b <new_branch>] [-m] [<branch>]
+'git-checkout' [-f] [-b <new_branch> [-l]] [-m] [<branch>]
'git-checkout' [-m] [<branch>] <paths>...
DESCRIPTION
by gitlink:git-check-ref-format[1]. Some of these checks
may restrict the characters allowed in a branch name.
+-l::
+ Create the new branch's ref log. This activates recording of
+ all changes to made the branch ref, enabling use of date
+ based sha1 expressions such as "<branchname>@{yesterday}".
+
-m::
If you have local modifications to one or more files that
are different between the current branch and the branch to
SYNOPSIS
--------
[verse]
-'git-clone' [-l [-s]] [-q] [-n] [--bare] [-o <name>] [-u <upload-pack>]
- [--reference <repository>]
- <repository> [<directory>]
+'git-clone' [--template=<template_directory>] [-l [-s]] [-q] [-n] [--bare]
+ [-o <name>] [-u <upload-pack>] [--reference <repository>]
+ [--use-separate-remote] <repository> [<directory>]
DESCRIPTION
-----------
files in `<directory>/.git`, make the `<directory>`
itself the `$GIT_DIR`. This implies `-n` option. When
this option is used, neither the `origin` branch nor the
- default `remotes/origin` file is created.
+ default `remotes/origin` file is created.
-o <name>::
Instead of using the branch name 'origin' to keep track
the command to specify non-default path for the command
run on the other end.
+--template=<template_directory>::
+ Specify the directory from which templates will be used;
+ if unset the templates are taken from the installation
+ defined default, typically `/usr/share/git-core/templates`.
+
+--use-separate-remote::
+ Save remotes heads under `$GIT_DIR/remotes/origin/` instead
+ of `$GIT_DIR/refs/heads/`. Only the master branch is saved
+ in the latter.
+
<repository>::
The (possibly remote) repository to clone from. It can
be any URL git-fetch supports.
<directory>::
- The name of a new directory to clone into. The "humanish"
+ The name of a new directory to clone into. The "humanish"
part of the source repository is used if no directory is
explicitly given ("repo" for "/path/to/repo.git" and "foo"
for "host.xz:foo/.git"). Cloning into an existing directory
Instead of committing only the files specified on the
command line, update them in the index file and then
commit the whole index. This is the traditional
- behaviour.
+ behavior.
-o|--only::
Commit only the files specified on the command line.
1. If you are going to offer anonymous CVS access via pserver, add a line in
/etc/inetd.conf like
-
++
+--
+------
cvspserver stream tcp nowait nobody git-cvsserver pserver
- Note: In some cases, you need to pass the 'pserver' argument twice for
- git-cvsserver to see it. So the line would look like
+------
+Note: In some cases, you need to pass the 'pserver' argument twice for
+git-cvsserver to see it. So the line would look like
+------
cvspserver stream tcp nowait nobody git-cvsserver pserver pserver
- No special setup is needed for SSH access, other than having GIT tools
- in the PATH. If you have clients that do not accept the CVS_SERVER
- env variable, you can rename git-cvsserver to cvs.
-
+------
+No special setup is needed for SSH access, other than having GIT tools
+in the PATH. If you have clients that do not accept the CVS_SERVER
+env variable, you can rename git-cvsserver to cvs.
+--
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
# optional for debugging
logfile=/path/to/logfile
- Note: you need to ensure each user that is going to invoke git-cvsserver has
- write access to the log file and to the git repository. When offering anon
- access via pserver, this means that the nobody user should have write access
- to at least the sqlite database at the root of the repository.
-
+------
+Note: you need to ensure each user that is going to invoke git-cvsserver has
+write access to the log file and to the git repository. When offering anon
+access via pserver, this means that the nobody user should have write access
+to at least the sqlite database at the root of the repository.
+--
3. On the 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. For example:
++
+--
+For SSH access, CVS_SERVER should be set to git-cvsserver
- For SSH access, CVS_SERVER should be set to git-cvsserver
-
- Example:
+Example:
+------
export CVSROOT=:ext:user@server:/var/git/project.git
export CVS_SERVER=git-cvsserver
-
+------
+--
4. For SSH clients that will make commits, make sure their .bashrc file
sets the GIT_AUTHOR and GIT_COMMITTER variables.
5. Clients should now be able to check out the project. Use the CVS 'module'
name to indicate what GIT 'head' you want to check out. Example:
-
++
+------
cvs co -d project-master master
+------
Eclipse CVS Client Notes
------------------------
4. Pick 'HEAD' when it asks what branch/tag to check out. Untick the
"launch commit wizard" to avoid committing the .project file.
-Protocol notes: If you are using anonymous acces via pserver, just select that.
+Protocol notes: If you are using anonymous access via pserver, just select that.
Those using SSH access should choose the 'ext' protocol, and configure 'ext'
access on the Preferences->Team->CVS->ExtConnection pane. Set CVS_SERVER to
'git-cvsserver'. Not that password support is not good when using 'ext',
when it gets one.
It's careful in that there's a magic request-line that gives the command and
-what directory to upload, and it verifies that the directory is ok.
+what directory to upload, and it verifies that the directory is OK.
It verifies that the directory has the magic file "git-daemon-export-ok", and
it will refuse to export any git directory that hasn't explicitly been marked
pass some directory paths as 'git-daemon' arguments, you can further restrict
the offers to a whitelist comprising of those.
-This is ideally suited for read-only updates, ie pulling from git repositories.
+This is ideally suited for read-only updates, i.e., pulling from git repositories.
OPTIONS
-------
OPTIONS
-------
<committish>::
- The object name of the comittish.
+ The object name of the committish.
--all::
Instead of using only the annotated tags, use any ref
torvalds@ppc970:~/v2.6/linux> git-diff-index HEAD
*100644->100664 blob 7476bb......->000000...... kernel/sched.c
-ie it shows that the tree has changed, and that `kernel/sched.c` has is
+i.e., it shows that the tree has changed, and that `kernel/sched.c` has is
not up-to-date and may contain new stuff. The all-zero sha1 means that to
get the real diff, you need to look at the object in the working directory
directly rather than do an object-to-object diff.
<path>...::
If provided, the results are limited to a subset of files
matching one of these prefix strings.
- ie file matches `/^<pattern1>|<pattern2>|.../`
+ i.e., file matches `/^<pattern1>|<pattern2>|.../`
Note that this parameter does not provide any wildcard or regexp
features.
+
When a single commit is given on one line of such input, it compares
the commit with its parents. The following flags further affects its
-behaviour. This does not apply to the case where two <tree-ish>
+behavior. This does not apply to the case where two <tree-ish>
separated with a single space are given.
-m::
nor deletion.
<2> show only names and the nature of change, but not actual
diff output. --name-status disables usual patch generation
-which in turn also disables recursive behaviour, so without -r
+which in turn also disables recursive behavior, so without -r
you would only see the directory name if there is a change in a
file in a subdirectory.
<3> limit diff output to named subtrees.
SYNOPSIS
--------
[verse]
-'git-format-patch' [-n | -k] [-o <dir> | --stdout] [--attach] [-s] [-c]
- [--diff-options] <his> [<mine>]
+'git-format-patch' [-n | -k] [-o <dir> | --stdout] [--attach]
+ [-s | --signoff] [--diff-options] [--start-number <n>]
+ <since>[..<until>]
DESCRIPTION
-----------
-Prepare each commit with its patch since <mine> head forked from
-<his> head, one file per patch formatted to resemble UNIX mailbox
-format, for e-mail submission or use with gitlink:git-am[1].
+
+Prepare each commit between <since> and <until> with its patch in
+one file per commit, formatted to resemble UNIX mailbox format.
+If ..<until> is not specified, the head of the current working
+tree is implied.
+
+The output of this command is convenient for e-mail submission or
+for use with gitlink:git-am[1].
Each output file is numbered sequentially from 1, and uses the
-first line of the commit message (massaged for pathname safety)
-as the filename.
+first line of the commit message (massaged for pathname safety) as
+the filename. The names of the output files are printed to standard
+output, unless the --stdout option is specified.
-When -o is specified, output files are created in <dir>; otherwise
-they are created in the current working directory. This option
-is ignored if --stdout is specified.
+If -o is specified, output files are created in <dir>. Otherwise
+they are created in the current working directory.
-When -n is specified, instead of "[PATCH] Subject", the first
-line is formatted as "[PATCH N/M] Subject", unless you have only
-one patch.
+If -n is specified, instead of "[PATCH] Subject", the first line
+is formatted as "[PATCH n/m] Subject".
OPTIONS
-n|--numbered::
Name output in '[PATCH n/m]' format.
+--start-number <n>::
+ Start numbering the patches at <n> instead of 1.
+
-k|--keep-subject::
Do not strip/add '[PATCH]' from the first line of the
commit log message.
Add `Signed-off-by:` line to the commit message, using
the committer identity of yourself.
--c|--check::
- Display suspicious lines in the patch. The definition
- of 'suspicious lines' is currently the lines that has
- trailing whitespaces, and the lines whose indentation
- has a SP character immediately followed by a TAB
- character.
-
--stdout::
- This flag generates the mbox formatted output to the
- standard output, instead of saving them into a file per
- patch and implies --mbox.
+ Print all commits to the standard output in mbox format,
+ instead of creating a file for each one.
--attach::
Create attachments instead of inlining patches.
cherry-pick them.
git-format-patch origin::
- Extract commits the current branch accumulated since it
- pulled from origin the last time in a patch form for
- e-mail submission.
+ Extract all commits which are in the current branch but
+ not in the origin branch. For each commit a separate file
+ is created in the current directory.
git-format-patch -M -B origin::
- The same as the previous one, except detect and handle
- renames and complete rewrites intelligently to produce
- renaming patch. A renaming patch reduces the amount of
- text output, and generally makes it easier to review
- it. Note that the "patch" program does not understand
- renaming patch well, so use it only when you know the
- recipient uses git to apply your patch.
+ The same as the previous one. Additionally, it detects
+ and handles renames and complete rewrites intelligently to
+ produce a renaming patch. A renaming patch reduces the
+ amount of text output, and generally makes it easier to
+ review it. Note that the "patch" program does not
+ understand renaming patches, so use it only when you know
+ the recipient uses git to apply your patch.
See Also
do have a valid tree.
Any corrupt objects you will have to find in backups or other archives
-(ie you can just remove them and do an "rsync" with some other site in
+(i.e., you can just remove them and do an "rsync" with some other site in
the hopes that somebody else has the object you have corrupted).
Of course, "valid tree" doesn't mean that it wasn't generated by some
-------
--cached::
Instead of searching in the working tree files, check
- the blobs registerd in the index file.
+ the blobs registered in the index file.
-a | --text::
Process binary files as if they were text.
-[ABC] <context>::
Show `context` trailing (`A` -- after), or leading (`B`
-- before), or both (`C` -- context) lines, and place a
- line containing `--` between continguous groups of
+ line containing `--` between contiguous groups of
matches.
-f <file>::
- if it does not contain a slash '/', it is a shell glob
pattern and used to match against the filename without
- leading directories (i.e. the same way as the current
- implementation).
+ leading directories.
- otherwise, it is a shell glob pattern, suitable for
consumption by fnmatch(3) with FNM_PATHNAME flag. I.e. a
--exclude-per-directory=.gitignore
--------------------------------------------------------------
+Another example:
+
+--------------------------------------------------------------
+ $ cat .gitignore
+ vmlinux*
+ $ ls arch/foo/kernel/vm*
+ arch/foo/kernel/vmlinux.lds.S
+ $ echo '!/vmlinux*' >arch/foo/kernel/.gitignore
+--------------------------------------------------------------
+
+The second .gitignore keeps `arch/foo/kernel/vmlinux.lds.S` file
+from getting ignored.
+
See Also
--------
SYNOPSIS
--------
+[verse]
'git-ls-tree' [-d] [-r] [-t] [-z]
- [--name-only] [--name-status] [--full-name] [--abbrev=[<n>]]
- <tree-ish> [paths...]
+ [--name-only] [--name-status] [--full-name] [--abbrev=[<n>]]
+ <tree-ish> [paths...]
DESCRIPTION
-----------
lines, show only handful hexdigits prefix.
Non default number of digits can be specified with --abbrev=<n>.
+--full-name::
+ Instead of showing the path names relative to the current working
+ directory, show the full path names.
+
paths::
When paths are given, show them (note that this isn't really raw
pathnames, but rather a list of patterns to match). Otherwise
Documentation by David Greaves, Junio C Hamano and the git-list
<git@vger.kernel.org>.
-This manual page is a stub. You can help the git documentation by expanding it.
-
GIT
---
Part of the gitlink:git[7] suite
fatal: merge program failed
where the latter example shows how "git-merge-index" will stop trying to
-merge once anything has returned an error (ie "cat" returned an error
+merge once anything has returned an error (i.e., "cat" returned an error
for the AA file, because it didn't exist in the original, and thus
"git-merge-index" didn't even try to merge the MM thing).
SYNOPSIS
--------
-'git-name-rev' [--tags] ( --all | --stdin | <commitish>... )
+'git-name-rev' [--tags] ( --all | --stdin | <committish>... )
DESCRIPTION
-----------
--- /dev/null
+git-p4import(1)
+===============
+
+NAME
+----
+git-p4import - Import a Perforce repository into git
+
+
+SYNOPSIS
+--------
+`git-p4import` [-q|-v] [--notags] [--authors <file>] [-t <timezone>] <//p4repo/path> <branch>
+
+`git-p4import` --stitch <//p4repo/path>
+
+`git-p4import`
+
+
+DESCRIPTION
+-----------
+Import a Perforce repository into an existing git repository. When
+a <//p4repo/path> and <branch> are specified a new branch with the
+given name will be created and the initial import will begin.
+
+Once the initial import is complete you can do an incremental import
+of new commits from the Perforce repository. You do this by checking
+out the appropriate git branch and then running `git-p4import` without
+any options.
+
+The standard p4 client is used to communicate with the Perforce
+repository; it must be configured correctly in order for `git-p4import`
+to operate (see below).
+
+
+OPTIONS
+-------
+-q::
+ Do not display any progress information.
+
+-v::
+ Give extra progress information.
+
+\--authors::
+ Specify an authors file containing a mapping of Perforce user
+ ids to full names and email addresses (see Notes below).
+
+\--notags::
+ Do not create a tag for each imported commit.
+
+\--stitch::
+ Import the contents of the given perforce branch into the
+ currently checked out git branch.
+
+\--log::
+ Store debugging information in the specified file.
+
+-t::
+ Specify that the remote repository is in the specified timezone.
+ Timezone must be in the format "US/Pacific" or "Europe/London"
+ etc. You only need to specify this once, it will be saved in
+ the git config file for the repository.
+
+<//p4repo/path>::
+ The Perforce path that will be imported into the specified branch.
+
+<branch>::
+ The new branch that will be created to hold the Perforce imports.
+
+
+P4 Client
+---------
+You must make the `p4` client command available in your $PATH and
+configure it to communicate with the target Perforce repository.
+Typically this means you must set the "$P4PORT" and "$P4CLIENT"
+environment variables.
+
+You must also configure a `p4` client "view" which maps the Perforce
+branch into the top level of your git repository, for example:
+
+------------
+Client: myhost
+
+Root: /home/sean/import
+
+Options: noallwrite clobber nocompress unlocked modtime rmdir
+
+View:
+ //public/jam/... //myhost/jam/...
+------------
+
+With the above `p4` client setup, you could import the "jam"
+perforce branch into a branch named "jammy", like so:
+
+------------
+$ mkdir -p /home/sean/import/jam
+$ cd /home/sean/import/jam
+$ git init-db
+$ git p4import //public/jam jammy
+------------
+
+
+Multiple Branches
+-----------------
+Note that by creating multiple "views" you can use `git-p4import`
+to import additional branches into the same git repository.
+However, the `p4` client has a limitation in that it silently
+ignores all but the last "view" that maps into the same local
+directory. So the following will *not* work:
+
+------------
+View:
+ //public/jam/... //myhost/jam/...
+ //public/other/... //myhost/jam/...
+ //public/guest/... //myhost/jam/...
+------------
+
+If you want more than one Perforce branch to be imported into the
+same directory you must employ a workaround. A simple option is
+to adjust your `p4` client before each import to only include a
+single view.
+
+Another option is to create multiple symlinks locally which all
+point to the same directory in your git repository and then use
+one per "view" instead of listing the actual directory.
+
+
+Tags
+----
+A git tag of the form p4/xx is created for every change imported from
+the Perforce repository where xx is the Perforce changeset number.
+Therefore after the import you can use git to access any commit by its
+Perforce number, eg. git show p4/327.
+
+The tag associated with the HEAD commit is also how `git-p4import`
+determines if there are new changes to incrementally import from the
+Perforce repository.
+
+If you import from a repository with many thousands of changes
+you will have an equal number of p4/xxxx git tags. Git tags can
+be expensive in terms of disk space and repository operations.
+If you don't need to perform further incremental imports, you
+may delete the tags.
+
+
+Notes
+-----
+You can interrupt the import (eg. ctrl-c) at any time and restart it
+without worry.
+
+Author information is automatically determined by querying the
+Perforce "users" table using the id associated with each change.
+However, if you want to manually supply these mappings you can do
+so with the "--authors" option. It accepts a file containing a list
+of mappings with each line containing one mapping in the format:
+
+------------
+ perforce_id = Full Name <email@address.com>
+------------
+
+
+Author
+------
+Written by Sean Estabrooks <seanlkml@sympatico.ca>
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
+
-----------
A "patch ID" is nothing but a SHA1 of the diff associated with a patch, with
whitespace and line numbers ignored. As such, it's "reasonably stable", but at
-the same time also reasonably unique, ie two patches that have the same "patch
+the same time also reasonably unique, i.e., two patches that have the same "patch
ID" are almost guaranteed to be the same thing.
IOW, you can use this thing to look for likely duplicate commits.
SYNOPSIS
--------
-'git-read-tree' (<tree-ish> | [[-m [--aggressive]| --reset] [-u | -i]] <tree-ish1> [<tree-ish2> [<tree-ish3>]])
+'git-read-tree' (<tree-ish> | [[-m [--aggressive] | --reset | --prefix=<prefix>] [-u | -i]] <tree-ish1> [<tree-ish2> [<tree-ish3>]])
DESCRIPTION
* when both sides adds a path identically. The resolution
is to add that path.
+--prefix=<prefix>/::
+ Keep the current index contents, and read the contents
+ of named tree-ish under directory at `<prefix>`. The
+ original index file cannot have anything at the path
+ `<prefix>` itself, and have nothing in `<prefix>/`
+ directory. Note that the `<prefix>/` value must end
+ with a slash.
+
+
<tree-ish#>::
The id of the tree object(s) to be read/merged.
will complain about unmerged entries if it sees a single entry that is not
stage 0.
-Ok, this all sounds like a collection of totally nonsensical rules,
+OK, this all sounds like a collection of totally nonsensical rules,
but it's actually exactly what you want in order to do a fast
merge. The different stages represent the "result tree" (stage 0, aka
"merged"), the original tree (stage 1, aka "orig"), and the two trees
- the index file saves and restores with all this information, so you
can merge things incrementally, but as long as it has entries in
- stages 1/2/3 (ie "unmerged entries") you can't write the result. So
+ stages 1/2/3 (i.e., "unmerged entries") you can't write the result. So
now the merge algorithm ends up being really simple:
* you walk the index in order, and ignore all entries of stage 0,
This is done to prevent you from losing your work-in-progress
changes, and mixing your random changes in an unrelated merge
commit. To illustrate, suppose you start from what has been
-commited last to your repository:
+committed last to your repository:
----------------
$ JC=`git-rev-parse --verify "HEAD^0"`
-------
--replace-all::
- Default behaviour is to replace at most one line. This replaces
+ Default behavior is to replace at most one line. This replaces
all lines matching the key (and optionally the value_regex).
--get::
OPTIONS
-------
--mixed::
- Resets the index but not the working tree (ie, the changed files
+ Resets the index but not the working tree (i.e., the changed files
are preserved but not marked for commit) and reports what has not
been updated. This is the default action.
DESCRIPTION
-----------
-Many git Porcelainish commands take mixture of flags
+Many git porcelainish commands take mixture of flags
(i.e. parameters that begin with a dash '-') and parameters
meant for underlying `git-rev-list` command they use internally
and flags and parameters for other commands they use as the
--short, --short=number::
Instead of outputting the full SHA1 values of object names try to
- abbriviate them to a shorter unique name. When no length is specified
+ abbreviate them to a shorter unique name. When no length is specified
7 is used. The minimum length is 4.
--since=datestring, --after=datestring::
happen to have both heads/master and tags/master, you can
explicitly say 'heads/master' to tell git which one you mean.
+* A suffix '@' followed by a date specification enclosed in a brace
+ pair (e.g. '\{yesterday\}', '\{1 month 2 weeks 3 days 1 hour 1
+ second ago\}' or '\{1979-02-26 18:30:00\}') to specify the value
+ of the ref at a prior point in time. This suffix may only be
+ used immediately following a ref name and the ref must have an
+ existing log ($GIT_DIR/logs/<ref>).
+
* A suffix '{caret}' to a revision parameter means the first parent of
that commit object. '{caret}<n>' means the <n>th parent (i.e.
'rev{caret}'
is not set, this will be prompted for.
--no-signed-off-by-cc::
- Do not add emails foudn in Signed-off-by: lines to the cc list.
+ Do not add emails found in Signed-off-by: lines to the cc list.
--quiet::
Make git-send-email less verbose. One line per email should be
There are three ways to specify which refs to update on the
remote end.
-With '--all' flag, all refs that exist locally are transfered to
+With '--all' flag, all refs that exist locally are transferred to
the remote side. You cannot specify any '<ref>' if you use
this flag.
-----------
Sets up the normal git environment variables and a few helper functions
-(currently just "die()"), and returns ok if it all looks like a git archive.
+(currently just "die()"), and returns OK if it all looks like a git archive.
So, to make the rest of the git scripts more careful and readable,
use it as follows:
----
git-shortlog - Summarize 'git log' output
-
SYNOPSIS
--------
git-log --pretty=short | 'git-shortlog'
DESCRIPTION
-----------
Summarizes 'git log' output in a format suitable for inclusion
-in release announcements.
-
+in release announcements. Each commit will be grouped by author
+the first line of the commit message will be shown.
+
+Additionally, "[PATCH]" will be stripped from the commit description.
+
+FILES
+-----
+'.mailmap'::
+ If this file exists, it will be used for mapping author email
+ addresses to a real author name. One mapping per line, first
+ the author name followed by the email address enclosed by
+ '<' and '>'. Use hash '#' for comments. Example:
+
+ # Keep alphabetized
+ Adam Morrow <adam@localhost.localdomain>
+ Eve Jones <eve@laptop.(none)>
Author
------
SYNOPSIS
--------
-'git-tar-tree' <tree-ish> [ <base> ]
+'git-tar-tree' [--remote=<repo>] <tree-ish> [ <base> ]
DESCRIPTION
-----------
Additionally the commit ID is stored in a global extended pax header.
It can be extracted using git-get-tar-commit-id.
+OPTIONS
+-------
+
+<tree-ish>::
+ The tree or commit to produce tar archive for. If it is
+ the object name of a commit object.
+
+<base>::
+ Leading path to the files in the resulting tar archive.
+
+--remote=<repo>::
+ Instead of making a tar archive from local repository,
+ retrieve a tar archive from a remote repository.
+
+Examples
+--------
+git tar-tree HEAD | (cd /var/tmp/ && mkdir junk && tar Cxf junk -)::
+
+ Create a tar archive that contains the contents of the
+ latest commit on the current branch, and extracts it in
+ `/var/tmp/junk` directory.
+
+git tar-tree v2.6.17 linux-2.6.17 | gzip >linux-2.6.17.tar.gz
+
+ Create a tarball for v2.6.17 release.
+
+git tar-tree --remote=example.com:git.git v0.99 >git-0.99.tar
+
+ Get a tarball v0.99 from example.com.
Author
------
link.
-Alternative/Augmentative Procelains
+Alternative/Augmentative Porcelains
-----------------------------------
- *Cogito* (http://www.kernel.org/pub/software/scm/cogito/)
--remove::
If a specified file is in the index but is missing then it's
removed.
- Default behaviour is to ignore removed file.
+ Default behavior is to ignore removed file.
--refresh::
Looks at the current index and checks to see if merges or
SYNOPSIS
--------
-'git-update-ref' <ref> <newvalue> [<oldvalue>]
+'git-update-ref' [-m <reason>] <ref> <newvalue> [<oldvalue>]
DESCRIPTION
-----------
ref symlink to some other tree, if you have copied a whole
archive by creating a symlink tree).
+Logging Updates
+---------------
+If config parameter "core.logAllRefUpdates" is true or the file
+"$GIT_DIR/logs/<ref>" exists then `git-update-ref` will append
+a line to the log file "$GIT_DIR/logs/<ref>" (dereferencing all
+symbolic refs before creating the log name) describing the change
+in ref value. Log lines are formatted as:
+
+ . oldsha1 SP newsha1 SP committer LF
++
+Where "oldsha1" is the 40 character hexadecimal value previously
+stored in <ref>, "newsha1" is the 40 character hexadecimal value of
+<newvalue> and "committer" is the committer's name, email address
+and date in the standard GIT committer ident format.
+
+Optionally with -m:
+
+ . oldsha1 SP newsha1 SP committer TAB message LF
++
+Where all fields are as described above and "message" is the
+value supplied to the -m option.
+
+An update will fail (without changing <ref>) if the current user is
+unable to create a new log file, append to the existing log file
+or does not have committer information available.
+
Author
------
Written by Linus Torvalds <torvalds@osdl.org>.
--- /dev/null
+git-upload-tar(1)
+=================
+
+NAME
+----
+git-upload-tar - Send tar archive
+
+
+SYNOPSIS
+--------
+'git-upload-tar' <directory>
+
+DESCRIPTION
+-----------
+Invoked by 'git-tar-tree --remote' and sends a generated tar archive
+to the other end over the git protocol.
+
+This command is usually not invoked directly by the end user.
+The UI for the protocol is on the 'git-tar-tree' side, and the
+program pair is meant to be used to get a tar achive from a
+remote repository.
+
+
+OPTIONS
+-------
+<directory>::
+ The repository to get a tar archive from.
+
+Author
+------
+Written by Junio C Hamano <junio@kernel.org>
+
+Documentation
+--------------
+Documentation by Junio C Hamano.
+
+GIT
+---
+Part of the gitlink:git[7] suite
SYNOPSIS
--------
-'git-write-tree' [--missing-ok]
+'git-write-tree' [--missing-ok] [--prefix=<prefix>/]
DESCRIPTION
-----------
directory exist in the object database. This option disables this
check.
+--prefix=<prefix>/::
+ Writes a tree object that represents a subdirectory
+ `<prefix>`. This can be used to write the tree object
+ for a subproject that is in the named subdirectory.
+
+
Author
------
Written by Linus Torvalds <torvalds@osdl.org>
"man git-commandname" for documentation of each command. CVS users may
also want to read link:cvs-migration.html[CVS migration].
+The COMMAND is either a name of a Git command (see below) or an alias
+as defined in the configuration file (see gitlink:git-repo-config[1]).
+
OPTIONS
-------
--version::
Invoked by 'git-clone-pack' and 'git-fetch-pack' to push
what are asked for.
+gitlink:git-upload-tar[1]::
+ Invoked by 'git-tar-tree --remote' to return the tar
+ archive the other end asked for.
+
High-level commands (porcelain)
-------------------------------
gitlink:git-prune[1]::
Prunes all unreachable objects from the object database.
+gitlink:git-quiltimport[1]::
+ Applies a quilt patchset onto the current branch.
+
gitlink:git-relink[1]::
Hardlink common objects in local repositories.
git archive::
Synonym for repository (for arch people).
+grafts::
+ Grafts enables two otherwise different lines of development to be
+ joined together by recording fake ancestry information for commits.
+ This way you can make git pretend the set of parents a commit
+ has is different from what was recorded when the commit was created.
+ Configured via the `.git/info/grafts` file.
+
hash::
In git's context, synonym to object name.
This hook is invoked by `git-receive-pack` on the remote repository,
which is happens when a `git push` is done on a local repository.
Just before updating the ref on the remote repository, the update hook
-is invoked. It's exit status determins the success or failure of
+is invoked. Its exit status determines the success or failure of
the ref update.
The hook executes once for each ref to be updated, and takes
The default post-update hook, when enabled, runs
`git-update-server-info` to keep the information used by dumb
-transports (eg, http) up-to-date. If you are publishing
+transports (e.g., http) up-to-date. If you are publishing
a git repository that is accessible via http, you should
probably enable this hook.
Stores shorthands to be used to give URL and default
refnames to interact with remote repository to `git
fetch`, `git pull` and `git push` commands.
+
+logs::
+ Records of changes made to refs are stored in this
+ directory. See the documentation on git-update-ref
+ for more information.
+
+logs/refs/heads/`name`::
+ Records all changes made to the branch tip named `name`.
+
+logs/refs/tags/`name`::
+ Records all changes made to the tag named `name`.
- The header appears at the beginning and consists of the following:
- 4-byte signature
- 4-byte version number (network byte order)
+ 4-byte signature:
+ The signature is: {'P', 'A', 'C', 'K'}
+
+ 4-byte version number (network byte order):
+ GIT currently accepts version number 2 or 3 but
+ generates version 2 only.
+
4-byte number of objects contained in the pack (network byte order)
Observation: we cannot have more than 4G versions ;-) and
8-byte integers to go beyond 4G objects per pack, but it is
not strictly necessary.
- - The header is followed by sorted 28-byte entries, one entry
+ - The header is followed by sorted 24-byte entries, one entry
per object in the pack. Each entry is:
4-byte network byte order integer, recording where the
- we actively try to generate deltas from a larger object to a
smaller one
- this means that the top-of-tree very seldom has deltas
- (ie deltas in _practice_ are "backwards deltas")
+ (i.e. deltas in _practice_ are "backwards deltas")
Again, we should reread that whole paragraph. Not just because
Linus has slipped Linus's Law in there on us, but because it is
A tree can refer to one or more "blob" objects, each corresponding to
a file. In addition, a tree can also refer to other tree objects,
-thus creating a directory heirarchy. You can examine the contents of
+thus creating a directory hierarchy. You can examine the contents of
any tree using ls-tree (remember that a long enough initial portion
of the SHA1 will also work):
------------------------------------------------
$ git ls-tree d0492b36
100644 blob a0423896973644771497bdc03eb99d5281615b51 file.txt
-$ git cat-file commit a0423896
+$ git cat-file blob a0423896
hello world!
------------------------------------------------
pages for any of the git commands; one good place to start would be
with the commands mentioned in link:everyday.html[Everyday git]. You
should be able to find any unknown jargon in the
-link:glossary.html[Glosssay].
+link:glossary.html[Glossary].
The link:cvs-migration.html[CVS migration] document explains how to
import a CVS repository into git, and shows how to use git in a
This creates a new directory "myrepo" containing a clone of Alice's
repository. The clone is on an equal footing with the original
-project, posessing its own copy of the original project's history.
+project, possessing its own copy of the original project's history.
Bob then makes some changes and commits them:
shows a list of all the changes that Bob made since he branched from
Alice's master branch.
-After examing those changes, and possibly fixing things, Alice can
+After examining those changes, and possibly fixing things, Alice can
pull the changes into her master branch:
-------------------------------------
$ git branch stable v2.5 # start a new branch named "stable" based
# at v2.5
$ git reset --hard HEAD^ # reset your current branch and working
- # directory its state at HEAD^
+ # directory to its state at HEAD^
-------------------------------------
Be careful with that last command: in addition to losing any changes
$ git grep "hello" v2.5
-------------------------------------
-searches for all occurences of "hello" in v2.5.
+searches for all occurrences of "hello" in v2.5.
If you leave out the commit name, git grep will search any of the
files it manages in your current directory. So
-------------------------------------
allows you to browse any commits from the last 2 weeks of commits
-that modified files under the "drivers" directory.
+that modified files under the "drivers" directory. (Note: you can
+adjust gitk's fonts by holding down the control key while pressing
+"-" or "+".)
Finally, most commands that take filenames will optionally allow you
to precede any filename by a commit, to specify a particular version
-fo the file:
+of the file:
-------------------------------------
$ git diff v2.5:Makefile HEAD:Makefile.in
-------------------------------------
+You can also use "git cat-file -p" to see any such file:
+
+-------------------------------------
+$ git cat-file -p v2.5:Makefile
+-------------------------------------
+
Next Steps
----------
smart enough to perform a close-to-optimal search even in the
case of complex non-linear history with lots of merged branches.
- * link:everyday.html[Everday GIT with 20 Commands Or So]
+ * link:everyday.html[Everyday GIT with 20 Commands Or So]
* link:cvs-migration.html[git for CVS users].
#!/bin/sh
GVF=GIT-VERSION-FILE
-DEF_VER=v1.3.GIT
+DEF_VER=v1.4.GIT
# First try git-describe, then see if there is a version file
# (included in release tarballs), then default
# The ones that do not have to link with lcrypto, lz nor xdiff.
SIMPLE_PROGRAMS = \
- git-get-tar-commit-id$X git-mailsplit$X \
+ git-mailsplit$X \
git-stripspace$X git-daemon$X
# ... and all the rest that could be moved out of bindir to gitexecdir
git-hash-object$X git-index-pack$X git-local-fetch$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-receive-pack$X git-rev-parse$X \
+ git-peek-remote$X git-prune-packed$X git-receive-pack$X \
git-send-pack$X git-shell$X \
git-show-index$X git-ssh-fetch$X \
git-ssh-upload$X git-unpack-file$X \
BUILT_INS = git-log$X git-whatchanged$X git-show$X \
git-count-objects$X git-diff$X git-push$X \
git-grep$X git-add$X git-rm$X git-rev-list$X \
- git-check-ref-format$X \
+ git-check-ref-format$X git-rev-parse$X \
git-init-db$X git-tar-tree$X git-upload-tar$X git-format-patch$X \
- git-ls-files$X git-ls-tree$X \
+ git-ls-files$X git-ls-tree$X git-get-tar-commit-id$X \
git-read-tree$X git-commit-tree$X \
git-apply$X git-show-branch$X git-diff-files$X \
git-diff-index$X git-diff-stages$X git-diff-tree$X git-cat-file$X
diffcore-delta.o log-tree.o
LIB_OBJS = \
- blob.o commit.o connect.o csum-file.o base85.o \
- date.o diff-delta.o entry.o exec_cmd.o ident.o index.o \
+ blob.o commit.o connect.o csum-file.o cache-tree.o base85.o \
+ date.o diff-delta.o entry.o exec_cmd.o ident.o lockfile.o \
object.o pack-check.o patch-delta.o path.o pkt-line.o \
quote.o read-cache.o refs.o run-command.o dir.o \
server-info.o setup.o sha1_file.o sha1_name.o strbuf.o \
BUILTIN_OBJS = \
builtin-log.o builtin-help.o builtin-count.o builtin-diff.o builtin-push.o \
builtin-grep.o builtin-add.o builtin-rev-list.o builtin-check-ref-format.o \
- builtin-rm.o builtin-init-db.o \
+ builtin-rm.o builtin-init-db.o builtin-rev-parse.o \
builtin-tar-tree.o builtin-upload-tar.o \
builtin-ls-files.o builtin-ls-tree.o \
builtin-read-tree.o builtin-commit-tree.o \
rm -f $@ && ln git$X $@
common-cmds.h: Documentation/git-*.txt
- ./generate-cmdlist.sh > $@
+ ./generate-cmdlist.sh > $@+
+ mv $@+ $@
$(patsubst %.sh,%,$(SCRIPT_SH)) : % : %.sh
- rm -f $@
+ rm -f $@ $@+
sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
-e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
-e 's/@@NO_CURL@@/$(NO_CURL)/g' \
-e 's/@@NO_PYTHON@@/$(NO_PYTHON)/g' \
- $@.sh >$@
- chmod +x $@
+ $@.sh >$@+
+ chmod +x $@+
+ mv $@+ $@
$(patsubst %.perl,%,$(SCRIPT_PERL)) : % : %.perl
- rm -f $@
+ rm -f $@ $@+
sed -e '1s|#!.*perl|#!$(PERL_PATH_SQ)|' \
-e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
- $@.perl >$@
- chmod +x $@
+ $@.perl >$@+
+ chmod +x $@+
+ mv $@+ $@
$(patsubst %.py,%,$(SCRIPT_PYTHON)) : % : %.py
- rm -f $@
+ rm -f $@ $@+
sed -e '1s|#!.*python|#!$(PYTHON_PATH_SQ)|' \
-e 's|@@GIT_PYTHON_PATH@@|$(GIT_PYTHON_DIR_SQ)|g' \
-e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
- $@.py >$@
- chmod +x $@
+ $@.py >$@+
+ chmod +x $@+
+ mv $@+ $@
git-cherry-pick: git-revert
- cp $< $@
+ cp $< $@+
+ mv $@+ $@
git-status: git-commit
- cp $< $@
+ cp $< $@+
+ mv $@+ $@
# These can record GIT_VERSION
git$X git.spec \
$(CC) -o $*.o -c $(ALL_CFLAGS) -DGIT_USER_AGENT='"git/$(GIT_VERSION)"' $<
ifdef NO_EXPAT
-http-fetch.o: http-fetch.c
+http-fetch.o: http-fetch.c http.h
$(CC) -o $*.o -c $(ALL_CFLAGS) -DNO_EXPAT $<
endif
git-imap-send$X: imap-send.o $(LIB_FILE)
+http.o http-fetch.o http-push.o: http.h
git-http-fetch$X: fetch.o http.o http-fetch.o $(LIB_FILE)
$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \
$(LIBS) $(CURL_LIBCURL) $(EXPAT_LIBEXPAT)
test-delta$X: test-delta.c diff-delta.o patch-delta.o
$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $^
+test-dump-cache-tree$X: dump-cache-tree.o $(GITLIBS)
+ $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)
+
check:
for i in *.c; do sparse $(ALL_CFLAGS) $(SPARSE_FLAGS) $$i || exit; done
### Maintainer's dist rules
git.spec: git.spec.in
- sed -e 's/@@VERSION@@/$(GIT_VERSION)/g' < $< > $@
+ sed -e 's/@@VERSION@@/$(GIT_VERSION)/g' < $< > $@+
+ mv $@+ $@
GIT_TARNAME=git-$(GIT_VERSION)
dist: git.spec git-tar-tree
:
rm -fr .doc-tmp-dir
mkdir .doc-tmp-dir .doc-tmp-dir/man1 .doc-tmp-dir/man7
- $(MAKE) -C Documentation DESTDIR=. \
+ $(MAKE) -C Documentation DESTDIR=./ \
man1=../.doc-tmp-dir/man1 \
man7=../.doc-tmp-dir/man7 \
install
rm -f *.spec *.pyc *.pyo */*.pyc */*.pyo common-cmds.h TAGS tags
rm -rf $(GIT_TARNAME) .doc-tmp-dir
rm -f $(GIT_TARNAME).tar.gz git-core_$(GIT_VERSION)-*.tar.gz
- rm -f $(htmldocs).tar $(manpages).tar
+ rm -f $(htmldocs).tar.gz $(manpages).tar.gz
$(MAKE) -C Documentation/ clean
$(MAKE) -C templates clean
$(MAKE) -C t/ clean
*) echo "no link: $$v";; \
esac ; \
done | sort
-
#define DEBUG 0
-static const char blame_usage[] = "[-c] [-l] [--] file [commit]\n"
+static const char blame_usage[] = "[-c] [-l] [-t] [-S <revs-file>] [--] 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"
+ " -t, --time Show raw timestamp (Default: off)\n"
+ " -S, --revs-file Use revisions from revs-file instead of calling git-rev-list\n"
" -h, --help This message";
static struct commit **blame_lines;
free(p);
}
-static int get_blob_sha1_internal(unsigned char *sha1, const char *base,
+static int get_blob_sha1_internal(const unsigned char *sha1, const char *base,
int baselen, const char *pathname,
unsigned mode, int stage);
return 0;
}
-static int get_blob_sha1_internal(unsigned char *sha1, const char *base,
+static int get_blob_sha1_internal(const unsigned char *sha1, const char *base,
int baselen, const char *pathname,
unsigned mode, int stage)
{
*tmp = 0;
}
-static const char* format_time(unsigned long time, const char* tz_str)
+static const char* format_time(unsigned long time, const char* tz_str,
+ int show_raw_time)
{
static char time_buf[128];
time_t t = time;
int minutes, tz;
struct tm *tm;
+ if (show_raw_time) {
+ sprintf(time_buf, "%lu %s", time, tz_str);
+ return time_buf;
+ }
+
tz = atoi(tz_str);
minutes = tz < 0 ? -tz : tz;
minutes = (minutes / 100)*60 + (minutes % 100);
char filename_buf[256];
int sha1_len = 8;
int compability = 0;
+ int show_raw_time = 0;
int options = 1;
struct commit* start_commit;
!strcmp(argv[i], "--compability")) {
compability = 1;
continue;
+ } else if(!strcmp(argv[i], "-t") ||
+ !strcmp(argv[i], "--time")) {
+ show_raw_time = 1;
+ continue;
} else if(!strcmp(argv[i], "-S")) {
if (i + 1 < argc &&
!read_ancestry(argv[i + 1], &sha1_p)) {
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);
+ format_time(ci.author_time, ci.author_tz,
+ show_raw_time),
+ i+1);
} else {
if (found_rename)
printf(" %-*.*s", longest_file, longest_file,
u->pathname);
printf(" (%-*.*s %10s %*d) ",
longest_author, longest_author, ci.author,
- format_time(ci.author_time, ci.author_tz),
+ format_time(ci.author_time, ci.author_tz,
+ show_raw_time),
max_digits, i+1);
}
#include "cache.h"
#include "builtin.h"
#include "dir.h"
+#include "cache-tree.h"
static const char builtin_add_usage[] =
"git-add [-n] [-v] <filepattern>...";
die("unable to add %s to index",path);
if (verbose)
printf("add '%s'\n", path);
+ cache_tree_invalidate_path(active_cache_tree, path);
return 0;
}
-static struct cache_file cache_file;
+static struct lock_file lock_file;
int cmd_add(int argc, const char **argv, char **envp)
{
git_config(git_default_config);
- newfd = hold_index_file_for_update(&cache_file, get_index_file());
+ newfd = hold_lock_file_for_update(&lock_file, get_index_file());
if (newfd < 0)
- die("unable to create new cachefile");
+ die("unable to create new index file");
if (read_cache() < 0)
die("index file corrupt");
if (active_cache_changed) {
if (write_cache(newfd, active_cache, active_nr) ||
- commit_index_file(&cache_file))
+ commit_lock_file(&lock_file))
die("Unable to write new index file");
}
*/
#include <fnmatch.h>
#include "cache.h"
+#include "cache-tree.h"
#include "quote.h"
#include "blob.h"
#include "delta.h"
static int apply_one_fragment(struct buffer_desc *desc, struct fragment *frag)
{
+ int match_beginning, match_end;
char *buf = desc->buffer;
const char *patch = frag->patch;
int offset, size = frag->size;
newlines = new;
leading = frag->leading;
trailing = frag->trailing;
+
+ /*
+ * If we don't have any leading/trailing data in the patch,
+ * we want it to match at the beginning/end of the file.
+ */
+ match_beginning = !leading && (frag->oldpos == 1);
+ match_end = !trailing;
+
lines = 0;
pos = frag->newpos;
for (;;) {
offset = find_offset(buf, desc->size, oldlines, oldsize, pos, &lines);
+ if (match_end && offset + oldsize != desc->size)
+ offset = -1;
+ if (match_beginning && offset)
+ offset = -1;
if (offset >= 0) {
int diff = newsize - oldsize;
unsigned long size = desc->size + diff;
/* Am I at my context limits? */
if ((leading <= p_context) && (trailing <= p_context))
break;
+ if (match_beginning || match_end) {
+ match_beginning = match_end = 0;
+ continue;
+ }
/* Reduce the number of context lines
* Reduce both leading and trailing if they are equal
* otherwise just reduce the larger context.
if (write_index) {
if (remove_file_from_cache(patch->old_name) < 0)
die("unable to remove %s from index", patch->old_name);
+ cache_tree_invalidate_path(active_cache_tree, patch->old_name);
}
if (!cached)
unlink(patch->old_name);
if (!mode)
mode = S_IFREG | 0644;
- create_one_file(path, mode, buf, size);
+ create_one_file(path, mode, buf, size);
add_index_file(path, mode, buf, size);
+ cache_tree_invalidate_path(active_cache_tree, path);
}
static void write_out_one_result(struct patch *patch)
}
}
-static struct cache_file cache_file;
+static struct lock_file lock_file;
static struct excludes {
struct excludes *next;
apply = 0;
write_index = check_index && apply;
- if (write_index && newfd < 0)
- newfd = hold_index_file_for_update(&cache_file, get_index_file());
+ if (write_index && newfd < 0) {
+ newfd = hold_lock_file_for_update(&lock_file,
+ get_index_file());
+ if (newfd < 0)
+ die("unable to create new index file");
+ }
if (check_index) {
if (read_cache() < 0)
die("unable to read index file");
if (write_index) {
if (write_cache(newfd, active_cache, active_nr) ||
- commit_index_file(&cache_file))
- die("Unable to write new cachefile");
+ commit_lock_file(&lock_file))
+ die("Unable to write new index file");
}
return 0;
/* custom pretty-print here */
if (!strcmp(type, tree_type))
- return execl_git_cmd("ls-tree", argv[2], NULL);
+ return cmd_ls_tree(2, argv + 1, NULL);
buf = read_sha1_file(sha1, type, &size);
if (!buf)
if (opt->diffopt.detect_rename)
opt->diffopt.setup |= (DIFF_SETUP_USE_SIZE_CACHE |
DIFF_SETUP_USE_CACHE);
- while (fgets(line, sizeof(line), stdin))
- if (line[0] == '\n')
+ while (fgets(line, sizeof(line), stdin)) {
+ unsigned char sha1[20];
+
+ if (get_sha1_hex(line, sha1)) {
+ fputs(line, stdout);
fflush(stdout);
+ }
else
diff_tree_stdin(line);
-
+ }
return 0;
}
push_arg("-n");
if (opt->regflags & REG_EXTENDED)
push_arg("-E");
+ if (opt->regflags & REG_ICASE)
+ push_arg("-i");
if (opt->word_regexp)
push_arg("-w");
if (opt->name_only)
struct tree_desc *tree,
const char *tree_name, const char *base)
{
- unsigned mode;
int len;
int hit = 0;
- const char *path;
- const unsigned char *sha1;
+ struct name_entry entry;
char *down;
char *path_buf = xmalloc(PATH_MAX + strlen(tree_name) + 100);
}
len = strlen(path_buf);
- while (tree->size) {
- int pathlen;
- sha1 = tree_entry_extract(tree, &path, &mode);
- pathlen = strlen(path);
- strcpy(path_buf + len, path);
+ while (tree_entry(tree, &entry)) {
+ strcpy(path_buf + len, entry.path);
- if (S_ISDIR(mode))
+ if (S_ISDIR(entry.mode))
/* Match "abc/" against pathspec to
* decide if we want to descend into "abc"
* directory.
*/
- strcpy(path_buf + len + pathlen, "/");
+ strcpy(path_buf + len + entry.pathlen, "/");
if (!pathspec_matches(paths, down))
;
- else if (S_ISREG(mode))
- hit |= grep_sha1(opt, sha1, path_buf);
- else if (S_ISDIR(mode)) {
+ else if (S_ISREG(entry.mode))
+ hit |= grep_sha1(opt, entry.sha1, path_buf);
+ else if (S_ISDIR(entry.mode)) {
char type[20];
struct tree_desc sub;
void *data;
- data = read_sha1_file(sha1, type, &sub.size);
+ data = read_sha1_file(entry.sha1, type, &sub.size);
if (!data)
die("unable to read tree (%s)",
- sha1_to_hex(sha1));
+ sha1_to_hex(entry.sha1));
sub.buf = data;
hit |= grep_tree(opt, paths, &sub, tree_name, down);
free(data);
}
- update_tree_entry(tree);
}
return hit;
}
git_config(git_default_config);
/*
+ * We would have created the above under user's umask -- under
+ * shared-repository settings, we would need to fix them up.
+ */
+ if (shared_repository) {
+ path[len] = 0;
+ adjust_shared_perm(path);
+ strcpy(path + len, "refs");
+ adjust_shared_perm(path);
+ strcpy(path + len, "refs/heads");
+ adjust_shared_perm(path);
+ strcpy(path + len, "refs/tags");
+ adjust_shared_perm(path);
+ }
+
+ /*
* Create the default symlink from ".git/HEAD" to the "master"
* branch, if it does not exist yet.
*/
safe_create_dir(path, 1);
if (shared_repository)
- git_config_set("core.sharedRepository", "true");
+ git_config_set("core.sharedrepository", "true");
return 0;
}
init_revisions(&rev);
rev.diff = 1;
rev.diffopt.recursive = 1;
+ rev.simplify_history = 0;
return cmd_log_wc(argc, argv, envp, &rev);
}
(c >= '0' && c <= '9') || c == '.' || c == '_';
}
+static char *extra_headers = NULL;
+static int extra_headers_size = 0;
+
+static int git_format_config(const char *var, const char *value)
+{
+ if (!strcmp(var, "format.headers")) {
+ int len = strlen(value);
+ extra_headers_size += len + 1;
+ extra_headers = realloc(extra_headers, extra_headers_size);
+ extra_headers[extra_headers_size - len - 1] = 0;
+ strcat(extra_headers, value);
+ return 0;
+ }
+ return git_default_config(var, value);
+}
+
+
static FILE *realstdout = NULL;
-static char *output_directory = NULL;
+static const char *output_directory = NULL;
static void reopen_stdout(struct commit *commit, int nr, int keep_subject)
{
int len = 0;
if (output_directory) {
- strncpy(filename, output_directory, 1010);
+ safe_strncpy(filename, output_directory, 1010);
len = strlen(filename);
if (filename[len - 1] != '/')
filename[len++] = '/';
int nr = 0, total, i, j;
int use_stdout = 0;
int numbered = 0;
+ int start_number = -1;
int keep_subject = 0;
+ char *add_signoff = NULL;
init_revisions(&rev);
rev.commit_format = CMIT_FMT_EMAIL;
rev.ignore_merges = 1;
rev.diffopt.output_format = DIFF_FORMAT_PATCH;
+ git_config(git_format_config);
+ rev.extra_headers = extra_headers;
+
/*
* Parse the arguments before setup_revisions(), or something
* like "git fmt-patch -o a123 HEAD^.." may fail; a123 is
else if (!strcmp(argv[i], "-n") ||
!strcmp(argv[i], "--numbered"))
numbered = 1;
+ else if (!strncmp(argv[i], "--start-number=", 15))
+ start_number = strtol(argv[i] + 15, NULL, 10);
+ else if (!strcmp(argv[i], "--start-number")) {
+ i++;
+ if (i == argc)
+ die("Need a number for --start-number");
+ start_number = strtol(argv[i], NULL, 10);
+ }
else if (!strcmp(argv[i], "-k") ||
!strcmp(argv[i], "--keep-subject")) {
keep_subject = 1;
rev.total = -1;
- } else if (!strcmp(argv[i], "-o")) {
- if (argc < 3)
- die ("Which directory?");
- if (mkdir(argv[i + 1], 0777) < 0 && errno != EEXIST)
- die("Could not create directory %s",
- argv[i + 1]);
- output_directory = strdup(argv[i + 1]);
+ }
+ else if (!strcmp(argv[i], "--output-directory") ||
+ !strcmp(argv[i], "-o")) {
i++;
+ if (argc <= i)
+ die("Which directory?");
+ if (output_directory)
+ die("Two output directories?");
+ output_directory = argv[i];
+ }
+ else if (!strcmp(argv[i], "--signoff") ||
+ !strcmp(argv[i], "-s")) {
+ const char *committer = git_committer_info(1);
+ const char *endpos = strchr(committer, '>');
+ if (!endpos)
+ die("bogos committer info %s\n", committer);
+ add_signoff = xmalloc(endpos - committer + 2);
+ memcpy(add_signoff, committer, endpos - committer + 1);
+ add_signoff[endpos - committer + 1] = 0;
}
else if (!strcmp(argv[i], "--attach"))
rev.mime_boundary = git_version_string;
}
argc = j;
- if (numbered && keep_subject < 0)
+ if (start_number < 0)
+ start_number = 1;
+ if (numbered && keep_subject)
die ("-n and -k are mutually exclusive.");
argc = setup_revisions(argc, argv, &rev, "HEAD");
if (argc > 1)
die ("unrecognized argument: %s", argv[1]);
+ if (output_directory) {
+ if (use_stdout)
+ die("standard output, or directory, which one?");
+ if (mkdir(output_directory, 0777) < 0 && errno != EEXIST)
+ die("Could not create directory %s",
+ output_directory);
+ }
+
if (rev.pending_objects && rev.pending_objects->next == NULL) {
rev.pending_objects->item->flags |= UNINTERESTING;
add_head(&rev);
}
total = nr;
if (numbered)
- rev.total = total;
+ rev.total = total + start_number - 1;
+ rev.add_signoff = add_signoff;
while (0 <= --nr) {
int shown;
commit = list[nr];
- rev.nr = total - nr;
+ rev.nr = total - nr + (start_number - 1);
if (!use_stdout)
reopen_stdout(commit, rev.nr, keep_subject);
shown = log_tree_commit(&rev, commit);
free(commit->buffer);
commit->buffer = NULL;
+
+ /* We put one extra blank line between formatted
+ * patches and this flag is used by log-tree code
+ * to see if it needs to emit a LF before showing
+ * the log; when using one file per patch, we do
+ * not want the extra blank line.
+ */
+ if (!use_stdout)
+ rev.shown_one = 0;
if (shown) {
if (rev.mime_boundary)
printf("\n--%s%s--\n\n\n",
if (!use_stdout)
fclose(stdout);
}
- if (output_directory)
- free(output_directory);
free(list);
return 0;
}
}
}
-static int show_tree(unsigned char *sha1, const char *base, int baselen,
+static int show_tree(const unsigned char *sha1, const char *base, int baselen,
const char *pathname, unsigned mode, int stage)
{
int retval = 0;
{
const char *uri[MAX_URI];
int i, n;
- int remote;
+ int common_argc;
const char **argv;
int argc;
argv[argc++] = "--force";
if (execute)
argv[argc++] = execute;
- if (thin)
- argv[argc++] = "--thin";
- remote = argc;
- argv[argc++] = "dummy-remote";
- while (refspec_nr--)
- argv[argc++] = *refspec++;
- argv[argc] = NULL;
+ common_argc = argc;
for (i = 0; i < n; i++) {
int error;
+ int dest_argc = common_argc;
+ int dest_refspec_nr = refspec_nr;
+ const char **dest_refspec = refspec;
const char *dest = uri[i];
const char *sender = "git-send-pack";
if (!strncmp(dest, "http://", 7) ||
!strncmp(dest, "https://", 8))
sender = "git-http-push";
+ else if (thin)
+ argv[dest_argc++] = "--thin";
argv[0] = sender;
- argv[remote] = dest;
+ argv[dest_argc++] = dest;
+ while (dest_refspec_nr--)
+ argv[dest_argc++] = *dest_refspec++;
+ argv[dest_argc] = NULL;
error = run_command_v(argc, argv);
if (!error)
continue;
#include "object.h"
#include "tree.h"
+#include "tree-walk.h"
+#include "cache-tree.h"
#include <sys/time.h>
#include <signal.h>
#include "builtin.h"
static int aggressive = 0;
static int verbose_update = 0;
static volatile int progress_update = 0;
+static const char *prefix = NULL;
static int head_idx = -1;
static int merge_size = 0;
static struct object_list *trees = NULL;
-static struct cache_entry df_conflict_entry = {
+static struct cache_entry df_conflict_entry = {
+};
+
+struct tree_entry_list {
+ struct tree_entry_list *next;
+ unsigned directory : 1;
+ unsigned executable : 1;
+ unsigned symlink : 1;
+ unsigned int mode;
+ const char *name;
+ const unsigned char *sha1;
};
static struct tree_entry_list df_conflict_list = {
typedef int (*merge_fn_t)(struct cache_entry **src);
-static int entcmp(char *name1, int dir1, char *name2, int dir2)
+static struct tree_entry_list *create_tree_entry_list(struct tree *tree)
+{
+ struct tree_desc desc;
+ struct name_entry one;
+ struct tree_entry_list *ret = NULL;
+ struct tree_entry_list **list_p = &ret;
+
+ desc.buf = tree->buffer;
+ desc.size = tree->size;
+
+ while (tree_entry(&desc, &one)) {
+ struct tree_entry_list *entry;
+
+ entry = xmalloc(sizeof(struct tree_entry_list));
+ entry->name = one.path;
+ entry->sha1 = one.sha1;
+ entry->mode = one.mode;
+ entry->directory = S_ISDIR(one.mode) != 0;
+ entry->executable = (one.mode & S_IXUSR) != 0;
+ entry->symlink = S_ISLNK(one.mode) != 0;
+ entry->next = NULL;
+
+ *list_p = entry;
+ list_p = &entry->next;
+ }
+ return ret;
+}
+
+static int entcmp(const char *name1, int dir1, const char *name2, int dir2)
{
int len1 = strlen(name1);
int len2 = strlen(name2);
int src_size = len + 1;
do {
int i;
- char *first;
+ const char *first;
int firstdir = 0;
int pathlen;
unsigned ce_size;
}
if (posns[i]->directory) {
+ struct tree *tree = lookup_tree(posns[i]->sha1);
any_dirs = 1;
- parse_tree(posns[i]->item.tree);
- subposns[i] = posns[i]->item.tree->entries;
+ parse_tree(tree);
+ subposns[i] = create_tree_entry_list(tree);
posns[i] = posns[i]->next;
src[i + merge] = &df_conflict_entry;
continue;
any_files = 1;
- memcpy(ce->sha1, posns[i]->item.any->sha1, 20);
+ memcpy(ce->sha1, posns[i]->sha1, 20);
src[i + merge] = ce;
subposns[i] = &df_conflict_list;
posns[i] = posns[i]->next;
fprintf(stderr, "%4u%% (%u/%u) done\r",
percent, cnt, total);
last_percent = percent;
+ progress_update = 0;
}
}
}
if (len) {
posns = xmalloc(len * sizeof(struct tree_entry_list *));
for (i = 0; i < len; i++) {
- posns[i] = ((struct tree *) posn->item)->entries;
+ posns[i] = create_tree_entry_list((struct tree *) posn->item);
posn = posn->next;
}
- if (unpack_trees_rec(posns, len, "", fn, &indpos))
+ if (unpack_trees_rec(posns, len, prefix ? prefix : "",
+ fn, &indpos))
return -1;
}
die("Entry '%s' not uptodate. Cannot merge.", ce->name);
}
+static void invalidate_ce_path(struct cache_entry *ce)
+{
+ if (ce)
+ cache_tree_invalidate_path(active_cache_tree, ce->name);
+}
+
/*
* We do not want to remove or overwrite a working tree file that
* is not tracked.
*merge = *old;
} else {
verify_uptodate(old);
+ invalidate_ce_path(old);
}
}
- else
+ else {
verify_absent(merge->name, "overwritten");
+ invalidate_ce_path(merge);
+ }
merge->ce_flags &= ~htons(CE_STAGEMASK);
add_cache_entry(merge, ADD_CACHE_OK_TO_ADD);
verify_absent(ce->name, "removed");
ce->ce_mode = 0;
add_cache_entry(ce, ADD_CACHE_OK_TO_ADD);
+ invalidate_ce_path(ce);
return 1;
}
}
/*
+ * Bind merge.
+ *
+ * Keep the index entries at stage0, collapse stage1 but make sure
+ * stage0 does not have anything there.
+ */
+static int bind_merge(struct cache_entry **src)
+{
+ struct cache_entry *old = src[0];
+ struct cache_entry *a = src[1];
+
+ if (merge_size != 1)
+ return error("Cannot do a bind merge of %d trees\n",
+ merge_size);
+ if (a && old)
+ die("Entry '%s' overlaps. Cannot bind.", a->name);
+ if (!a)
+ return keep_entry(old);
+ else
+ return merged_entry(a, NULL);
+}
+
+/*
* One-way merge.
*
* The rule is:
static int read_cache_unmerged(void)
{
- int i, deleted;
+ int i;
struct cache_entry **dst;
+ struct cache_entry *last = NULL;
read_cache();
dst = active_cache;
- deleted = 0;
for (i = 0; i < active_nr; i++) {
struct cache_entry *ce = active_cache[i];
if (ce_stage(ce)) {
- deleted++;
- continue;
+ if (last && !strcmp(ce->name, last->name))
+ continue;
+ invalidate_ce_path(ce);
+ last = ce;
+ ce->ce_mode = 0;
+ ce->ce_flags &= ~htons(CE_STAGEMASK);
}
- if (deleted)
- *dst = ce;
- dst++;
+ *dst++ = ce;
}
- active_nr -= deleted;
- return deleted;
+ active_nr = dst - active_cache;
+ return !!last;
}
-static const char read_tree_usage[] = "git-read-tree (<sha> | -m [--aggressive] [-u | -i] <sha1> [<sha2> [<sha3>]])";
+static void prime_cache_tree_rec(struct cache_tree *it, struct tree *tree)
+{
+ struct tree_desc desc;
+ struct name_entry entry;
+ int cnt;
+
+ memcpy(it->sha1, tree->object.sha1, 20);
+ desc.buf = tree->buffer;
+ desc.size = tree->size;
+ cnt = 0;
+ while (tree_entry(&desc, &entry)) {
+ if (!S_ISDIR(entry.mode))
+ cnt++;
+ else {
+ struct cache_tree_sub *sub;
+ struct tree *subtree = lookup_tree(entry.sha1);
+ if (!subtree->object.parsed)
+ parse_tree(subtree);
+ sub = cache_tree_sub(it, entry.path);
+ sub->cache_tree = cache_tree();
+ prime_cache_tree_rec(sub->cache_tree, subtree);
+ cnt += sub->cache_tree->entry_count;
+ }
+ }
+ it->entry_count = cnt;
+}
-static struct cache_file cache_file;
+static void prime_cache_tree(void)
+{
+ struct tree *tree = (struct tree *)trees->item;
+ if (!tree)
+ return;
+ active_cache_tree = cache_tree();
+ prime_cache_tree_rec(active_cache_tree, tree);
+
+}
+
+static const char read_tree_usage[] = "git-read-tree (<sha> | [[-m [--aggressive] | --reset | --prefix=<prefix>] [-u | -i]] <sha1> [<sha2> [<sha3>]])";
+
+static struct lock_file lock_file;
int cmd_read_tree(int argc, const char **argv, char **envp)
{
setup_git_directory();
git_config(git_default_config);
- newfd = hold_index_file_for_update(&cache_file, get_index_file());
+ newfd = hold_lock_file_for_update(&lock_file, get_index_file());
if (newfd < 0)
- die("unable to create new cachefile");
+ die("unable to create new index file");
git_config(git_default_config);
continue;
}
- /* This differs from "-m" in that we'll silently ignore unmerged entries */
+ /* "--prefix=<subdirectory>/" means keep the current index
+ * entries and put the entries from the tree under the
+ * given subdirectory.
+ */
+ if (!strncmp(arg, "--prefix=", 9)) {
+ if (stage || merge || prefix)
+ usage(read_tree_usage);
+ prefix = arg + 9;
+ merge = 1;
+ stage = 1;
+ if (read_cache_unmerged())
+ die("you need to resolve your current index first");
+ continue;
+ }
+
+ /* This differs from "-m" in that we'll silently ignore
+ * unmerged entries and overwrite working tree files that
+ * correspond to them.
+ */
if (!strcmp(arg, "--reset")) {
- if (stage || merge)
+ if (stage || merge || prefix)
usage(read_tree_usage);
reset = 1;
merge = 1;
/* "-m" stands for "merge", meaning we start in stage 1 */
if (!strcmp(arg, "-m")) {
- if (stage || merge)
+ if (stage || merge || prefix)
usage(read_tree_usage);
if (read_cache_unmerged())
die("you need to resolve your current index first");
if ((update||index_only) && !merge)
usage(read_tree_usage);
+ if (prefix) {
+ int pfxlen = strlen(prefix);
+ int pos;
+ if (prefix[pfxlen-1] != '/')
+ die("prefix must end with /");
+ if (stage != 2)
+ die("binding merge takes only one tree");
+ pos = cache_name_pos(prefix, pfxlen);
+ if (0 <= pos)
+ die("corrupt index file");
+ pos = -pos-1;
+ if (pos < active_nr &&
+ !strncmp(active_cache[pos]->name, prefix, pfxlen))
+ die("subdirectory '%s' already exists.", prefix);
+ pos = cache_name_pos(prefix, pfxlen-1);
+ if (0 <= pos)
+ die("file '%.*s' already exists.", pfxlen-1, prefix);
+ }
+
if (merge) {
if (stage < 2)
die("just how do you expect me to merge %d trees?", stage-1);
switch (stage - 1) {
case 1:
- fn = oneway_merge;
+ fn = prefix ? bind_merge : oneway_merge;
break;
case 2:
fn = twoway_merge;
break;
case 3:
- fn = threeway_merge;
- break;
default:
fn = threeway_merge;
+ cache_tree_free(&active_cache_tree);
break;
}
}
unpack_trees(fn);
+
+ /*
+ * When reading only one tree (either the most basic form,
+ * "-m ent" or "--reset ent" form), we can obtain a fully
+ * valid cache-tree because the index must match exactly
+ * what came from the tree.
+ */
+ if (trees && trees->item && !prefix && (!merge || (stage == 2))) {
+ cache_tree_free(&active_cache_tree);
+ prime_cache_tree();
+ }
+
if (write_cache(newfd, active_cache, active_nr) ||
- commit_index_file(&cache_file))
+ commit_lock_file(&lock_file))
die("unable to write new index file");
return 0;
}
if (obj->flags & (UNINTERESTING | SEEN))
return p;
obj->flags |= SEEN;
+ name = strdup(name);
return add_object(obj, p, path, name);
}
const char *name)
{
struct object *obj = &tree->object;
- struct tree_entry_list *entry;
+ struct tree_desc desc;
+ struct name_entry entry;
struct name_path me;
if (!revs.tree_objects)
if (parse_tree(tree) < 0)
die("bad tree object %s", sha1_to_hex(obj->sha1));
obj->flags |= SEEN;
+ name = strdup(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, &me, entry->name);
+
+ desc.buf = tree->buffer;
+ desc.size = tree->size;
+
+ while (tree_entry(&desc, &entry)) {
+ if (S_ISDIR(entry.mode))
+ p = process_tree(lookup_tree(entry.sha1), p, &me, entry.path);
else
- p = process_blob(entry->item.blob, p, &me, entry->name);
- free(entry);
- entry = next;
+ p = process_blob(lookup_blob(entry.sha1), p, &me, entry.path);
}
+ free(tree->buffer);
+ tree->buffer = NULL;
return p;
}
--- /dev/null
+/*
+ * rev-parse.c
+ *
+ * Copyright (C) Linus Torvalds, 2005
+ */
+#include "cache.h"
+#include "commit.h"
+#include "refs.h"
+#include "quote.h"
+#include "builtin.h"
+
+#define DO_REVS 1
+#define DO_NOREV 2
+#define DO_FLAGS 4
+#define DO_NONFLAGS 8
+static int filter = ~0;
+
+static const char *def = NULL;
+
+#define NORMAL 0
+#define REVERSED 1
+static int show_type = NORMAL;
+static int symbolic = 0;
+static int abbrev = 0;
+static int output_sq = 0;
+
+static int revs_count = 0;
+
+/*
+ * Some arguments are relevant "revision" arguments,
+ * others are about output format or other details.
+ * This sorts it all out.
+ */
+static int is_rev_argument(const char *arg)
+{
+ static const char *rev_args[] = {
+ "--all",
+ "--bisect",
+ "--dense",
+ "--branches",
+ "--header",
+ "--max-age=",
+ "--max-count=",
+ "--min-age=",
+ "--no-merges",
+ "--objects",
+ "--objects-edge",
+ "--parents",
+ "--pretty",
+ "--remotes",
+ "--sparse",
+ "--tags",
+ "--topo-order",
+ "--date-order",
+ "--unpacked",
+ NULL
+ };
+ const char **p = rev_args;
+
+ /* accept -<digit>, like traditional "head" */
+ if ((*arg == '-') && isdigit(arg[1]))
+ return 1;
+
+ for (;;) {
+ const char *str = *p++;
+ int len;
+ if (!str)
+ return 0;
+ len = strlen(str);
+ if (!strcmp(arg, str) ||
+ (str[len-1] == '=' && !strncmp(arg, str, len)))
+ return 1;
+ }
+}
+
+/* Output argument as a string, either SQ or normal */
+static void show(const char *arg)
+{
+ if (output_sq) {
+ int sq = '\'', ch;
+
+ putchar(sq);
+ while ((ch = *arg++)) {
+ if (ch == sq)
+ fputs("'\\'", stdout);
+ putchar(ch);
+ }
+ putchar(sq);
+ putchar(' ');
+ }
+ else
+ puts(arg);
+}
+
+/* Output a revision, only if filter allows it */
+static void show_rev(int type, const unsigned char *sha1, const char *name)
+{
+ if (!(filter & DO_REVS))
+ return;
+ def = NULL;
+ revs_count++;
+
+ if (type != show_type)
+ putchar('^');
+ if (symbolic && name)
+ show(name);
+ else if (abbrev)
+ show(find_unique_abbrev(sha1, abbrev));
+ else
+ show(sha1_to_hex(sha1));
+}
+
+/* Output a flag, only if filter allows it. */
+static int show_flag(const char *arg)
+{
+ if (!(filter & DO_FLAGS))
+ return 0;
+ if (filter & (is_rev_argument(arg) ? DO_REVS : DO_NOREV)) {
+ show(arg);
+ return 1;
+ }
+ return 0;
+}
+
+static void show_default(void)
+{
+ const char *s = def;
+
+ if (s) {
+ unsigned char sha1[20];
+
+ def = NULL;
+ if (!get_sha1(s, sha1)) {
+ show_rev(NORMAL, sha1, s);
+ return;
+ }
+ }
+}
+
+static int show_reference(const char *refname, const unsigned char *sha1)
+{
+ show_rev(NORMAL, sha1, refname);
+ return 0;
+}
+
+static void show_datestring(const char *flag, const char *datestr)
+{
+ static char buffer[100];
+
+ /* date handling requires both flags and revs */
+ if ((filter & (DO_FLAGS | DO_REVS)) != (DO_FLAGS | DO_REVS))
+ return;
+ snprintf(buffer, sizeof(buffer), "%s%lu", flag, approxidate(datestr));
+ show(buffer);
+}
+
+static int show_file(const char *arg)
+{
+ show_default();
+ if ((filter & (DO_NONFLAGS|DO_NOREV)) == (DO_NONFLAGS|DO_NOREV)) {
+ show(arg);
+ return 1;
+ }
+ return 0;
+}
+
+int cmd_rev_parse(int argc, const char **argv, char **envp)
+{
+ int i, as_is = 0, verify = 0;
+ unsigned char sha1[20];
+ const char *prefix = setup_git_directory();
+
+ git_config(git_default_config);
+
+ for (i = 1; i < argc; i++) {
+ const char *arg = argv[i];
+ char *dotdot;
+
+ if (as_is) {
+ if (show_file(arg) && as_is < 2)
+ verify_filename(prefix, arg);
+ continue;
+ }
+ if (!strcmp(arg,"-n")) {
+ if (++i >= argc)
+ die("-n requires an argument");
+ if ((filter & DO_FLAGS) && (filter & DO_REVS)) {
+ show(arg);
+ show(argv[i]);
+ }
+ continue;
+ }
+ if (!strncmp(arg,"-n",2)) {
+ if ((filter & DO_FLAGS) && (filter & DO_REVS))
+ show(arg);
+ continue;
+ }
+
+ if (*arg == '-') {
+ if (!strcmp(arg, "--")) {
+ as_is = 2;
+ /* Pass on the "--" if we show anything but files.. */
+ if (filter & (DO_FLAGS | DO_REVS))
+ show_file(arg);
+ continue;
+ }
+ if (!strcmp(arg, "--default")) {
+ def = argv[i+1];
+ i++;
+ continue;
+ }
+ if (!strcmp(arg, "--revs-only")) {
+ filter &= ~DO_NOREV;
+ continue;
+ }
+ if (!strcmp(arg, "--no-revs")) {
+ filter &= ~DO_REVS;
+ continue;
+ }
+ if (!strcmp(arg, "--flags")) {
+ filter &= ~DO_NONFLAGS;
+ continue;
+ }
+ if (!strcmp(arg, "--no-flags")) {
+ filter &= ~DO_FLAGS;
+ continue;
+ }
+ if (!strcmp(arg, "--verify")) {
+ filter &= ~(DO_FLAGS|DO_NOREV);
+ verify = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--short") ||
+ !strncmp(arg, "--short=", 8)) {
+ filter &= ~(DO_FLAGS|DO_NOREV);
+ verify = 1;
+ abbrev = DEFAULT_ABBREV;
+ if (arg[7] == '=')
+ abbrev = strtoul(arg + 8, NULL, 10);
+ if (abbrev < MINIMUM_ABBREV)
+ abbrev = MINIMUM_ABBREV;
+ else if (40 <= abbrev)
+ abbrev = 40;
+ continue;
+ }
+ if (!strcmp(arg, "--sq")) {
+ output_sq = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--not")) {
+ show_type ^= REVERSED;
+ continue;
+ }
+ if (!strcmp(arg, "--symbolic")) {
+ symbolic = 1;
+ continue;
+ }
+ if (!strcmp(arg, "--all")) {
+ for_each_ref(show_reference);
+ continue;
+ }
+ if (!strcmp(arg, "--branches")) {
+ for_each_branch_ref(show_reference);
+ continue;
+ }
+ if (!strcmp(arg, "--tags")) {
+ for_each_tag_ref(show_reference);
+ continue;
+ }
+ if (!strcmp(arg, "--remotes")) {
+ for_each_remote_ref(show_reference);
+ continue;
+ }
+ if (!strcmp(arg, "--show-prefix")) {
+ if (prefix)
+ puts(prefix);
+ continue;
+ }
+ if (!strcmp(arg, "--show-cdup")) {
+ const char *pfx = prefix;
+ while (pfx) {
+ pfx = strchr(pfx, '/');
+ if (pfx) {
+ pfx++;
+ printf("../");
+ }
+ }
+ putchar('\n');
+ continue;
+ }
+ if (!strcmp(arg, "--git-dir")) {
+ const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
+ static char cwd[PATH_MAX];
+ if (gitdir) {
+ puts(gitdir);
+ continue;
+ }
+ if (!prefix) {
+ puts(".git");
+ continue;
+ }
+ if (!getcwd(cwd, PATH_MAX))
+ die("unable to get current working directory");
+ printf("%s/.git\n", cwd);
+ continue;
+ }
+ if (!strncmp(arg, "--since=", 8)) {
+ show_datestring("--max-age=", arg+8);
+ continue;
+ }
+ if (!strncmp(arg, "--after=", 8)) {
+ show_datestring("--max-age=", arg+8);
+ continue;
+ }
+ if (!strncmp(arg, "--before=", 9)) {
+ show_datestring("--min-age=", arg+9);
+ continue;
+ }
+ if (!strncmp(arg, "--until=", 8)) {
+ show_datestring("--min-age=", arg+8);
+ continue;
+ }
+ if (show_flag(arg) && verify)
+ die("Needed a single revision");
+ continue;
+ }
+
+ /* Not a flag argument */
+ dotdot = strstr(arg, "..");
+ if (dotdot) {
+ unsigned char end[20];
+ char *next = dotdot + 2;
+ const char *this = arg;
+ *dotdot = 0;
+ if (!*next)
+ next = "HEAD";
+ if (dotdot == arg)
+ this = "HEAD";
+ if (!get_sha1(this, sha1) && !get_sha1(next, end)) {
+ show_rev(NORMAL, end, next);
+ show_rev(REVERSED, sha1, this);
+ continue;
+ }
+ *dotdot = '.';
+ }
+ if (!get_sha1(arg, sha1)) {
+ show_rev(NORMAL, sha1, arg);
+ continue;
+ }
+ if (*arg == '^' && !get_sha1(arg+1, sha1)) {
+ show_rev(REVERSED, sha1, arg+1);
+ continue;
+ }
+ as_is = 1;
+ if (!show_file(arg))
+ continue;
+ if (verify)
+ die("Needed a single revision");
+ verify_filename(prefix, arg);
+ }
+ show_default();
+ if (verify && revs_count != 1)
+ die("Needed a single revision");
+ return 0;
+}
#include "cache.h"
#include "builtin.h"
#include "dir.h"
+#include "cache-tree.h"
static const char builtin_rm_usage[] =
"git-rm [-n] [-v] [-f] <filepattern>...";
return ret;
}
-static struct cache_file cache_file;
+static struct lock_file lock_file;
int cmd_rm(int argc, const char **argv, char **envp)
{
git_config(git_default_config);
- newfd = hold_index_file_for_update(&cache_file, get_index_file());
+ newfd = hold_lock_file_for_update(&lock_file, get_index_file());
if (newfd < 0)
die("unable to create new index file");
}
die(builtin_rm_usage);
}
- pathspec = get_pathspec(prefix, argv + i);
+ if (argc <= i)
+ usage(builtin_rm_usage);
+ pathspec = get_pathspec(prefix, argv + i);
seen = NULL;
- if (pathspec) {
- for (i = 0; pathspec[i] ; i++)
- /* nothing */;
- seen = xmalloc(i);
- memset(seen, 0, i);
- }
+ for (i = 0; pathspec[i] ; i++)
+ /* nothing */;
+ seen = xmalloc(i);
+ memset(seen, 0, i);
for (i = 0; i < active_nr; i++) {
struct cache_entry *ce = active_cache[i];
if (remove_file_from_cache(path))
die("git rm: unable to remove %s", path);
+ cache_tree_invalidate_path(active_cache_tree, path);
}
+ if (show_only)
+ return 0;
+
/*
* Then, if we used "-f", remove the filenames from the
* workspace. If we fail to remove the first one, we
if (active_cache_changed) {
if (write_cache(newfd, active_cache, active_nr) ||
- commit_index_file(&cache_file))
+ commit_lock_file(&lock_file))
die("Unable to write new index file");
}
int i = path->len;
if (i > maxlen)
i = maxlen;
- while (i > 0 && path->buf[i] != '/')
+ do {
i--;
+ } while (i > 0 && path->buf[i] != '/');
return i;
}
/* XXX: should we provide more meaningful info here? */
sprintf(header.uid, "%07o", 0);
sprintf(header.gid, "%07o", 0);
- strncpy(header.uname, "git", 31);
- strncpy(header.gname, "git", 31);
+ safe_strncpy(header.uname, "git", sizeof(header.uname));
+ safe_strncpy(header.gname, "git", sizeof(header.gname));
sprintf(header.devmajor, "%07o", 0);
sprintf(header.devminor, "%07o", 0);
static void traverse_tree(struct tree_desc *tree, struct strbuf *path)
{
int pathlen = path->len;
+ struct name_entry entry;
- while (tree->size) {
- const char *name;
- const unsigned char *sha1;
- unsigned mode;
+ while (tree_entry(tree, &entry)) {
void *eltbuf;
char elttype[20];
unsigned long eltsize;
- sha1 = tree_entry_extract(tree, &name, &mode);
- update_tree_entry(tree);
-
- eltbuf = read_sha1_file(sha1, elttype, &eltsize);
+ eltbuf = read_sha1_file(entry.sha1, elttype, &eltsize);
if (!eltbuf)
- die("cannot read %s", sha1_to_hex(sha1));
+ die("cannot read %s", sha1_to_hex(entry.sha1));
path->len = pathlen;
- strbuf_append_string(path, name);
- if (S_ISDIR(mode))
+ strbuf_append_string(path, entry.path);
+ if (S_ISDIR(entry.mode))
strbuf_append_string(path, "/");
- write_entry(sha1, path, mode, eltbuf, eltsize);
+ write_entry(entry.sha1, path, entry.mode, eltbuf, eltsize);
- if (S_ISDIR(mode)) {
+ if (S_ISDIR(entry.mode)) {
struct tree_desc subtree;
subtree.buf = eltbuf;
subtree.size = eltsize;
return remote_tar(argc, argv);
return generate_tar(argc, argv, envp);
}
+
+/* ustar header + extended global header content */
+#define HEADERSIZE (2 * RECORDSIZE)
+
+int cmd_get_tar_commit_id(int argc, const char **argv, char **envp)
+{
+ char buffer[HEADERSIZE];
+ struct ustar_header *header = (struct ustar_header *)buffer;
+ char *content = buffer + RECORDSIZE;
+ ssize_t n;
+
+ n = xread(0, buffer, HEADERSIZE);
+ if (n < HEADERSIZE)
+ die("git-get-tar-commit-id: read error");
+ if (header->typeflag[0] != 'g')
+ return 1;
+ if (memcmp(content, "52 comment=", 11))
+ return 1;
+
+ n = xwrite(1, content + 11, 41);
+ if (n < 41)
+ die("git-get-tar-commit-id: write error");
+
+ return 0;
+}
extern int cmd_init_db(int argc, const char **argv, char **envp);
extern int cmd_tar_tree(int argc, const char **argv, char **envp);
extern int cmd_upload_tar(int argc, const char **argv, char **envp);
+extern int cmd_get_tar_commit_id(int argc, const char **argv, char **envp);
extern int cmd_ls_files(int argc, const char **argv, char **envp);
extern int cmd_ls_tree(int argc, const char **argv, char **envp);
extern int cmd_read_tree(int argc, const char **argv, char **envp);
extern int cmd_diff_stages(int argc, const char **argv, char **envp);
extern int cmd_diff_tree(int argc, const char **argv, char **envp);
extern int cmd_cat_file(int argc, const char **argv, char **envp);
+extern int cmd_rev_parse(int argc, const char **argv, char **envp);
#endif
--- /dev/null
+#include "cache.h"
+#include "tree.h"
+#include "cache-tree.h"
+
+#define DEBUG 0
+
+struct cache_tree *cache_tree(void)
+{
+ struct cache_tree *it = xcalloc(1, sizeof(struct cache_tree));
+ it->entry_count = -1;
+ return it;
+}
+
+void cache_tree_free(struct cache_tree **it_p)
+{
+ int i;
+ struct cache_tree *it = *it_p;
+
+ if (!it)
+ return;
+ for (i = 0; i < it->subtree_nr; i++)
+ if (it->down[i])
+ cache_tree_free(&it->down[i]->cache_tree);
+ free(it->down);
+ free(it);
+ *it_p = NULL;
+}
+
+static int subtree_name_cmp(const char *one, int onelen,
+ const char *two, int twolen)
+{
+ if (onelen < twolen)
+ return -1;
+ if (twolen < onelen)
+ return 1;
+ return memcmp(one, two, onelen);
+}
+
+static int subtree_pos(struct cache_tree *it, const char *path, int pathlen)
+{
+ struct cache_tree_sub **down = it->down;
+ int lo, hi;
+ lo = 0;
+ hi = it->subtree_nr;
+ while (lo < hi) {
+ int mi = (lo + hi) / 2;
+ struct cache_tree_sub *mdl = down[mi];
+ int cmp = subtree_name_cmp(path, pathlen,
+ mdl->name, mdl->namelen);
+ if (!cmp)
+ return mi;
+ if (cmp < 0)
+ hi = mi;
+ else
+ lo = mi + 1;
+ }
+ return -lo-1;
+}
+
+static struct cache_tree_sub *find_subtree(struct cache_tree *it,
+ const char *path,
+ int pathlen,
+ int create)
+{
+ struct cache_tree_sub *down;
+ int pos = subtree_pos(it, path, pathlen);
+ if (0 <= pos)
+ return it->down[pos];
+ if (!create)
+ return NULL;
+
+ pos = -pos-1;
+ if (it->subtree_alloc <= it->subtree_nr) {
+ it->subtree_alloc = alloc_nr(it->subtree_alloc);
+ it->down = xrealloc(it->down, it->subtree_alloc *
+ sizeof(*it->down));
+ }
+ it->subtree_nr++;
+
+ down = xmalloc(sizeof(*down) + pathlen + 1);
+ down->cache_tree = NULL;
+ down->namelen = pathlen;
+ memcpy(down->name, path, pathlen);
+ down->name[pathlen] = 0;
+
+ if (pos < it->subtree_nr)
+ memmove(it->down + pos + 1,
+ it->down + pos,
+ sizeof(down) * (it->subtree_nr - pos - 1));
+ it->down[pos] = down;
+ return down;
+}
+
+struct cache_tree_sub *cache_tree_sub(struct cache_tree *it, const char *path)
+{
+ int pathlen = strlen(path);
+ return find_subtree(it, path, pathlen, 1);
+}
+
+void cache_tree_invalidate_path(struct cache_tree *it, const char *path)
+{
+ /* a/b/c
+ * ==> invalidate self
+ * ==> find "a", have it invalidate "b/c"
+ * a
+ * ==> invalidate self
+ * ==> if "a" exists as a subtree, remove it.
+ */
+ const char *slash;
+ int namelen;
+ struct cache_tree_sub *down;
+
+#if DEBUG
+ fprintf(stderr, "cache-tree invalidate <%s>\n", path);
+#endif
+
+ if (!it)
+ return;
+ slash = strchr(path, '/');
+ it->entry_count = -1;
+ if (!slash) {
+ int pos;
+ namelen = strlen(path);
+ pos = subtree_pos(it, path, namelen);
+ if (0 <= pos) {
+ cache_tree_free(&it->down[pos]->cache_tree);
+ free(it->down[pos]);
+ /* 0 1 2 3 4 5
+ * ^ ^subtree_nr = 6
+ * pos
+ * move 4 and 5 up one place (2 entries)
+ * 2 = 6 - 3 - 1 = subtree_nr - pos - 1
+ */
+ memmove(it->down+pos, it->down+pos+1,
+ sizeof(struct cache_tree_sub *) *
+ (it->subtree_nr - pos - 1));
+ it->subtree_nr--;
+ }
+ return;
+ }
+ namelen = slash - path;
+ down = find_subtree(it, path, namelen, 0);
+ if (down)
+ cache_tree_invalidate_path(down->cache_tree, slash + 1);
+}
+
+static int verify_cache(struct cache_entry **cache,
+ int entries)
+{
+ int i, funny;
+
+ /* Verify that the tree is merged */
+ funny = 0;
+ for (i = 0; i < entries; i++) {
+ struct cache_entry *ce = cache[i];
+ if (ce_stage(ce)) {
+ if (10 < ++funny) {
+ fprintf(stderr, "...\n");
+ break;
+ }
+ fprintf(stderr, "%s: unmerged (%s)\n",
+ ce->name, sha1_to_hex(ce->sha1));
+ }
+ }
+ if (funny)
+ return -1;
+
+ /* Also verify that the cache does not have path and path/file
+ * at the same time. At this point we know the cache has only
+ * stage 0 entries.
+ */
+ funny = 0;
+ for (i = 0; i < entries - 1; i++) {
+ /* path/file always comes after path because of the way
+ * the cache is sorted. Also path can appear only once,
+ * which means conflicting one would immediately follow.
+ */
+ const char *this_name = cache[i]->name;
+ const char *next_name = cache[i+1]->name;
+ int this_len = strlen(this_name);
+ if (this_len < strlen(next_name) &&
+ strncmp(this_name, next_name, this_len) == 0 &&
+ next_name[this_len] == '/') {
+ if (10 < ++funny) {
+ fprintf(stderr, "...\n");
+ break;
+ }
+ fprintf(stderr, "You have both %s and %s\n",
+ this_name, next_name);
+ }
+ }
+ if (funny)
+ return -1;
+ return 0;
+}
+
+static void discard_unused_subtrees(struct cache_tree *it)
+{
+ struct cache_tree_sub **down = it->down;
+ int nr = it->subtree_nr;
+ int dst, src;
+ for (dst = src = 0; src < nr; src++) {
+ struct cache_tree_sub *s = down[src];
+ if (s->used)
+ down[dst++] = s;
+ else {
+ cache_tree_free(&s->cache_tree);
+ free(s);
+ it->subtree_nr--;
+ }
+ }
+}
+
+int cache_tree_fully_valid(struct cache_tree *it)
+{
+ int i;
+ if (!it)
+ return 0;
+ if (it->entry_count < 0 || !has_sha1_file(it->sha1))
+ return 0;
+ for (i = 0; i < it->subtree_nr; i++) {
+ if (!cache_tree_fully_valid(it->down[i]->cache_tree))
+ return 0;
+ }
+ return 1;
+}
+
+static int update_one(struct cache_tree *it,
+ struct cache_entry **cache,
+ int entries,
+ const char *base,
+ int baselen,
+ int missing_ok,
+ int dryrun)
+{
+ unsigned long size, offset;
+ char *buffer;
+ int i;
+
+ if (0 <= it->entry_count && has_sha1_file(it->sha1))
+ return it->entry_count;
+
+ /*
+ * We first scan for subtrees and update them; we start by
+ * marking existing subtrees -- the ones that are unmarked
+ * should not be in the result.
+ */
+ for (i = 0; i < it->subtree_nr; i++)
+ it->down[i]->used = 0;
+
+ /*
+ * Find the subtrees and update them.
+ */
+ for (i = 0; i < entries; i++) {
+ struct cache_entry *ce = cache[i];
+ struct cache_tree_sub *sub;
+ const char *path, *slash;
+ int pathlen, sublen, subcnt;
+
+ path = ce->name;
+ pathlen = ce_namelen(ce);
+ if (pathlen <= baselen || memcmp(base, path, baselen))
+ break; /* at the end of this level */
+
+ slash = strchr(path + baselen, '/');
+ if (!slash)
+ continue;
+ /*
+ * a/bbb/c (base = a/, slash = /c)
+ * ==>
+ * path+baselen = bbb/c, sublen = 3
+ */
+ sublen = slash - (path + baselen);
+ sub = find_subtree(it, path + baselen, sublen, 1);
+ if (!sub->cache_tree)
+ sub->cache_tree = cache_tree();
+ subcnt = update_one(sub->cache_tree,
+ cache + i, entries - i,
+ path,
+ baselen + sublen + 1,
+ missing_ok,
+ dryrun);
+ i += subcnt - 1;
+ sub->used = 1;
+ }
+
+ discard_unused_subtrees(it);
+
+ /*
+ * Then write out the tree object for this level.
+ */
+ size = 8192;
+ buffer = xmalloc(size);
+ offset = 0;
+
+ for (i = 0; i < entries; i++) {
+ struct cache_entry *ce = cache[i];
+ struct cache_tree_sub *sub;
+ const char *path, *slash;
+ int pathlen, entlen;
+ const unsigned char *sha1;
+ unsigned mode;
+
+ path = ce->name;
+ pathlen = ce_namelen(ce);
+ if (pathlen <= baselen || memcmp(base, path, baselen))
+ break; /* at the end of this level */
+
+ slash = strchr(path + baselen, '/');
+ if (slash) {
+ entlen = slash - (path + baselen);
+ sub = find_subtree(it, path + baselen, entlen, 0);
+ if (!sub)
+ die("cache-tree.c: '%.*s' in '%s' not found",
+ entlen, path + baselen, path);
+ i += sub->cache_tree->entry_count - 1;
+ sha1 = sub->cache_tree->sha1;
+ mode = S_IFDIR;
+ }
+ else {
+ sha1 = ce->sha1;
+ mode = ntohl(ce->ce_mode);
+ entlen = pathlen - baselen;
+ }
+ if (!missing_ok && !has_sha1_file(sha1))
+ return error("invalid object %s", sha1_to_hex(sha1));
+
+ if (!ce->ce_mode)
+ continue; /* entry being removed */
+
+ if (size < offset + entlen + 100) {
+ size = alloc_nr(offset + entlen + 100);
+ buffer = xrealloc(buffer, size);
+ }
+ offset += sprintf(buffer + offset,
+ "%o %.*s", mode, entlen, path + baselen);
+ buffer[offset++] = 0;
+ memcpy(buffer + offset, sha1, 20);
+ offset += 20;
+
+#if DEBUG
+ fprintf(stderr, "cache-tree update-one %o %.*s\n",
+ mode, entlen, path + baselen);
+#endif
+ }
+
+ if (dryrun) {
+ unsigned char hdr[200];
+ int hdrlen;
+ write_sha1_file_prepare(buffer, offset, tree_type, it->sha1,
+ hdr, &hdrlen);
+ }
+ else
+ write_sha1_file(buffer, offset, tree_type, it->sha1);
+ free(buffer);
+ it->entry_count = i;
+#if DEBUG
+ fprintf(stderr, "cache-tree update-one (%d ent, %d subtree) %s\n",
+ it->entry_count, it->subtree_nr,
+ sha1_to_hex(it->sha1));
+#endif
+ return i;
+}
+
+int cache_tree_update(struct cache_tree *it,
+ struct cache_entry **cache,
+ int entries,
+ int missing_ok,
+ int dryrun)
+{
+ int i;
+ i = verify_cache(cache, entries);
+ if (i)
+ return i;
+ i = update_one(it, cache, entries, "", 0, missing_ok, dryrun);
+ if (i < 0)
+ return i;
+ return 0;
+}
+
+static void *write_one(struct cache_tree *it,
+ char *path,
+ int pathlen,
+ char *buffer,
+ unsigned long *size,
+ unsigned long *offset)
+{
+ int i;
+
+ /* One "cache-tree" entry consists of the following:
+ * path (NUL terminated)
+ * entry_count, subtree_nr ("%d %d\n")
+ * tree-sha1 (missing if invalid)
+ * subtree_nr "cache-tree" entries for subtrees.
+ */
+ if (*size < *offset + pathlen + 100) {
+ *size = alloc_nr(*offset + pathlen + 100);
+ buffer = xrealloc(buffer, *size);
+ }
+ *offset += sprintf(buffer + *offset, "%.*s%c%d %d\n",
+ pathlen, path, 0,
+ it->entry_count, it->subtree_nr);
+
+#if DEBUG
+ if (0 <= it->entry_count)
+ fprintf(stderr, "cache-tree <%.*s> (%d ent, %d subtree) %s\n",
+ pathlen, path, it->entry_count, it->subtree_nr,
+ sha1_to_hex(it->sha1));
+ else
+ fprintf(stderr, "cache-tree <%.*s> (%d subtree) invalid\n",
+ pathlen, path, it->subtree_nr);
+#endif
+
+ if (0 <= it->entry_count) {
+ memcpy(buffer + *offset, it->sha1, 20);
+ *offset += 20;
+ }
+ for (i = 0; i < it->subtree_nr; i++) {
+ struct cache_tree_sub *down = it->down[i];
+ if (i) {
+ struct cache_tree_sub *prev = it->down[i-1];
+ if (subtree_name_cmp(down->name, down->namelen,
+ prev->name, prev->namelen) <= 0)
+ die("fatal - unsorted cache subtree");
+ }
+ buffer = write_one(down->cache_tree, down->name, down->namelen,
+ buffer, size, offset);
+ }
+ return buffer;
+}
+
+void *cache_tree_write(struct cache_tree *root, unsigned long *size_p)
+{
+ char path[PATH_MAX];
+ unsigned long size = 8192;
+ char *buffer = xmalloc(size);
+
+ *size_p = 0;
+ path[0] = 0;
+ return write_one(root, path, 0, buffer, &size, size_p);
+}
+
+static struct cache_tree *read_one(const char **buffer, unsigned long *size_p)
+{
+ const char *buf = *buffer;
+ unsigned long size = *size_p;
+ const char *cp;
+ char *ep;
+ struct cache_tree *it;
+ int i, subtree_nr;
+
+ it = NULL;
+ /* skip name, but make sure name exists */
+ while (size && *buf) {
+ size--;
+ buf++;
+ }
+ if (!size)
+ goto free_return;
+ buf++; size--;
+ it = cache_tree();
+
+ cp = buf;
+ it->entry_count = strtol(cp, &ep, 10);
+ if (cp == ep)
+ goto free_return;
+ cp = ep;
+ subtree_nr = strtol(cp, &ep, 10);
+ if (cp == ep)
+ goto free_return;
+ while (size && *buf && *buf != '\n') {
+ size--;
+ buf++;
+ }
+ if (!size)
+ goto free_return;
+ buf++; size--;
+ if (0 <= it->entry_count) {
+ if (size < 20)
+ goto free_return;
+ memcpy(it->sha1, buf, 20);
+ buf += 20;
+ size -= 20;
+ }
+
+#if DEBUG
+ if (0 <= it->entry_count)
+ fprintf(stderr, "cache-tree <%s> (%d ent, %d subtree) %s\n",
+ *buffer, it->entry_count, subtree_nr,
+ sha1_to_hex(it->sha1));
+ else
+ fprintf(stderr, "cache-tree <%s> (%d subtrees) invalid\n",
+ *buffer, subtree_nr);
+#endif
+
+ /*
+ * Just a heuristic -- we do not add directories that often but
+ * we do not want to have to extend it immediately when we do,
+ * hence +2.
+ */
+ it->subtree_alloc = subtree_nr + 2;
+ it->down = xcalloc(it->subtree_alloc, sizeof(struct cache_tree_sub *));
+ for (i = 0; i < subtree_nr; i++) {
+ /* read each subtree */
+ struct cache_tree *sub;
+ struct cache_tree_sub *subtree;
+ const char *name = buf;
+
+ sub = read_one(&buf, &size);
+ if (!sub)
+ goto free_return;
+ subtree = cache_tree_sub(it, name);
+ subtree->cache_tree = sub;
+ }
+ if (subtree_nr != it->subtree_nr)
+ die("cache-tree: internal error");
+ *buffer = buf;
+ *size_p = size;
+ return it;
+
+ free_return:
+ cache_tree_free(&it);
+ return NULL;
+}
+
+struct cache_tree *cache_tree_read(const char *buffer, unsigned long size)
+{
+ if (buffer[0])
+ return NULL; /* not the whole tree */
+ return read_one(&buffer, &size);
+}
+
+struct cache_tree *cache_tree_find(struct cache_tree *it, const char *path)
+{
+ while (*path) {
+ const char *slash;
+ struct cache_tree_sub *sub;
+
+ slash = strchr(path, '/');
+ if (!slash)
+ slash = path + strlen(path);
+ /* between path and slash is the name of the
+ * subtree to look for.
+ */
+ sub = find_subtree(it, path, slash - path, 0);
+ if (!sub)
+ return NULL;
+ it = sub->cache_tree;
+ if (slash)
+ while (*slash && *slash == '/')
+ slash++;
+ if (!slash || !*slash)
+ return it; /* prefix ended with slashes */
+ path = slash;
+ }
+ return it;
+}
--- /dev/null
+#ifndef CACHE_TREE_H
+#define CACHE_TREE_H
+
+struct cache_tree;
+struct cache_tree_sub {
+ struct cache_tree *cache_tree;
+ int namelen;
+ int used;
+ char name[FLEX_ARRAY];
+};
+
+struct cache_tree {
+ int entry_count; /* negative means "invalid" */
+ unsigned char sha1[20];
+ int subtree_nr;
+ int subtree_alloc;
+ struct cache_tree_sub **down;
+};
+
+struct cache_tree *cache_tree(void);
+void cache_tree_free(struct cache_tree **);
+void cache_tree_invalidate_path(struct cache_tree *, const char *);
+struct cache_tree_sub *cache_tree_sub(struct cache_tree *, const char *);
+
+void *cache_tree_write(struct cache_tree *root, unsigned long *size_p);
+struct cache_tree *cache_tree_read(const char *buffer, unsigned long size);
+
+int cache_tree_fully_valid(struct cache_tree *);
+int cache_tree_update(struct cache_tree *, struct cache_entry **, int, int, int);
+
+struct cache_tree *cache_tree_find(struct cache_tree *, const char *);
+
+#endif
extern struct cache_entry **active_cache;
extern unsigned int active_nr, active_alloc, active_cache_changed;
+extern struct cache_tree *active_cache_tree;
#define GIT_DIR_ENVIRONMENT "GIT_DIR"
#define DEFAULT_GIT_DIR_ENVIRONMENT ".git"
#define REFRESH_IGNORE_MISSING 0x0008 /* ignore non-existent */
extern int refresh_cache(unsigned int flags);
-struct cache_file {
- struct cache_file *next;
- char lockfile[PATH_MAX];
+struct lock_file {
+ struct lock_file *next;
+ char filename[PATH_MAX];
};
-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 *);
+extern int hold_lock_file_for_update(struct lock_file *, const char *path);
+extern int commit_lock_file(struct lock_file *);
+extern void rollback_lock_file(struct lock_file *);
/* Environment bits from configuration mechanism */
extern int trust_executable_bit;
extern int assume_unchanged;
extern int prefer_symlink_refs;
+extern int log_all_ref_updates;
extern int warn_ambiguous_refs;
extern int diff_rename_limit_default;
extern int shared_repository;
int adjust_shared_perm(const char *path);
int safe_create_leading_directories(char *path);
-char *safe_strncpy(char *, const char *, size_t);
+size_t safe_strncpy(char *, const char *, size_t);
char *enter_repo(char *path, int strict);
/* Read and unpack a sha1 file into memory, write memory to a sha1 file */
#include "cache.h"
#include "strbuf.h"
#include "quote.h"
+#include "cache-tree.h"
#define CHECKOUT_ALL 4
static const char *prefix;
static const char checkout_cache_usage[] =
"git-checkout-index [-u] [-q] [-a] [-f] [-n] [--stage=[123]|all] [--prefix=<string>] [--temp] [--] <file>...";
-static struct cache_file cache_file;
+static struct lock_file lock_file;
int main(int argc, char **argv)
{
if (!strcmp(arg, "-u") || !strcmp(arg, "--index")) {
state.refresh_cache = 1;
if (newfd < 0)
- newfd = hold_index_file_for_update
- (&cache_file,
- get_index_file());
+ newfd = hold_lock_file_for_update
+ (&lock_file, get_index_file());
if (newfd < 0)
die("cannot open index.lock file.");
continue;
*/
if (state.refresh_cache) {
close(newfd); newfd = -1;
- rollback_index_file(&cache_file);
+ rollback_lock_file(&lock_file);
}
state.refresh_cache = 0;
}
if (0 <= newfd &&
(write_cache(newfd, active_cache, active_nr) ||
- commit_index_file(&cache_file)))
- die("Unable to write new cachefile");
+ commit_lock_file(&lock_file)))
+ die("Unable to write new index file");
return 0;
}
return 0;
}
+ if (!strcmp(var, "core.logallrefupdates")) {
+ log_all_ref_updates = git_config_bool(var, value);
+ return 0;
+ }
+
if (!strcmp(var, "core.warnambiguousrefs")) {
warn_ambiguous_refs = git_config_bool(var, value);
return 0;
}
if (!strcmp(var, "user.name")) {
- strncpy(git_default_name, value, sizeof(git_default_name));
+ safe_strncpy(git_default_name, value, sizeof(git_default_name));
return 0;
}
if (!strcmp(var, "user.email")) {
- strncpy(git_default_email, value, sizeof(git_default_email));
+ safe_strncpy(git_default_email, value, sizeof(git_default_email));
return 0;
}
if (!strcmp(var, "i18n.commitencoding")) {
- strncpy(git_commit_encoding, value, sizeof(git_commit_encoding));
+ safe_strncpy(git_commit_encoding, value, sizeof(git_commit_encoding));
return 0;
}
* contents of .git/config will be written into it.
*/
fd = open(lock_file, O_WRONLY | O_CREAT | O_EXCL, 0666);
- if (fd < 0) {
+ if (fd < 0 || adjust_shared_perm(lock_file)) {
fprintf(stderr, "could not lock config file\n");
free(store.key);
ret = -1;
#ifndef NO_IPV6
-static int git_tcp_connect(int fd[2], const char *prog, char *host, char *path)
+/*
+ * Returns a connected socket() fd, or else die()s.
+ */
+static int git_tcp_connect_sock(char *host)
{
int sockfd = -1;
char *colon, *end;
die("Unable to look up %s (%s)", host, gai_strerror(gai));
for (ai0 = ai; ai; ai = ai->ai_next) {
- sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+ sockfd = socket(ai->ai_family,
+ ai->ai_socktype, ai->ai_protocol);
if (sockfd < 0)
continue;
if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
if (sockfd < 0)
die("unable to connect a socket (%s)", strerror(errno));
- fd[0] = sockfd;
- fd[1] = sockfd;
- packet_write(sockfd, "%s %s\n", prog, path);
- return 0;
+ return sockfd;
}
#else /* NO_IPV6 */
-static int git_tcp_connect(int fd[2], const char *prog, char *host, char *path)
+/*
+ * Returns a connected socket() fd, or else die()s.
+ */
+static int git_tcp_connect_sock(char *host)
{
int sockfd = -1;
char *colon, *end;
port = colon + 1;
}
-
he = gethostbyname(host);
if (!he)
die("Unable to look up %s (%s)", host, hstrerror(h_errno));
if (sockfd < 0)
die("unable to connect a socket (%s)", strerror(errno));
+ return sockfd;
+}
+
+#endif /* NO_IPV6 */
+
+
+static void git_tcp_connect(int fd[2],
+ const char *prog, char *host, char *path)
+{
+ int sockfd = git_tcp_connect_sock(host);
+
fd[0] = sockfd;
fd[1] = sockfd;
- packet_write(sockfd, "%s %s\n", prog, path);
- return 0;
}
-#endif /* NO_IPV6 */
static char *git_proxy_command = NULL;
static const char *rhost_name = NULL;
return (git_proxy_command && *git_proxy_command);
}
-static int git_proxy_connect(int fd[2], const char *prog, char *host, char *path)
+static void git_proxy_connect(int fd[2],
+ const char *prog, char *host, char *path)
{
char *port = STR(DEFAULT_GIT_PORT);
char *colon, *end;
execlp(git_proxy_command, git_proxy_command, host, port, NULL);
die("exec failed");
}
+ if (pid < 0)
+ die("fork failed");
fd[0] = pipefd[0][0];
fd[1] = pipefd[1][1];
close(pipefd[0][1]);
close(pipefd[1][0]);
- packet_write(fd[1], "%s %s\n", prog, path);
- return pid;
}
/*
}
if (protocol == PROTO_GIT) {
- int ret;
+ /* These underlying connection commands die() if they
+ * cannot connect.
+ */
+ char *target_host = strdup(host);
if (git_use_proxy(host))
- ret = git_proxy_connect(fd, prog, host, path);
+ git_proxy_connect(fd, prog, host, path);
else
- ret = git_tcp_connect(fd, prog, host, path);
+ git_tcp_connect(fd, prog, host, path);
+ /*
+ * Separate original protocol components prog and path
+ * from extended components with a NUL byte.
+ */
+ packet_write(fd[1],
+ "%s %s%chost=%s%c",
+ prog, path, 0,
+ target_host, 0);
+ free(target_host);
if (free_path)
free(path);
- return ret;
+ return 0;
}
if (pipe(pipefd[0]) < 0 || pipe(pipefd[1]) < 0)
die("unable to create pipe pair for communication");
pid = fork();
+ if (pid < 0)
+ die("unable to fork");
if (!pid) {
snprintf(command, sizeof(command), "%s %s", prog,
sq_quote(path));
asciidoc -b xhtml11 -d manpage \
-f ../../Documentation/asciidoc.conf $<
test: git-svn
- cd t && $(SHELL) ./t0000-contrib-git-svn.sh
- cd t && $(SHELL) ./t0001-contrib-git-svn-props.sh
+ cd t && $(SHELL) ./t0000-contrib-git-svn.sh $(TEST_FLAGS)
+ cd t && $(SHELL) ./t0001-contrib-git-svn-props.sh $(TEST_FLAGS)
+
+# we can test NO_OPTIMIZE_COMMITS independently of LC_ALL
+full-test:
+ $(MAKE) test GIT_SVN_NO_LIB=1 GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C
+ $(MAKE) test GIT_SVN_NO_LIB=0 GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C
+ $(MAKE) test GIT_SVN_NO_LIB=1 GIT_SVN_NO_OPTIMIZE_COMMITS=0 \
+ LC_ALL=en_US.UTF-8
+ $(MAKE) test GIT_SVN_NO_LIB=0 GIT_SVN_NO_OPTIMIZE_COMMITS=0 \
+ LC_ALL=en_US.UTF-8
clean:
rm -f git-svn *.xml *.html *.1
use vars qw/ $AUTHOR $VERSION
$SVN_URL $SVN_INFO $SVN_WC $SVN_UUID
$GIT_SVN_INDEX $GIT_SVN
- $GIT_DIR $REV_DIR/;
+ $GIT_DIR $GIT_SVN_DIR $REVDB/;
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
-$VERSION = '1.1.0-pre';
+$VERSION = '1.1.1-broken';
use Cwd qw/abs_path/;
$GIT_DIR = abs_path($ENV{GIT_DIR} || '.git');
$ENV{GIT_DIR} = $GIT_DIR;
+my $LC_ALL = $ENV{LC_ALL};
+my $TZ = $ENV{TZ};
# make sure the svn binary gives consistent output between locales and TZs:
$ENV{TZ} = 'UTC';
$ENV{LC_ALL} = 'C';
use IO::File qw//;
use File::Basename qw/dirname basename/;
use File::Path qw/mkpath/;
-use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
+use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/;
use File::Spec qw//;
use POSIX qw/strftime/;
+use IPC::Open3;
+use Memoize;
+memoize('revisions_eq');
+
+my ($SVN_PATH, $SVN, $SVN_LOG, $_use_lib);
+$_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB};
+libsvn_load();
+my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS};
my $sha1 = qr/[a-f\d]{40}/;
my $sha1_short = qr/[a-f\d]{4,40}/;
my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit,
- $_find_copies_harder, $_l, $_version, $_upgrade, $_authors);
-my (@_branch_from, %tree_map, %users);
-my $_svn_co_url_revs;
+ $_find_copies_harder, $_l, $_cp_similarity, $_cp_remote,
+ $_repack, $_repack_nr, $_repack_flags,
+ $_template, $_shared, $_no_default_regex, $_no_graft_copy,
+ $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit,
+ $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m);
+my (@_branch_from, %tree_map, %users, %rusers, %equiv);
+my ($_svn_co_url_revs, $_svn_pg_peg_revs);
+my @repo_path_split_cache;
my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext,
'branch|b=s' => \@_branch_from,
- 'authors-file|A=s' => \$_authors );
+ 'branch-all-refs|B' => \$_branch_all_refs,
+ 'authors-file|A=s' => \$_authors,
+ 'repack:i' => \$_repack,
+ 'repack-flags|repack-args|repack-opts=s' => \$_repack_flags);
+
+my ($_trunk, $_tags, $_branches);
+my %multi_opts = ( 'trunk|T=s' => \$_trunk,
+ 'tags|t=s' => \$_tags,
+ 'branches|b=s' => \$_branches );
+my %init_opts = ( 'template=s' => \$_template, 'shared' => \$_shared );
# yes, 'native' sets "\n". Patches to fix this for non-*nix systems welcome:
my %EOL = ( CR => "\015", LF => "\012", CRLF => "\015\012", native => "\012" );
fetch => [ \&fetch, "Download new revisions from SVN",
{ 'revision|r=s' => \$_revision, %fc_opts } ],
init => [ \&init, "Initialize a repo for tracking" .
- " (requires URL argument)", { } ],
+ " (requires URL argument)",
+ \%init_opts ],
commit => [ \&commit, "Commit git revisions to SVN",
{ 'stdin|' => \$_stdin,
'edit|e' => \$_edit,
'rmdir' => \$_rmdir,
'find-copies-harder' => \$_find_copies_harder,
'l=i' => \$_l,
+ 'copy-similarity|C=i'=> \$_cp_similarity,
%fc_opts,
} ],
- 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { } ],
+ 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings",
+ { 'revision|r=i' => \$_revision } ],
rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)",
{ 'no-ignore-externals' => \$_no_ignore_ext,
+ 'copy-remote|remote=s' => \$_cp_remote,
'upgrade' => \$_upgrade } ],
+ 'graft-branches' => [ \&graft_branches,
+ 'Detect merges/branches from already imported history',
+ { 'merge-rx|m' => \@_opt_m,
+ 'no-default-regex' => \$_no_default_regex,
+ 'no-graft-copy' => \$_no_graft_copy } ],
+ 'multi-init' => [ \&multi_init,
+ 'Initialize multiple trees (like git-svnimport)',
+ { %multi_opts, %fc_opts } ],
+ 'multi-fetch' => [ \&multi_fetch,
+ 'Fetch multiple trees (like git-svnimport)',
+ \%fc_opts ],
+ 'log' => [ \&show_log, 'Show commit logs',
+ { 'limit=i' => \$_limit,
+ 'revision|r=s' => \$_revision,
+ 'verbose|v' => \$_verbose,
+ 'incremental' => \$_incremental,
+ 'oneline' => \$_oneline,
+ 'show-commit' => \$_show_commit,
+ 'authors-file|A=s' => \$_authors,
+ } ],
);
+
my $cmd;
for (my $i = 0; $i < @ARGV; $i++) {
if (defined $cmd{$ARGV[$i]}) {
my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd);
-# convert GetOpt::Long specs for use by git-repo-config
-foreach my $o (keys %opts) {
- my $v = $opts{$o};
- my ($key) = ($o =~ /^([a-z\-]+)/);
- $key =~ s/-//g;
- my $arg = 'git-repo-config';
- $arg .= ' --int' if ($o =~ /=i$/);
- $arg .= ' --bool' if ($o !~ /=[sfi]$/);
- if (ref $v eq 'ARRAY') {
- chomp(my @tmp = `$arg --get-all svn.$key`);
- @$v = @tmp if @tmp;
- } else {
- chomp(my $tmp = `$arg --get svn.$key`);
- if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
- $$v = $tmp;
- }
- }
-}
-
-GetOptions(%opts, 'help|H|h' => \$_help,
- 'version|V' => \$_version,
- 'id|i=s' => \$GIT_SVN) or exit 1;
-
-$GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
-$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
-$SVN_URL = undef;
-$REV_DIR = "$GIT_DIR/$GIT_SVN/revs";
-$SVN_WC = "$GIT_DIR/$GIT_SVN/tree";
+read_repo_config(\%opts);
+my $rv = GetOptions(%opts, 'help|H|h' => \$_help,
+ 'version|V' => \$_version,
+ 'id|i=s' => \$GIT_SVN);
+exit 1 if (!$rv && $cmd ne 'log');
+set_default_vals();
usage(0) if $_help;
version() if $_version;
usage(1) unless defined $cmd;
+init_vars();
load_authors() if $_authors;
+load_all_refs() if $_branch_all_refs;
svn_compat_check();
+migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init)$/;
$cmd{$cmd}->[0]->(@ARGV);
exit 0;
print $fd ' ',pack('A13',$_),$cmd{$_}->[1],"\n";
foreach (keys %{$cmd{$_}->[2]}) {
# prints out arguments as they should be passed:
- my $x = s#=s$## ? '<arg>' : s#=i$## ? '<num>' : '';
+ my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : '';
print $fd ' ' x 17, join(', ', map { length $_ > 1 ?
"--$_" : "-$_" }
split /\|/,$_)," $x\n";
}
sub rebuild {
+ if (quiet_run(qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0")) {
+ copy_remote_ref();
+ }
$SVN_URL = shift or undef;
my $newest_rev = 0;
if ($_upgrade) {
croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
next if (!@commit); # skip merges
- my $id = $commit[$#commit];
- my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
- \s([a-f\d\-]+)$/x);
- if (!$rev || !$uuid || !$url) {
- # some of the original repositories I made had
- # indentifiers like this:
- ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)
- \@([a-f\d\-]+)/x);
- if (!$rev || !$uuid) {
- croak "Unable to extract revision or UUID from ",
- "$c, $id\n";
- }
+ my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]);
+ if (!$rev || !$uuid) {
+ croak "Unable to extract revision or UUID from ",
+ "$c, $commit[$#commit]\n";
}
# if we merged or otherwise started elsewhere, this is
next if (defined $SVN_UUID && ($uuid ne $SVN_UUID));
next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL));
- print "r$rev = $c\n";
unless (defined $latest) {
if (!$SVN_URL && !$url) {
croak "SVN repository location required: $url\n";
setup_git_svn();
$latest = $rev;
}
- assert_revision_eq_or_unknown($rev, $c);
- sys('git-update-ref',"$GIT_SVN/revs/$rev",$c);
+ revdb_set($REVDB, $rev, $c);
+ print "r$rev = $c\n";
$newest_rev = $rev if ($rev > $newest_rev);
}
close $rev_list or croak $?;
+
+ goto out if $_use_lib;
if (!chdir $SVN_WC) {
svn_cmd_checkout($SVN_URL, $latest, $SVN_WC);
chdir $SVN_WC or croak $!;
sys(@svn_up,"-r$newest_rev");
$ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
index_changes();
- exec('git-write-tree');
+ exec('git-write-tree') or croak $!;
}
waitpid $pid, 0;
-
+ croak $? if $?;
+out:
if ($_upgrade) {
print STDERR <<"";
Keeping deprecated refs/head/$GIT_SVN-HEAD for now. Please remove it
sub init {
$SVN_URL = shift or die "SVN repository location required " .
"as a command-line argument\n";
+ $SVN_URL =~ s!/+$!!; # strip trailing slash
unless (-d $GIT_DIR) {
- sys('git-init-db');
+ my @init_db = ('git-init-db');
+ push @init_db, "--template=$_template" if defined $_template;
+ push @init_db, "--shared" if defined $_shared;
+ sys(@init_db);
}
setup_git_svn();
}
sub fetch {
- my (@parents) = @_;
check_upgrade_needed();
- $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
+ $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
+ my $ret = $_use_lib ? fetch_lib(@_) : fetch_cmd(@_);
+ if ($ret->{commit} && quiet_run(qw(git-rev-parse --verify
+ refs/heads/master^0))) {
+ sys(qw(git-update-ref refs/heads/master),$ret->{commit});
+ }
+ return $ret;
+}
+
+sub fetch_cmd {
+ my (@parents) = @_;
my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL);
unless ($_revision) {
$_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD';
my $svn_log = svn_log_raw(@log_args);
my $base = next_log_entry($svn_log) or croak "No base revision!\n";
- my $last_commit = undef;
+ # don't need last_revision from grab_base_rev() because
+ # user could've specified a different revision to skip (they
+ # didn't want to import certain revisions into git for whatever
+ # reason, so trust $base->{revision} instead.
+ my (undef, $last_commit) = svn_grab_base_rev();
unless (-d $SVN_WC) {
svn_cmd_checkout($SVN_URL,$base->{revision},$SVN_WC);
chdir $SVN_WC or croak $!;
} else {
chdir $SVN_WC or croak $!;
read_uuid();
- $last_commit = file_to_s("$REV_DIR/$base->{revision}");
+ # looks like a user manually cp'd and svn switch'ed
+ unless ($last_commit) {
+ sys(qw/svn revert -R ./);
+ assert_svn_wc_clean($base->{revision});
+ $last_commit = git_commit($base, @parents);
+ assert_tree($last_commit);
+ }
}
my @svn_up = qw(svn up);
push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
my $last = $base;
while (my $log_msg = next_log_entry($svn_log)) {
- assert_tree($last_commit);
if ($last->{revision} >= $log_msg->{revision}) {
croak "Out of order: last >= current: ",
"$last->{revision} >= $log_msg->{revision}\n";
$last_commit = git_commit($log_msg, $last_commit, @parents);
$last = $log_msg;
}
- unless (-e "$GIT_DIR/refs/heads/master") {
- sys(qw(git-update-ref refs/heads/master),$last_commit);
- }
+ close $svn_log->{fh};
+ $last->{commit} = $last_commit;
return $last;
}
+sub fetch_lib {
+ my (@parents) = @_;
+ $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url");
+ my $repo;
+ ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
+ $SVN_LOG ||= libsvn_connect($repo);
+ $SVN ||= libsvn_connect($repo);
+ my ($last_rev, $last_commit) = svn_grab_base_rev();
+ my ($base, $head) = libsvn_parse_revision($last_rev);
+ if ($base > $head) {
+ return { revision => $last_rev, commit => $last_commit }
+ }
+ my $index = set_index($GIT_SVN_INDEX);
+
+ # limit ourselves and also fork() since get_log won't release memory
+ # after processing a revision and SVN stuff seems to leak
+ my $inc = 1000;
+ my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
+ read_uuid();
+ if (defined $last_commit) {
+ unless (-e $GIT_SVN_INDEX) {
+ sys(qw/git-read-tree/, $last_commit);
+ }
+ chomp (my $x = `git-write-tree`);
+ my ($y) = (`git-cat-file commit $last_commit`
+ =~ /^tree ($sha1)/m);
+ if ($y ne $x) {
+ unlink $GIT_SVN_INDEX or croak $!;
+ sys(qw/git-read-tree/, $last_commit);
+ }
+ chomp ($x = `git-write-tree`);
+ if ($y ne $x) {
+ print STDERR "trees ($last_commit) $y != $x\n",
+ "Something is seriously wrong...\n";
+ }
+ }
+ while (1) {
+ # fork, because using SVN::Pool with get_log() still doesn't
+ # seem to help enough to keep memory usage down.
+ defined(my $pid = fork) or croak $!;
+ if (!$pid) {
+ $SVN::Error::handler = \&libsvn_skip_unknown_revs;
+
+ # Yes I'm perfectly aware that the fourth argument
+ # below is the limit revisions number. Unfortunately
+ # performance sucks with it enabled, so it's much
+ # faster to fetch revision ranges instead of relying
+ # on the limiter.
+ $SVN_LOG->get_log( '/'.$SVN_PATH, $min, $max, 0, 1, 1,
+ sub {
+ my $log_msg;
+ if ($last_commit) {
+ $log_msg = libsvn_fetch(
+ $last_commit, @_);
+ $last_commit = git_commit(
+ $log_msg,
+ $last_commit,
+ @parents);
+ } else {
+ $log_msg = libsvn_new_tree(@_);
+ $last_commit = git_commit(
+ $log_msg, @parents);
+ }
+ });
+ exit 0;
+ }
+ waitpid $pid, 0;
+ croak $? if $?;
+ ($last_rev, $last_commit) = svn_grab_base_rev();
+ last if ($max >= $head);
+ $min = $max + 1;
+ $max += $inc;
+ $max = $head if ($max > $head);
+ }
+ restore_index($index);
+ return { revision => $last_rev, commit => $last_commit };
+}
+
sub commit {
my (@commits) = @_;
check_upgrade_needed();
}
}
chomp @revs;
+ $_use_lib ? commit_lib(@revs) : commit_cmd(@revs);
+ print "Done committing ",scalar @revs," revisions to SVN\n";
+}
- fetch();
- chdir $SVN_WC or croak $!;
+sub commit_cmd {
+ my (@revs) = @_;
+
+ chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n";
my $info = svn_info('.');
+ my $fetched = fetch();
+ if ($info->{Revision} != $fetched->{revision}) {
+ print STDERR "There are new revisions that were fetched ",
+ "and need to be merged (or acknowledged) ",
+ "before committing.\n";
+ exit 1;
+ }
+ $info = svn_info('.');
read_uuid($info);
- my $svn_current_rev = $info->{'Last Changed Rev'};
+ my $last = $fetched;
foreach my $c (@revs) {
- my $mods = svn_checkout_tree($svn_current_rev, $c);
+ my $mods = svn_checkout_tree($last, $c);
if (scalar @$mods == 0) {
print "Skipping, no changes detected\n";
next;
}
- $svn_current_rev = svn_commit_tree($svn_current_rev, $c);
+ $last = svn_commit_tree($last, $c);
}
- print "Done committing ",scalar @revs," revisions to SVN\n";
+}
+
+sub commit_lib {
+ my (@revs) = @_;
+ my ($r_last, $cmt_last) = svn_grab_base_rev();
+ defined $r_last or die "Must have an existing revision to commit\n";
+ my $fetched = fetch();
+ if ($r_last != $fetched->{revision}) {
+ print STDERR "There are new revisions that were fetched ",
+ "and need to be merged (or acknowledged) ",
+ "before committing.\n",
+ "last rev: $r_last\n",
+ " current: $fetched->{revision}\n";
+ exit 1;
+ }
+ read_uuid();
+ my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : ();
+ my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
+
+ foreach my $c (@revs) {
+ # fork for each commit because there's a memory leak I
+ # can't track down... (it's probably in the SVN code)
+ defined(my $pid = open my $fh, '-|') or croak $!;
+ if (!$pid) {
+ if (defined $LC_ALL) {
+ $ENV{LC_ALL} = $LC_ALL;
+ } else {
+ delete $ENV{LC_ALL};
+ }
+ my $log_msg = get_commit_message($c, $commit_msg);
+ my $ed = SVN::Git::Editor->new(
+ { r => $r_last,
+ ra => $SVN,
+ c => $c,
+ svn_path => $SVN_PATH
+ },
+ $SVN->get_commit_editor(
+ $log_msg->{msg},
+ sub {
+ libsvn_commit_cb(
+ @_, $c,
+ $log_msg->{msg},
+ $r_last,
+ $cmt_last)
+ },
+ @lock)
+ );
+ my $mods = libsvn_checkout_tree($cmt_last, $c, $ed);
+ if (@$mods == 0) {
+ print "No changes\nr$r_last = $cmt_last\n";
+ $ed->abort_edit;
+ } else {
+ $ed->close_edit;
+ }
+ exit 0;
+ }
+ my ($r_new, $cmt_new, $no);
+ while (<$fh>) {
+ print $_;
+ chomp;
+ if (/^r(\d+) = ($sha1)$/o) {
+ ($r_new, $cmt_new) = ($1, $2);
+ } elsif ($_ eq 'No changes') {
+ $no = 1;
+ }
+ }
+ close $fh or croak $?;
+ if (! defined $r_new && ! defined $cmt_new) {
+ unless ($no) {
+ die "Failed to parse revision information\n";
+ }
+ } else {
+ ($r_last, $cmt_last) = ($r_new, $cmt_new);
+ }
+ }
+ unlink $commit_msg;
}
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_SVN_DIR/info/url");
+ $_use_lib ? show_ignore_lib() : show_ignore_cmd();
+}
- $SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
+sub show_ignore_cmd {
+ require File::Find or die $!;
+ if (defined $_revision) {
+ die "-r/--revision option doesn't work unless the Perl SVN ",
+ "libraries are used\n";
+ }
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),$_);
+ @{$ign{$_}} = svn_propget_base('svn:ignore', $_);
}}, no_chdir=>1},'.');
print "\n# /\n";
}
}
+sub show_ignore_lib {
+ my $repo;
+ ($repo, $SVN_PATH) = repo_path_split($SVN_URL);
+ $SVN ||= libsvn_connect($repo);
+ my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum;
+ libsvn_traverse_ignore(\*STDOUT, $SVN_PATH, $r);
+}
+
+sub graft_branches {
+ my $gr_file = "$GIT_DIR/info/grafts";
+ my ($grafts, $comments) = read_grafts($gr_file);
+ my $gr_sha1;
+
+ if (%$grafts) {
+ # temporarily disable our grafts file to make this idempotent
+ chomp($gr_sha1 = safe_qx(qw/git-hash-object -w/,$gr_file));
+ rename $gr_file, "$gr_file~$gr_sha1" or croak $!;
+ }
+
+ my $l_map = read_url_paths();
+ my @re = map { qr/$_/is } @_opt_m if @_opt_m;
+ unless ($_no_default_regex) {
+ push @re, ( qr/\b(?:merge|merging|merged)\s+(\S.+)/is,
+ qr/\b(?:from|of)\s+(\S.+)/is );
+ }
+ foreach my $u (keys %$l_map) {
+ if (@re) {
+ foreach my $p (keys %{$l_map->{$u}}) {
+ graft_merge_msg($grafts,$l_map,$u,$p);
+ }
+ }
+ unless ($_no_graft_copy) {
+ if ($_use_lib) {
+ graft_file_copy_lib($grafts,$l_map,$u);
+ } else {
+ graft_file_copy_cmd($grafts,$l_map,$u);
+ }
+ }
+ }
+
+ write_grafts($grafts, $comments, $gr_file);
+ unlink "$gr_file~$gr_sha1" if $gr_sha1;
+}
+
+sub multi_init {
+ my $url = shift;
+ $_trunk ||= 'trunk';
+ $_trunk =~ s#/+$##;
+ $url =~ s#/+$## if $url;
+ if ($_trunk !~ m#^[a-z\+]+://#) {
+ $_trunk = '/' . $_trunk if ($_trunk !~ m#^/#);
+ unless ($url) {
+ print STDERR "E: '$_trunk' is not a complete URL ",
+ "and a separate URL is not specified\n";
+ exit 1;
+ }
+ $_trunk = $url . $_trunk;
+ }
+ if ($GIT_SVN eq 'git-svn') {
+ print "GIT_SVN_ID set to 'trunk' for $_trunk\n";
+ $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk';
+ }
+ init_vars();
+ init($_trunk);
+ complete_url_ls_init($url, $_branches, '--branches/-b', '');
+ complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/');
+}
+
+sub multi_fetch {
+ # try to do trunk first, since branches/tags
+ # may be descended from it.
+ if (-e "$GIT_DIR/svn/trunk/info/url") {
+ fetch_child_id('trunk', @_);
+ }
+ rec_fetch('', "$GIT_DIR/svn", @_);
+}
+
+sub show_log {
+ my (@args) = @_;
+ my ($r_min, $r_max);
+ my $r_last = -1; # prevent dupes
+ rload_authors() if $_authors;
+ if (defined $TZ) {
+ $ENV{TZ} = $TZ;
+ } else {
+ delete $ENV{TZ};
+ }
+ if (defined $_revision) {
+ if ($_revision =~ /^(\d+):(\d+)$/) {
+ ($r_min, $r_max) = ($1, $2);
+ } elsif ($_revision =~ /^\d+$/) {
+ $r_min = $r_max = $_revision;
+ } else {
+ print STDERR "-r$_revision is not supported, use ",
+ "standard \'git log\' arguments instead\n";
+ exit 1;
+ }
+ }
+
+ my $pid = open(my $log,'-|');
+ defined $pid or croak $!;
+ if (!$pid) {
+ exec(git_svn_log_cmd($r_min,$r_max), @args) or croak $!;
+ }
+ setup_pager();
+ my (@k, $c, $d);
+
+ while (<$log>) {
+ if (/^commit ($sha1_short)/o) {
+ my $cmt = $1;
+ if ($c && cmt_showable($c) && $c->{r} != $r_last) {
+ $r_last = $c->{r};
+ process_commit($c, $r_min, $r_max, \@k) or
+ goto out;
+ }
+ $d = undef;
+ $c = { c => $cmt };
+ } elsif (/^author (.+) (\d+) ([\-\+]?\d+)$/) {
+ get_author_info($c, $1, $2, $3);
+ } elsif (/^(?:tree|parent|committer) /) {
+ # ignore
+ } elsif (/^:\d{6} \d{6} $sha1_short/o) {
+ push @{$c->{raw}}, $_;
+ } elsif (/^diff /) {
+ $d = 1;
+ push @{$c->{diff}}, $_;
+ } elsif ($d) {
+ push @{$c->{diff}}, $_;
+ } elsif (/^ (git-svn-id:.+)$/) {
+ (undef, $c->{r}, undef) = extract_metadata($1);
+ } elsif (s/^ //) {
+ push @{$c->{l}}, $_;
+ }
+ }
+ if ($c && defined $c->{r} && $c->{r} != $r_last) {
+ $r_last = $c->{r};
+ process_commit($c, $r_min, $r_max, \@k);
+ }
+ if (@k) {
+ my $swap = $r_max;
+ $r_max = $r_min;
+ $r_min = $swap;
+ process_commit($_, $r_min, $r_max) foreach reverse @k;
+ }
+out:
+ close $log;
+ print '-' x72,"\n" unless $_incremental || $_oneline;
+}
+
########################### utility functions #########################
+sub cmt_showable {
+ my ($c) = @_;
+ return 1 if defined $c->{r};
+ if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
+ $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
+ my @msg = safe_qx(qw/git-cat-file commit/, $c->{c});
+ shift @msg while ($msg[0] ne "\n");
+ shift @msg;
+ @{$c->{l}} = grep !/^git-svn-id: /, @msg;
+
+ (undef, $c->{r}, undef) = extract_metadata(
+ (grep(/^git-svn-id: /, @msg))[-1]);
+ }
+ return defined $c->{r};
+}
+
+sub git_svn_log_cmd {
+ my ($r_min, $r_max) = @_;
+ my @cmd = (qw/git-log --abbrev-commit --pretty=raw
+ --default/, "refs/remotes/$GIT_SVN");
+ push @cmd, '--summary' if $_verbose;
+ return @cmd unless defined $r_max;
+ if ($r_max == $r_min) {
+ push @cmd, '--max-count=1';
+ if (my $c = revdb_get($REVDB, $r_max)) {
+ push @cmd, $c;
+ }
+ } else {
+ my ($c_min, $c_max);
+ $c_max = revdb_get($REVDB, $r_max);
+ $c_min = revdb_get($REVDB, $r_min);
+ if ($c_min && $c_max) {
+ if ($r_max > $r_max) {
+ push @cmd, "$c_min..$c_max";
+ } else {
+ push @cmd, "$c_max..$c_min";
+ }
+ } elsif ($r_max > $r_min) {
+ push @cmd, $c_max;
+ } else {
+ push @cmd, $c_min;
+ }
+ }
+ return @cmd;
+}
+
+sub fetch_child_id {
+ my $id = shift;
+ print "Fetching $id\n";
+ my $ref = "$GIT_DIR/refs/remotes/$id";
+ my $ca = file_to_s($ref) if (-r $ref);
+ defined(my $pid = fork) or croak $!;
+ if (!$pid) {
+ $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
+ init_vars();
+ fetch(@_);
+ exit 0;
+ }
+ waitpid $pid, 0;
+ croak $? if $?;
+ return unless $_repack || -r $ref;
+
+ my $cb = file_to_s($ref);
+
+ defined($pid = open my $fh, '-|') or croak $!;
+ my $url = file_to_s("$GIT_DIR/svn/$id/info/url");
+ $url = qr/\Q$url\E/;
+ if (!$pid) {
+ exec qw/git-rev-list --pretty=raw/,
+ $ca ? "$ca..$cb" : $cb or croak $!;
+ }
+ while (<$fh>) {
+ if (/^ git-svn-id: $url\@\d+ [a-f0-9\-]+$/) {
+ check_repack();
+ } elsif (/^ git-svn-id: \S+\@\d+ [a-f0-9\-]+$/) {
+ last;
+ }
+ }
+ close $fh;
+}
+
+sub rec_fetch {
+ my ($pfx, $p, @args) = @_;
+ my @dir;
+ foreach (sort <$p/*>) {
+ if (-r "$_/info/url") {
+ $pfx .= '/' if $pfx && $pfx !~ m!/$!;
+ my $id = $pfx . basename $_;
+ next if $id eq 'trunk';
+ fetch_child_id($id, @args);
+ } elsif (-d $_) {
+ push @dir, $_;
+ }
+ }
+ foreach (@dir) {
+ my $x = $_;
+ $x =~ s!^\Q$GIT_DIR\E/svn/!!;
+ rec_fetch($x, $_);
+ }
+}
+
+sub complete_url_ls_init {
+ my ($url, $var, $switch, $pfx) = @_;
+ unless ($var) {
+ print STDERR "W: $switch not specified\n";
+ return;
+ }
+ $var =~ s#/+$##;
+ if ($var !~ m#^[a-z\+]+://#) {
+ $var = '/' . $var if ($var !~ m#^/#);
+ unless ($url) {
+ print STDERR "E: '$var' is not a complete URL ",
+ "and a separate URL is not specified\n";
+ exit 1;
+ }
+ $var = $url . $var;
+ }
+ chomp(my @ls = $_use_lib ? libsvn_ls_fullurl($var)
+ : safe_qx(qw/svn ls --non-interactive/, $var));
+ my $old = $GIT_SVN;
+ defined(my $pid = fork) or croak $!;
+ if (!$pid) {
+ foreach my $u (map { "$var/$_" } (grep m!/$!, @ls)) {
+ $u =~ s#/+$##;
+ if ($u !~ m!\Q$var\E/(.+)$!) {
+ print STDERR "W: Unrecognized URL: $u\n";
+ die "This should never happen\n";
+ }
+ my $id = $pfx.$1;
+ print "init $u => $id\n";
+ $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
+ init_vars();
+ init($u);
+ }
+ exit 0;
+ }
+ waitpid $pid, 0;
+ croak $? if $?;
+}
+
+sub common_prefix {
+ my $paths = shift;
+ my %common;
+ foreach (@$paths) {
+ my @tmp = split m#/#, $_;
+ my $p = '';
+ while (my $x = shift @tmp) {
+ $p .= "/$x";
+ $common{$p} ||= 0;
+ $common{$p}++;
+ }
+ }
+ foreach (sort {length $b <=> length $a} keys %common) {
+ if ($common{$_} == @$paths) {
+ return $_;
+ }
+ }
+ return '';
+}
+
+# this isn't funky-filename safe, but good enough for now...
+sub graft_file_copy_cmd {
+ my ($grafts, $l_map, $u) = @_;
+ my $paths = $l_map->{$u};
+ my $pfx = common_prefix([keys %$paths]);
+ $SVN_URL ||= $u.$pfx;
+ my $pid = open my $fh, '-|';
+ defined $pid or croak $!;
+ unless ($pid) {
+ my @exec = qw/svn log -v/;
+ push @exec, "-r$_revision" if defined $_revision;
+ exec @exec, $u.$pfx or croak $!;
+ }
+ my ($r, $mp) = (undef, undef);
+ while (<$fh>) {
+ chomp;
+ if (/^\-{72}$/) {
+ $mp = $r = undef;
+ } elsif (/^r(\d+) \| /) {
+ $r = $1 unless defined $r;
+ } elsif (/^Changed paths:/) {
+ $mp = 1;
+ } elsif ($mp && m#^ [AR] /(\S.*?) \(from /(\S+?):(\d+)\)$#) {
+ my ($p1, $p0, $r0) = ($1, $2, $3);
+ my $c = find_graft_path_commit($paths, $p1, $r);
+ next unless $c;
+ find_graft_path_parents($grafts, $paths, $c, $p0, $r0);
+ }
+ }
+}
+
+sub graft_file_copy_lib {
+ my ($grafts, $l_map, $u) = @_;
+ my $tree_paths = $l_map->{$u};
+ my $pfx = common_prefix([keys %$tree_paths]);
+ my ($repo, $path) = repo_path_split($u.$pfx);
+ $SVN_LOG ||= libsvn_connect($repo);
+ $SVN ||= libsvn_connect($repo);
+
+ my ($base, $head) = libsvn_parse_revision();
+ my $inc = 1000;
+ my ($min, $max) = ($base, $head < $base+$inc ? $head : $base+$inc);
+ my $eh = $SVN::Error::handler;
+ $SVN::Error::handler = \&libsvn_skip_unknown_revs;
+ while (1) {
+ my $pool = SVN::Pool->new;
+ $SVN_LOG->get_log( "/$path", $min, $max, 0, 1, 1,
+ sub {
+ libsvn_graft_file_copies($grafts, $tree_paths,
+ $path, @_);
+ }, $pool);
+ $pool->clear;
+ last if ($max >= $head);
+ $min = $max + 1;
+ $max += $inc;
+ $max = $head if ($max > $head);
+ }
+ $SVN::Error::handler = $eh;
+}
+
+sub process_merge_msg_matches {
+ my ($grafts, $l_map, $u, $p, $c, @matches) = @_;
+ my (@strong, @weak);
+ foreach (@matches) {
+ # merging with ourselves is not interesting
+ next if $_ eq $p;
+ if ($l_map->{$u}->{$_}) {
+ push @strong, $_;
+ } else {
+ push @weak, $_;
+ }
+ }
+ foreach my $w (@weak) {
+ last if @strong;
+ # no exact match, use branch name as regexp.
+ my $re = qr/\Q$w\E/i;
+ foreach (keys %{$l_map->{$u}}) {
+ if (/$re/) {
+ push @strong, $_;
+ last;
+ }
+ }
+ last if @strong;
+ $w = basename($w);
+ $re = qr/\Q$w\E/i;
+ foreach (keys %{$l_map->{$u}}) {
+ if (/$re/) {
+ push @strong, $_;
+ last;
+ }
+ }
+ }
+ my ($rev) = ($c->{m} =~ /^git-svn-id:\s(?:\S+?)\@(\d+)
+ \s(?:[a-f\d\-]+)$/xsm);
+ unless (defined $rev) {
+ ($rev) = ($c->{m} =~/^git-svn-id:\s(\d+)
+ \@(?:[a-f\d\-]+)/xsm);
+ return unless defined $rev;
+ }
+ foreach my $m (@strong) {
+ my ($r0, $s0) = find_rev_before($rev, $m);
+ $grafts->{$c->{c}}->{$s0} = 1 if defined $s0;
+ }
+}
+
+sub graft_merge_msg {
+ my ($grafts, $l_map, $u, $p, @re) = @_;
+
+ my $x = $l_map->{$u}->{$p};
+ my $rl = rev_list_raw($x);
+ while (my $c = next_rev_list_entry($rl)) {
+ foreach my $re (@re) {
+ my (@br) = ($c->{m} =~ /$re/g);
+ next unless @br;
+ process_merge_msg_matches($grafts,$l_map,$u,$p,$c,@br);
+ }
+ }
+}
+
sub read_uuid {
return if $SVN_UUID;
- my $info = shift || svn_info('.');
- $SVN_UUID = $info->{'Repository UUID'} or
+ if ($_use_lib) {
+ my $pool = SVN::Pool->new;
+ $SVN_UUID = $SVN->get_uuid($pool);
+ $pool->clear;
+ } else {
+ my $info = shift || svn_info('.');
+ $SVN_UUID = $info->{'Repository UUID'} or
croak "Repository UUID unreadable\n";
- s_to_file($SVN_UUID,"$GIT_DIR/$GIT_SVN/info/uuid");
+ }
+}
+
+sub quiet_run {
+ my $pid = fork;
+ defined $pid or croak $!;
+ if (!$pid) {
+ open my $null, '>', '/dev/null' or croak $!;
+ open STDERR, '>&', $null or croak $!;
+ open STDOUT, '>&', $null or croak $!;
+ exec @_ or croak $!;
+ }
+ waitpid $pid, 0;
+ return $?;
+}
+
+sub repo_path_split {
+ my $full_url = shift;
+ $full_url =~ s#/+$##;
+
+ foreach (@repo_path_split_cache) {
+ if ($full_url =~ s#$_##) {
+ my $u = $1;
+ $full_url =~ s#^/+##;
+ return ($u, $full_url);
+ }
+ }
+
+ my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i);
+ $path =~ s#^/+##;
+ my @paths = split(m#/+#, $path);
+
+ if ($_use_lib) {
+ while (1) {
+ $SVN = libsvn_connect($url);
+ last if (defined $SVN &&
+ defined eval { $SVN->get_latest_revnum });
+ my $n = shift @paths || last;
+ $url .= "/$n";
+ }
+ } else {
+ while (quiet_run(qw/svn ls --non-interactive/, $url)) {
+ my $n = shift @paths || last;
+ $url .= "/$n";
+ }
+ }
+ push @repo_path_split_cache, qr/^(\Q$url\E)/;
+ $path = join('/',@paths);
+ return ($url, $path);
}
sub setup_git_svn {
unless (-d $GIT_DIR) {
croak "GIT_DIR=$GIT_DIR does not exist!\n";
}
- mkpath(["$GIT_DIR/$GIT_SVN"]);
- mkpath(["$GIT_DIR/$GIT_SVN/info"]);
- mkpath([$REV_DIR]);
- s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
+ mkpath([$GIT_SVN_DIR]);
+ mkpath(["$GIT_SVN_DIR/info"]);
+ open my $fh, '>>',$REVDB or croak $!;
+ close $fh;
+ s_to_file($SVN_URL,"$GIT_SVN_DIR/info/url");
- open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!;
- print $fd '.svn',"\n";
- close $fd or croak $!;
}
sub assert_svn_wc_clean {
+ return if $_use_lib;
my ($svn_rev) = @_;
croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
my $lcr = svn_info('.')->{'Last Changed Rev'};
}
}
-sub assert_tree {
+sub get_tree_from_treeish {
my ($treeish) = @_;
croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o;
chomp(my $type = `git-cat-file -t $treeish`);
} else {
die "$treeish is a $type, expected tree, tag or commit\n";
}
+ return $expected;
+}
+
+sub assert_tree {
+ return if $_use_lib;
+ my ($treeish) = @_;
+ my $expected = get_tree_from_treeish($treeish);
- my $old_index = $ENV{GIT_INDEX_FILE};
my $tmpindex = $GIT_SVN_INDEX.'.assert-tmp';
if (-e $tmpindex) {
unlink $tmpindex or croak $!;
}
- $ENV{GIT_INDEX_FILE} = $tmpindex;
+ my $old_index = set_index($tmpindex);
index_changes(1);
chomp(my $tree = `git-write-tree`);
- if ($old_index) {
- $ENV{GIT_INDEX_FILE} = $old_index;
- } else {
- delete $ENV{GIT_INDEX_FILE};
- }
+ restore_index($old_index);
if ($tree ne $expected) {
croak "Tree mismatch, Got: $tree, Expected: $expected\n";
}
croak "Error parsing $_\n";
}
}
- close $diff_fh or croak $!;
+ close $diff_fh or croak $?;
return \@mods;
}
}
}
-sub svn_checkout_tree {
- my ($svn_rev, $treeish) = @_;
- my $from = file_to_s("$REV_DIR/$svn_rev");
- assert_svn_wc_clean($svn_rev);
+
+sub get_diff {
+ my ($from, $treeish) = @_;
assert_tree($from);
print "diff-tree $from $treeish\n";
my $pid = open my $diff_fh, '-|';
defined $pid or croak $!;
if ($pid == 0) {
- my @diff_tree = qw(git-diff-tree -z -r -C);
+ my @diff_tree = qw(git-diff-tree -z -r);
+ if ($_cp_similarity) {
+ push @diff_tree, "-C$_cp_similarity";
+ } else {
+ push @diff_tree, '-C';
+ }
push @diff_tree, '--find-copies-harder' if $_find_copies_harder;
push @diff_tree, "-l$_l" if defined $_l;
exec(@diff_tree, $from, $treeish) or croak $!;
}
- my $mods = parse_diff_tree($diff_fh);
- unless (@$mods) {
- # git can do empty commits, but SVN doesn't allow it...
- return $mods;
- }
+ return parse_diff_tree($diff_fh);
+}
+
+sub svn_checkout_tree {
+ my ($from, $treeish) = @_;
+ my $mods = get_diff($from->{commit}, $treeish);
+ return $mods unless (scalar @$mods);
my ($rm, $add) = precommit_check($mods);
my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
} elsif ($m->{chg} eq 'T') {
sys(qw(svn rm --force),$m->{file_b});
apply_mod_line_blob($m);
- sys(qw(svn add --force), $m->{file_b});
+ sys(qw(svn add), $m->{file_b});
svn_check_prop_executable($m);
} elsif ($m->{chg} eq 'A') {
svn_ensure_parent_path( $m->{file_b} );
apply_mod_line_blob($m);
- sys(qw(svn add --force), $m->{file_b});
+ sys(qw(svn add), $m->{file_b});
svn_check_prop_executable($m);
} else {
croak "Invalid chg: $m->{chg}\n";
return $mods;
}
+sub libsvn_checkout_tree {
+ my ($from, $treeish, $ed) = @_;
+ my $mods = get_diff($from, $treeish);
+ return $mods unless (scalar @$mods);
+ my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 );
+ foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) {
+ my $f = $m->{chg};
+ if (defined $o{$f}) {
+ $ed->$f($m);
+ } else {
+ croak "Invalid change type: $f\n";
+ }
+ }
+ $ed->rmdirs if $_rmdir;
+ return $mods;
+}
+
# svn ls doesn't work with respect to the current working tree, but what's
# in the repository. There's not even an option for it... *sigh*
# (added files don't show up and removed files remain in the ls listing)
}
}
-sub svn_commit_tree {
- my ($svn_rev, $commit) = @_;
- my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$";
+sub get_commit_message {
+ my ($commit, $commit_msg) = (@_);
my %log_msg = ( msg => '' );
open my $msg, '>', $commit_msg or croak $!;
+ print "commit: $commit\n";
chomp(my $type = `git-cat-file -t $commit`);
if ($type eq 'commit') {
my $pid = open my $msg_fh, '-|';
print $msg $_ or croak $!;
}
}
- close $msg_fh or croak $!;
+ close $msg_fh or croak $?;
}
close $msg or croak $!;
{ local $/; chomp($log_msg{msg} = <$msg>); }
close $msg or croak $!;
- my ($oneline) = ($log_msg{msg} =~ /([^\n\r]+)/);
+ return \%log_msg;
+}
+
+sub svn_commit_tree {
+ my ($last, $commit) = @_;
+ my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$";
+ my $log_msg = get_commit_message($commit, $commit_msg);
+ my ($oneline) = ($log_msg->{msg} =~ /([^\n\r]+)/);
print "Committing $commit: $oneline\n";
+ if (defined $LC_ALL) {
+ $ENV{LC_ALL} = $LC_ALL;
+ } else {
+ delete $ENV{LC_ALL};
+ }
my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
- my ($committed) = grep(/^Committed revision \d+\./,@ci_output);
+ $ENV{LC_ALL} = 'C';
unlink $commit_msg;
- defined $committed or croak
+ my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/);
+ if (!defined $committed) {
+ my $out = join("\n",@ci_output);
+ print STDERR "W: Trouble parsing \`svn commit' output:\n\n",
+ $out, "\n\nAssuming English locale...";
+ ($committed) = ($out =~ /^Committed revision \d+\./sm);
+ defined $committed or die " FAILED!\n",
"Commit output failed to parse committed revision!\n",
- join("\n",@ci_output),"\n";
- my ($rev_committed) = ($committed =~ /^Committed revision (\d+)\./);
+ print STDERR " OK\n";
+ }
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";
+ if ($_optimize_commits && ($committed == ($last->{revision} + 1))) {
+ push @svn_up, "-r$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"
+ if ($info->{'Last Changed Rev'} != $committed) {
+ croak "$info->{'Last Changed 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;
+ $log_msg->{date} = "$tz $Y-$m-$d $H:$M:$S";
+ $log_msg->{author} = $info->{'Last Changed Author'};
+ $log_msg->{revision} = $committed;
+ $log_msg->{msg} .= "\n";
+ $log_msg->{parents} = [ $last->{commit} ];
+ $log_msg->{commit} = git_commit($log_msg, $commit);
+ return $log_msg;
}
# resync immediately
- push @svn_up, "-r$svn_rev";
+ push @svn_up, "-r$last->{revision}";
sys(@svn_up);
- return fetch("$rev_committed=$commit")->{revision};
+ return fetch("$committed=$commit");
+}
+
+sub rev_list_raw {
+ my (@args) = @_;
+ my $pid = open my $fh, '-|';
+ defined $pid or croak $!;
+ if (!$pid) {
+ exec(qw/git-rev-list --pretty=raw/, @args) or croak $!;
+ }
+ return { fh => $fh, t => { } };
+}
+
+sub next_rev_list_entry {
+ my $rl = shift;
+ my $fh = $rl->{fh};
+ my $x = $rl->{t};
+ while (<$fh>) {
+ if (/^commit ($sha1)$/o) {
+ if ($x->{c}) {
+ $rl->{t} = { c => $1 };
+ return $x;
+ } else {
+ $x->{c} = $1;
+ }
+ } elsif (/^parent ($sha1)$/o) {
+ $x->{p}->{$1} = 1;
+ } elsif (s/^ //) {
+ $x->{m} ||= '';
+ $x->{m} .= $_;
+ }
+ }
+ return ($x != $rl->{t}) ? $x : undef;
}
# read the entire log into a temporary file (which is removed ASAP)
exec (qw(svn log), @log_args) or croak $!
}
waitpid $pid, 0;
- croak if $?;
+ croak $? if $?;
seek $log_fh, 0, 0 or croak $!;
return { state => 'sep', fh => $log_fh };
}
push @{$ret->{-order}}, $1;
}
}
- close $info_fh or croak $!;
+ close $info_fh or croak $?;
return $ret;
}
sub eol_cp {
my ($from, $to) = @_;
- my $es = safe_qx(qw/svn propget svn:eol-style/, $to);
+ my $es = svn_propget_base('svn:eol-style', $to);
open my $rfd, '<', $from or croak $!;
binmode $rfd or croak $!;
open my $wfd, '>', $to or croak $!;
binmode $wfd or croak $!;
+ eol_cp_fd($rfd, $wfd, $es);
+ close $rfd or croak $!;
+ close $wfd or croak $!;
+}
- my $eol = $EOL{$es} or undef;
- if ($eol) {
- print "$eol: $from => $to\n";
- }
+sub eol_cp_fd {
+ my ($rfd, $wfd, $es) = @_;
+ my $eol = defined $es ? $EOL{$es} : undef;
my $buf;
+ use bytes;
while (1) {
my ($r, $w, $t);
defined($r = sysread($rfd, $buf, 4096)) or croak $!;
return unless $r;
- $buf =~ s/(?:\015|\012|\015\012)/$eol/gs if $eol;
+ if ($eol) {
+ if ($buf =~ /\015$/) {
+ my $c;
+ defined($r = sysread($rfd,$c,1)) or croak $!;
+ $buf .= $c if $r > 0;
+ }
+ $buf =~ s/(?:\015\012|\015|\012)/$eol/gs;
+ $r = length($buf);
+ }
for ($w = 0; $w < $r; $w += $t) {
$t = syswrite($wfd, $buf, $r - $w, $w) or croak $!;
}
}
+ no bytes;
}
sub do_update_index {
while (my $x = <$p>) {
chomp $x;
if (!$no_text_base && lstat $x && ! -l _ &&
- safe_qx(qw/svn propget svn:keywords/,$x)) {
+ svn_propget_base('svn:keywords', $x)) {
my $mode = -x _ ? 0755 : 0644;
my ($v,$d,$f) = File::Spec->splitpath($x);
my $tb = File::Spec->catfile($d, '.svn', 'tmp',
}
print $ui $x,"\0";
}
- close $ui or croak $!;
+ close $ui or croak $?;
}
sub index_changes {
- my $no_text_base = shift;
+ return if $_use_lib;
+
+ if (!-f "$GIT_SVN_DIR/info/exclude") {
+ open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!;
+ print $fd '.svn',"\n";
+ close $fd or croak $!;
+ }
+ my $no_text_base = shift;
do_update_index([qw/git-diff-files --name-only -z/],
'remove',
$no_text_base);
do_update_index([qw/git-ls-files -z --others/,
- "--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude"],
+ "--exclude-from=$GIT_SVN_DIR/info/exclude"],
'add',
$no_text_base);
}
}
sub assert_revision_unknown {
- my $revno = shift;
- if (-f "$REV_DIR/$revno") {
- croak "$REV_DIR/$revno already exists! ",
- "Why are we refetching it?";
+ my $r = shift;
+ if (my $c = revdb_get($REVDB, $r)) {
+ croak "$r = $c already exists! Why are we refetching it?";
}
}
return 1;
}
-sub assert_revision_eq_or_unknown {
- my ($revno, $commit) = @_;
- if (-f "$REV_DIR/$revno") {
- my $current = file_to_s("$REV_DIR/$revno");
- if (($commit ne $current) && !trees_eq($commit, $current)) {
- croak "$REV_DIR/$revno already exists!\n",
- "current: $current\nexpected: $commit\n";
- }
- return;
- }
-}
-
sub git_commit {
my ($log_msg, @parents) = @_;
assert_revision_unknown($log_msg->{revision});
- my $out_fh = IO::File->new_tmpfile or croak $!;
-
map_tree_joins() if (@_branch_from && !%tree_map);
+ my (@tmp_parents, @exec_parents, %seen_parent);
+ if (my $lparents = $log_msg->{parents}) {
+ @tmp_parents = @$lparents
+ }
# commit parents can be conditionally bound to a particular
# svn revision via: "svn_revno=commit_sha1", filter them out here:
- my @exec_parents;
foreach my $p (@parents) {
next unless defined $p;
if ($p =~ /^(\d+)=($sha1_short)$/o) {
if ($1 == $log_msg->{revision}) {
- push @exec_parents, $2;
+ push @tmp_parents, $2;
}
} else {
- push @exec_parents, $p if $p =~ /$sha1_short/o;
+ push @tmp_parents, $p if $p =~ /$sha1_short/o;
}
}
+ my $tree = $log_msg->{tree};
+ if (!defined $tree) {
+ my $index = set_index($GIT_SVN_INDEX);
+ index_changes();
+ chomp($tree = `git-write-tree`);
+ croak $? if $?;
+ restore_index($index);
+ }
+ if (exists $tree_map{$tree}) {
+ push @tmp_parents, @{$tree_map{$tree}};
+ }
+ foreach (@tmp_parents) {
+ next if $seen_parent{$_};
+ $seen_parent{$_} = 1;
+ push @exec_parents, $_;
+ # MAXPARENT is defined to 16 in commit-tree.c:
+ last if @exec_parents > 16;
+ }
- my $pid = fork;
- defined $pid or croak $!;
+ defined(my $pid = open my $out_fh, '-|') or croak $!;
if ($pid == 0) {
- $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
- index_changes();
- chomp(my $tree = `git-write-tree`);
- croak if $?;
- if (exists $tree_map{$tree}) {
- my %seen_parent = map { $_ => 1 } @exec_parents;
- foreach (@{$tree_map{$tree}}) {
- # MAXPARENT is defined to 16 in commit-tree.c:
- if ($seen_parent{$_} || @exec_parents > 16) {
- next;
- }
- push @exec_parents, $_;
- $seen_parent{$_} = 1;
- }
- }
my $msg_fh = IO::File->new_tmpfile or croak $!;
print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ",
"$SVN_URL\@$log_msg->{revision}",
" $SVN_UUID\n" or croak $!;
$msg_fh->flush == 0 or croak $!;
seek $msg_fh, 0, 0 or croak $!;
-
set_commit_env($log_msg);
-
my @exec = ('git-commit-tree',$tree);
push @exec, '-p', $_ foreach @exec_parents;
open STDIN, '<&', $msg_fh or croak $!;
- open STDOUT, '>&', $out_fh or croak $!;
exec @exec or croak $!;
}
- waitpid($pid,0);
- croak if $?;
-
- $out_fh->flush == 0 or croak $!;
- seek $out_fh, 0, 0 or croak $!;
chomp(my $commit = do { local $/; <$out_fh> });
+ close $out_fh or croak $?;
if ($commit !~ /^$sha1$/o) {
croak "Failed to commit, invalid sha1: $commit\n";
}
my @update_ref = ('git-update-ref',"refs/remotes/$GIT_SVN",$commit);
if (my $primary_parent = shift @exec_parents) {
- $pid = fork;
- defined $pid or croak $!;
- if (!$pid) {
- close STDERR;
- close STDOUT;
- exec 'git-rev-parse','--verify',
- "refs/remotes/$GIT_SVN^0";
- }
- waitpid $pid, 0;
+ quiet_run(qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0");
push @update_ref, $primary_parent unless $?;
}
sys(@update_ref);
- sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit);
+ revdb_set($REVDB, $log_msg->{revision}, $commit);
+
+ # this output is read via pipe, do not change:
print "r$log_msg->{revision} = $commit\n";
+ check_repack();
return $commit;
}
+sub check_repack {
+ if ($_repack && (--$_repack_nr == 0)) {
+ $_repack_nr = $_repack;
+ sys("git repack $_repack_flags");
+ }
+}
+
sub set_commit_env {
my ($log_msg) = @_;
my $author = $log_msg->{author};
+ if (!defined $author || length $author == 0) {
+ $author = '(no author)';
+ }
my ($name,$email) = defined $users{$author} ? @{$users{$author}}
: ($author,"$author\@$SVN_UUID");
$ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name;
if ($pid == 0) {
open STDOUT, '>&', $blob_fh or croak $!;
- exec('git-cat-file','blob',$blob);
+ exec('git-cat-file','blob',$blob) or croak $!;
}
waitpid $pid, 0;
croak $? if $?;
my $pid = open my $child, '-|';
defined $pid or croak $!;
if ($pid == 0) {
- exec(@_) or croak $?;
+ exec(@_) or croak $!;
}
my @ret = (<$child>);
close $child or croak $?;
if (grep /usage: checkout URL\[\@REV\]/,@co_help) {
$_svn_co_url_revs = 1;
}
+ if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) {
+ $_svn_pg_peg_revs = 1;
+ }
# I really, really hope nobody hits this...
unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) {
}
sub check_upgrade_needed {
+ if (!-r $REVDB) {
+ -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
+ open my $fh, '>>',$REVDB or croak $!;
+ close $fh;
+ }
my $old = eval {
my $pid = open my $child, '-|';
defined $pid or croak $!;
if ($pid == 0) {
close STDERR;
- exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $?;
+ exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $!;
}
my @ret = (<$child>);
close $child or croak $?;
# fills %tree_map with a reverse mapping of trees to commits. Useful
# for finding parents to commit on.
sub map_tree_joins {
+ my %seen;
foreach my $br (@_branch_from) {
my $pid = open my $pipe, '-|';
defined $pid or croak $!;
if ($pid == 0) {
- exec(qw(git-rev-list --pretty=raw), $br) or croak $?;
+ exec(qw(git-rev-list --topo-order --pretty=raw), $br)
+ or croak $!;
}
while (<$pipe>) {
if (/^commit ($sha1)$/o) {
my $commit = $1;
+
+ # if we've seen a commit,
+ # we've seen its parents
+ last if $seen{$commit};
my ($tree) = (<$pipe> =~ /^tree ($sha1)$/o);
unless (defined $tree) {
die "Failed to parse commit $commit\n";
}
push @{$tree_map{$tree}}, $commit;
+ $seen{$commit} = 1;
}
}
- close $pipe or croak $?;
+ close $pipe; # we could be breaking the pipe early
}
}
+sub load_all_refs {
+ if (@_branch_from) {
+ print STDERR '--branch|-b parameters are ignored when ',
+ "--branch-all-refs|-B is passed\n";
+ }
+
+ # don't worry about rev-list on non-commit objects/tags,
+ # it shouldn't blow up if a ref is a blob or tree...
+ chomp(@_branch_from = `git-rev-parse --symbolic --all`);
+}
+
# '<svn username> = real-name <email address>' mapping based on git-svnimport:
sub load_authors {
open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
close $authors or croak $!;
}
+sub rload_authors {
+ open my $authors, '<', $_authors or die "Can't open $_authors $!\n";
+ while (<$authors>) {
+ chomp;
+ next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/;
+ my ($user, $name, $email) = ($1, $2, $3);
+ $rusers{"$name <$email>"} = $user;
+ }
+ close $authors or croak $!;
+}
+
+sub svn_propget_base {
+ my ($p, $f) = @_;
+ $f .= '@BASE' if $_svn_pg_peg_revs;
+ return safe_qx(qw/svn propget/, $p, $f);
+}
+
+sub git_svn_each {
+ my $sub = shift;
+ foreach (`git-rev-parse --symbolic --all`) {
+ next unless s#^refs/remotes/##;
+ chomp $_;
+ next unless -f "$GIT_DIR/svn/$_/info/url";
+ &$sub($_);
+ }
+}
+
+sub migrate_revdb {
+ git_svn_each(sub {
+ my $id = shift;
+ defined(my $pid = fork) or croak $!;
+ if (!$pid) {
+ $GIT_SVN = $ENV{GIT_SVN_ID} = $id;
+ init_vars();
+ exit 0 if -r $REVDB;
+ print "Upgrading svn => git mapping...\n";
+ -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]);
+ open my $fh, '>>',$REVDB or croak $!;
+ close $fh;
+ rebuild();
+ print "Done upgrading. You may now delete the ",
+ "deprecated $GIT_SVN_DIR/revs directory\n";
+ exit 0;
+ }
+ waitpid $pid, 0;
+ croak $? if $?;
+ });
+}
+
+sub migration_check {
+ migrate_revdb() unless (-e $REVDB);
+ return if (-d "$GIT_DIR/svn" || !-d $GIT_DIR);
+ print "Upgrading repository...\n";
+ unless (-d "$GIT_DIR/svn") {
+ mkdir "$GIT_DIR/svn" or croak $!;
+ }
+ print "Data from a previous version of git-svn exists, but\n\t",
+ "$GIT_SVN_DIR\n\t(required for this version ",
+ "($VERSION) of git-svn) does not.\n";
+
+ foreach my $x (`git-rev-parse --symbolic --all`) {
+ next unless $x =~ s#^refs/remotes/##;
+ chomp $x;
+ next unless -f "$GIT_DIR/$x/info/url";
+ my $u = eval { file_to_s("$GIT_DIR/$x/info/url") };
+ next unless $u;
+ my $dn = dirname("$GIT_DIR/svn/$x");
+ mkpath([$dn]) unless -d $dn;
+ rename "$GIT_DIR/$x", "$GIT_DIR/svn/$x" or croak "$!: $x";
+ }
+ migrate_revdb() if (-d $GIT_SVN_DIR && !-w $REVDB);
+ print "Done upgrading.\n";
+}
+
+sub find_rev_before {
+ my ($r, $id, $eq_ok) = @_;
+ my $f = "$GIT_DIR/svn/$id/.rev_db";
+ return (undef,undef) unless -r $f;
+ --$r unless $eq_ok;
+ while ($r > 0) {
+ if (my $c = revdb_get($f, $r)) {
+ return ($r, $c);
+ }
+ --$r;
+ }
+ return (undef, undef);
+}
+
+sub init_vars {
+ $GIT_SVN ||= $ENV{GIT_SVN_ID} || 'git-svn';
+ $GIT_SVN_DIR = "$GIT_DIR/svn/$GIT_SVN";
+ $REVDB = "$GIT_SVN_DIR/.rev_db";
+ $GIT_SVN_INDEX = "$GIT_SVN_DIR/index";
+ $SVN_URL = undef;
+ $SVN_WC = "$GIT_SVN_DIR/tree";
+}
+
+# convert GetOpt::Long specs for use by git-repo-config
+sub read_repo_config {
+ return unless -d $GIT_DIR;
+ my $opts = shift;
+ foreach my $o (keys %$opts) {
+ my $v = $opts->{$o};
+ my ($key) = ($o =~ /^([a-z\-]+)/);
+ $key =~ s/-//g;
+ my $arg = 'git-repo-config';
+ $arg .= ' --int' if ($o =~ /[:=]i$/);
+ $arg .= ' --bool' if ($o !~ /[:=][sfi]$/);
+ if (ref $v eq 'ARRAY') {
+ chomp(my @tmp = `$arg --get-all svn.$key`);
+ @$v = @tmp if @tmp;
+ } else {
+ chomp(my $tmp = `$arg --get svn.$key`);
+ if ($tmp && !($arg =~ / --bool / && $tmp eq 'false')) {
+ $$v = $tmp;
+ }
+ }
+ }
+}
+
+sub set_default_vals {
+ if (defined $_repack) {
+ $_repack = 1000 if ($_repack <= 0);
+ $_repack_nr = $_repack;
+ $_repack_flags ||= '-d';
+ }
+}
+
+sub read_grafts {
+ my $gr_file = shift;
+ my ($grafts, $comments) = ({}, {});
+ if (open my $fh, '<', $gr_file) {
+ my @tmp;
+ while (<$fh>) {
+ if (/^($sha1)\s+/) {
+ my $c = $1;
+ if (@tmp) {
+ @{$comments->{$c}} = @tmp;
+ @tmp = ();
+ }
+ foreach my $p (split /\s+/, $_) {
+ $grafts->{$c}->{$p} = 1;
+ }
+ } else {
+ push @tmp, $_;
+ }
+ }
+ close $fh or croak $!;
+ @{$comments->{'END'}} = @tmp if @tmp;
+ }
+ return ($grafts, $comments);
+}
+
+sub write_grafts {
+ my ($grafts, $comments, $gr_file) = @_;
+
+ open my $fh, '>', $gr_file or croak $!;
+ foreach my $c (sort keys %$grafts) {
+ if ($comments->{$c}) {
+ print $fh $_ foreach @{$comments->{$c}};
+ }
+ my $p = $grafts->{$c};
+ delete $p->{$c}; # commits are not self-reproducing...
+ my $pid = open my $ch, '-|';
+ defined $pid or croak $!;
+ if (!$pid) {
+ exec(qw/git-cat-file commit/, $c) or croak $!;
+ }
+ while (<$ch>) {
+ if (/^parent ([a-f\d]{40})/) {
+ $p->{$1} = 1;
+ } else {
+ last unless /^\S/i;
+ }
+ }
+ close $ch; # breaking the pipe
+ print $fh $c, ' ', join(' ', sort keys %$p),"\n";
+ }
+ if ($comments->{'END'}) {
+ print $fh $_ foreach @{$comments->{'END'}};
+ }
+ close $fh or croak $!;
+}
+
+sub read_url_paths {
+ my $l_map = {};
+ git_svn_each(sub { my $x = shift;
+ my $url = file_to_s("$GIT_DIR/svn/$x/info/url");
+ my ($u, $p) = repo_path_split($url);
+ $l_map->{$u}->{$p} = $x;
+ });
+ return $l_map;
+}
+
+sub extract_metadata {
+ my $id = shift;
+ my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
+ \s([a-f\d\-]+)$/x);
+ if (!$rev || !$uuid || !$url) {
+ # some of the original repositories I made had
+ # indentifiers like this:
+ ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/);
+ }
+ return ($url, $rev, $uuid);
+}
+
+sub tz_to_s_offset {
+ my ($tz) = @_;
+ $tz =~ s/(\d\d)$//;
+ return ($1 * 60) + ($tz * 3600);
+}
+
+sub setup_pager { # translated to Perl from pager.c
+ return unless (-t *STDOUT);
+ my $pager = $ENV{PAGER};
+ if (!defined $pager) {
+ $pager = 'less';
+ } elsif (length $pager == 0 || $pager eq 'cat') {
+ return;
+ }
+ pipe my $rfd, my $wfd or return;
+ defined(my $pid = fork) or croak $!;
+ if (!$pid) {
+ open STDOUT, '>&', $wfd or croak $!;
+ return;
+ }
+ open STDIN, '<&', $rfd or croak $!;
+ $ENV{LESS} ||= '-S';
+ exec $pager or croak "Can't run pager: $!\n";;
+}
+
+sub get_author_info {
+ my ($dest, $author, $t, $tz) = @_;
+ $author =~ s/(?:^\s*|\s*$)//g;
+ $dest->{a_raw} = $author;
+ my $_a;
+ if ($_authors) {
+ $_a = $rusers{$author} || undef;
+ }
+ if (!$_a) {
+ ($_a) = ($author =~ /<([^>]+)\@[^>]+>$/);
+ }
+ $dest->{t} = $t;
+ $dest->{tz} = $tz;
+ $dest->{a} = $_a;
+ # Date::Parse isn't in the standard Perl distro :(
+ if ($tz =~ s/^\+//) {
+ $t += tz_to_s_offset($tz);
+ } elsif ($tz =~ s/^\-//) {
+ $t -= tz_to_s_offset($tz);
+ }
+ $dest->{t_utc} = $t;
+}
+
+sub process_commit {
+ my ($c, $r_min, $r_max, $defer) = @_;
+ if (defined $r_min && defined $r_max) {
+ if ($r_min == $c->{r} && $r_min == $r_max) {
+ show_commit($c);
+ return 0;
+ }
+ return 1 if $r_min == $r_max;
+ if ($r_min < $r_max) {
+ # we need to reverse the print order
+ return 0 if (defined $_limit && --$_limit < 0);
+ push @$defer, $c;
+ return 1;
+ }
+ if ($r_min != $r_max) {
+ return 1 if ($r_min < $c->{r});
+ return 1 if ($r_max > $c->{r});
+ }
+ }
+ return 0 if (defined $_limit && --$_limit < 0);
+ show_commit($c);
+ return 1;
+}
+
+sub show_commit {
+ my $c = shift;
+ if ($_oneline) {
+ my $x = "\n";
+ if (my $l = $c->{l}) {
+ while ($l->[0] =~ /^\s*$/) { shift @$l }
+ $x = $l->[0];
+ }
+ $_l_fmt ||= 'A' . length($c->{r});
+ print 'r',pack($_l_fmt, $c->{r}),' | ';
+ print "$c->{c} | " if $_show_commit;
+ print $x;
+ } else {
+ show_commit_normal($c);
+ }
+}
+
+sub show_commit_normal {
+ my ($c) = @_;
+ print '-' x72, "\nr$c->{r} | ";
+ print "$c->{c} | " if $_show_commit;
+ print "$c->{a} | ", strftime("%Y-%m-%d %H:%M:%S %z (%a, %d %b %Y)",
+ localtime($c->{t_utc})), ' | ';
+ my $nr_line = 0;
+
+ if (my $l = $c->{l}) {
+ while ($l->[$#$l] eq "\n" && $l->[($#$l - 1)] eq "\n") {
+ pop @$l;
+ }
+ $nr_line = scalar @$l;
+ if (!$nr_line) {
+ print "1 line\n\n\n";
+ } else {
+ if ($nr_line == 1) {
+ $nr_line = '1 line';
+ } else {
+ $nr_line .= ' lines';
+ }
+ print $nr_line, "\n\n";
+ print $_ foreach @$l;
+ }
+ } else {
+ print "1 line\n\n";
+
+ }
+ foreach my $x (qw/raw diff/) {
+ if ($c->{$x}) {
+ print "\n";
+ print $_ foreach @{$c->{$x}}
+ }
+ }
+}
+
+sub libsvn_load {
+ return unless $_use_lib;
+ $_use_lib = eval {
+ require SVN::Core;
+ if ($SVN::Core::VERSION lt '1.2.1') {
+ die "Need SVN::Core 1.2.1 or better ",
+ "(got $SVN::Core::VERSION) ",
+ "Falling back to command-line svn\n";
+ }
+ require SVN::Ra;
+ require SVN::Delta;
+ push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor';
+ my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file.
+ $SVN::Node::dir.$SVN::Node::unknown.
+ $SVN::Node::none.$SVN::Node::file.
+ $SVN::Node::dir.$SVN::Node::unknown;
+ 1;
+ };
+}
+
+sub libsvn_connect {
+ my ($url) = @_;
+ my $auth = SVN::Core::auth_open([SVN::Client::get_simple_provider(),
+ SVN::Client::get_ssl_server_trust_file_provider(),
+ SVN::Client::get_username_provider()]);
+ my $s = eval { SVN::Ra->new(url => $url, auth => $auth) };
+ return $s;
+}
+
+sub libsvn_get_file {
+ my ($gui, $f, $rev) = @_;
+ my $p = $f;
+ return unless ($p =~ s#^\Q$SVN_PATH\E/?##);
+
+ my ($hash, $pid, $in, $out);
+ my $pool = SVN::Pool->new;
+ defined($pid = open3($in, $out, '>&STDERR',
+ qw/git-hash-object -w --stdin/)) or croak $!;
+ my ($r, $props) = $SVN->get_file($f, $rev, $in, $pool);
+ $in->flush == 0 or croak $!;
+ close $in or croak $!;
+ $pool->clear;
+ chomp($hash = do { local $/; <$out> });
+ close $out or croak $!;
+ waitpid $pid, 0;
+ $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
+
+ my $mode = exists $props->{'svn:executable'} ? '100755' : '100644';
+ if (exists $props->{'svn:special'}) {
+ $mode = '120000';
+ my $link = `git-cat-file blob $hash`;
+ $link =~ s/^link // or die "svn:special file with contents: <",
+ $link, "> is not understood\n";
+ defined($pid = open3($in, $out, '>&STDERR',
+ qw/git-hash-object -w --stdin/)) or croak $!;
+ print $in $link;
+ $in->flush == 0 or croak $!;
+ close $in or croak $!;
+ chomp($hash = do { local $/; <$out> });
+ close $out or croak $!;
+ waitpid $pid, 0;
+ $hash =~ /^$sha1$/o or die "not a sha1: $hash\n";
+ }
+ print $gui $mode,' ',$hash,"\t",$p,"\0" or croak $!;
+}
+
+sub libsvn_log_entry {
+ my ($rev, $author, $date, $msg, $parents) = @_;
+ my ($Y,$m,$d,$H,$M,$S) = ($date =~ /^(\d{4})\-(\d\d)\-(\d\d)T
+ (\d\d)\:(\d\d)\:(\d\d).\d+Z$/x)
+ or die "Unable to parse date: $date\n";
+ if (defined $_authors && ! defined $users{$author}) {
+ die "Author: $author not defined in $_authors file\n";
+ }
+ return { revision => $rev, date => "+0000 $Y-$m-$d $H:$M:$S",
+ author => $author, msg => $msg."\n", parents => $parents || [] }
+}
+
+sub process_rm {
+ my ($gui, $last_commit, $f) = @_;
+ $f =~ s#^\Q$SVN_PATH\E/?## or return;
+ # remove entire directories.
+ if (safe_qx('git-ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) {
+ defined(my $pid = open my $ls, '-|') or croak $!;
+ if (!$pid) {
+ exec(qw/git-ls-tree -r --name-only -z/,
+ $last_commit,'--',$f) or croak $!;
+ }
+ local $/ = "\0";
+ while (<$ls>) {
+ print $gui '0 ',0 x 40,"\t",$_ or croak $!;
+ }
+ close $ls or croak $?;
+ } else {
+ print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!;
+ }
+}
+
+sub libsvn_fetch {
+ my ($last_commit, $paths, $rev, $author, $date, $msg) = @_;
+ open my $gui, '| git-update-index -z --index-info' or croak $!;
+ my @amr;
+ foreach my $f (keys %$paths) {
+ my $m = $paths->{$f}->action();
+ $f =~ s#^/+##;
+ if ($m =~ /^[DR]$/) {
+ process_rm($gui, $last_commit, $f);
+ next if $m eq 'D';
+ # 'R' can be file replacements, too, right?
+ }
+ my $pool = SVN::Pool->new;
+ my $t = $SVN->check_path($f, $rev, $pool);
+ if ($t == $SVN::Node::file) {
+ if ($m =~ /^[AMR]$/) {
+ push @amr, $f;
+ } else {
+ die "Unrecognized action: $m, ($f r$rev)\n";
+ }
+ }
+ $pool->clear;
+ }
+ libsvn_get_file($gui, $_, $rev) foreach (@amr);
+ close $gui or croak $?;
+ return libsvn_log_entry($rev, $author, $date, $msg, [$last_commit]);
+}
+
+sub svn_grab_base_rev {
+ defined(my $pid = open my $fh, '-|') or croak $!;
+ if (!$pid) {
+ open my $null, '>', '/dev/null' or croak $!;
+ open STDERR, '>&', $null or croak $!;
+ exec qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0"
+ or croak $!;
+ }
+ chomp(my $c = do { local $/; <$fh> });
+ close $fh;
+ if (defined $c && length $c) {
+ my ($url, $rev, $uuid) = extract_metadata((grep(/^git-svn-id: /,
+ safe_qx(qw/git-cat-file commit/, $c)))[-1]);
+ return ($rev, $c);
+ }
+ return (undef, undef);
+}
+
+sub libsvn_parse_revision {
+ my $base = shift;
+ my $head = $SVN->get_latest_revnum();
+ if (!defined $_revision || $_revision eq 'BASE:HEAD') {
+ return ($base + 1, $head) if (defined $base);
+ return (0, $head);
+ }
+ return ($1, $2) if ($_revision =~ /^(\d+):(\d+)$/);
+ return ($_revision, $_revision) if ($_revision =~ /^\d+$/);
+ if ($_revision =~ /^BASE:(\d+)$/) {
+ return ($base + 1, $1) if (defined $base);
+ return (0, $head);
+ }
+ return ($1, $head) if ($_revision =~ /^(\d+):HEAD$/);
+ die "revision argument: $_revision not understood by git-svn\n",
+ "Try using the command-line svn client instead\n";
+}
+
+sub libsvn_traverse {
+ my ($gui, $pfx, $path, $rev) = @_;
+ my $cwd = "$pfx/$path";
+ my $pool = SVN::Pool->new;
+ $cwd =~ s#^/+##g;
+ my ($dirent, $r, $props) = $SVN->get_dir($cwd, $rev, $pool);
+ foreach my $d (keys %$dirent) {
+ my $t = $dirent->{$d}->kind;
+ if ($t == $SVN::Node::dir) {
+ libsvn_traverse($gui, $cwd, $d, $rev);
+ } elsif ($t == $SVN::Node::file) {
+ libsvn_get_file($gui, "$cwd/$d", $rev);
+ }
+ }
+ $pool->clear;
+}
+
+sub libsvn_traverse_ignore {
+ my ($fh, $path, $r) = @_;
+ $path =~ s#^/+##g;
+ my $pool = SVN::Pool->new;
+ my ($dirent, undef, $props) = $SVN->get_dir($path, $r, $pool);
+ my $p = $path;
+ $p =~ s#^\Q$SVN_PATH\E/?##;
+ print $fh length $p ? "\n# $p\n" : "\n# /\n";
+ if (my $s = $props->{'svn:ignore'}) {
+ $s =~ s/[\r\n]+/\n/g;
+ chomp $s;
+ if (length $p == 0) {
+ $s =~ s#\n#\n/$p#g;
+ print $fh "/$s\n";
+ } else {
+ $s =~ s#\n#\n/$p/#g;
+ print $fh "/$p/$s\n";
+ }
+ }
+ foreach (sort keys %$dirent) {
+ next if $dirent->{$_}->kind != $SVN::Node::dir;
+ libsvn_traverse_ignore($fh, "$path/$_", $r);
+ }
+ $pool->clear;
+}
+
+sub revisions_eq {
+ my ($path, $r0, $r1) = @_;
+ return 1 if $r0 == $r1;
+ my $nr = 0;
+ if ($_use_lib) {
+ # should be OK to use Pool here (r1 - r0) should be small
+ my $pool = SVN::Pool->new;
+ $SVN->get_log("/$path", $r0, $r1, 0, 1, 1, sub {$nr++},$pool);
+ $pool->clear;
+ } else {
+ my ($url, undef) = repo_path_split($SVN_URL);
+ my $svn_log = svn_log_raw("$url/$path","-r$r0:$r1");
+ while (next_log_entry($svn_log)) { $nr++ }
+ close $svn_log->{fh};
+ }
+ return 0 if ($nr > 1);
+ return 1;
+}
+
+sub libsvn_find_parent_branch {
+ my ($paths, $rev, $author, $date, $msg) = @_;
+ my $svn_path = '/'.$SVN_PATH;
+
+ # look for a parent from another branch:
+ my $i = $paths->{$svn_path} or return;
+ my $branch_from = $i->copyfrom_path or return;
+ my $r = $i->copyfrom_rev;
+ print STDERR "Found possible branch point: ",
+ "$branch_from => $svn_path, $r\n";
+ $branch_from =~ s#^/##;
+ my $l_map = read_url_paths();
+ my $url = $SVN->{url};
+ defined $l_map->{$url} or return;
+ my $id = $l_map->{$url}->{$branch_from} or return;
+ my ($r0, $parent) = find_rev_before($r,$id,1);
+ return unless (defined $r0 && defined $parent);
+ if (revisions_eq($branch_from, $r0, $r)) {
+ unlink $GIT_SVN_INDEX;
+ print STDERR "Found branch parent: $parent\n";
+ sys(qw/git-read-tree/, $parent);
+ return libsvn_fetch($parent, $paths, $rev,
+ $author, $date, $msg);
+ }
+ print STDERR "Nope, branch point not imported or unknown\n";
+ return undef;
+}
+
+sub libsvn_new_tree {
+ if (my $log_entry = libsvn_find_parent_branch(@_)) {
+ return $log_entry;
+ }
+ my ($paths, $rev, $author, $date, $msg) = @_;
+ open my $gui, '| git-update-index -z --index-info' or croak $!;
+ my $pool = SVN::Pool->new;
+ libsvn_traverse($gui, '', $SVN_PATH, $rev, $pool);
+ $pool->clear;
+ close $gui or croak $?;
+ return libsvn_log_entry($rev, $author, $date, $msg);
+}
+
+sub find_graft_path_commit {
+ my ($tree_paths, $p1, $r1) = @_;
+ foreach my $x (keys %$tree_paths) {
+ next unless ($p1 =~ /^\Q$x\E/);
+ my $i = $tree_paths->{$x};
+ my ($r0, $parent) = find_rev_before($r1,$i,1);
+ return $parent if (defined $r0 && $r0 == $r1);
+ print STDERR "r$r1 of $i not imported\n";
+ next;
+ }
+ return undef;
+}
+
+sub find_graft_path_parents {
+ my ($grafts, $tree_paths, $c, $p0, $r0) = @_;
+ foreach my $x (keys %$tree_paths) {
+ next unless ($p0 =~ /^\Q$x\E/);
+ my $i = $tree_paths->{$x};
+ my ($r, $parent) = find_rev_before($r0, $i, 1);
+ if (defined $r && defined $parent && revisions_eq($x,$r,$r0)) {
+ $grafts->{$c}->{$parent} = 1;
+ }
+ }
+}
+
+sub libsvn_graft_file_copies {
+ my ($grafts, $tree_paths, $path, $paths, $rev) = @_;
+ foreach (keys %$paths) {
+ my $i = $paths->{$_};
+ my ($m, $p0, $r0) = ($i->action, $i->copyfrom_path,
+ $i->copyfrom_rev);
+ next unless (defined $p0 && defined $r0);
+
+ my $p1 = $_;
+ $p1 =~ s#^/##;
+ $p0 =~ s#^/##;
+ my $c = find_graft_path_commit($tree_paths, $p1, $rev);
+ next unless $c;
+ find_graft_path_parents($grafts, $tree_paths, $c, $p0, $r0);
+ }
+}
+
+sub set_index {
+ my $old = $ENV{GIT_INDEX_FILE};
+ $ENV{GIT_INDEX_FILE} = shift;
+ return $old;
+}
+
+sub restore_index {
+ my ($old) = @_;
+ if (defined $old) {
+ $ENV{GIT_INDEX_FILE} = $old;
+ } else {
+ delete $ENV{GIT_INDEX_FILE};
+ }
+}
+
+sub libsvn_commit_cb {
+ my ($rev, $date, $committer, $c, $msg, $r_last, $cmt_last) = @_;
+ if ($_optimize_commits && $rev == ($r_last + 1)) {
+ my $log = libsvn_log_entry($rev,$committer,$date,$msg);
+ $log->{tree} = get_tree_from_treeish($c);
+ my $cmt = git_commit($log, $cmt_last, $c);
+ my @diff = safe_qx('git-diff-tree', $cmt, $c);
+ if (@diff) {
+ print STDERR "Trees differ: $cmt $c\n",
+ join('',@diff),"\n";
+ exit 1;
+ }
+ } else {
+ fetch("$rev=$c");
+ }
+}
+
+sub libsvn_ls_fullurl {
+ my $fullurl = shift;
+ my ($repo, $path) = repo_path_split($fullurl);
+ $SVN ||= libsvn_connect($repo);
+ my @ret;
+ my $pool = SVN::Pool->new;
+ my ($dirent, undef, undef) = $SVN->get_dir($path,
+ $SVN->get_latest_revnum, $pool);
+ foreach my $d (keys %$dirent) {
+ if ($dirent->{$d}->kind == $SVN::Node::dir) {
+ push @ret, "$d/"; # add '/' for compat with cli svn
+ }
+ }
+ $pool->clear;
+ return @ret;
+}
+
+
+sub libsvn_skip_unknown_revs {
+ my $err = shift;
+ my $errno = $err->apr_err();
+ # Maybe the branch we're tracking didn't
+ # exist when the repo started, so it's
+ # not an error if it doesn't, just continue
+ #
+ # Wonderfully consistent library, eh?
+ # 160013 - svn:// and file://
+ # 175002 - http(s)://
+ # More codes may be discovered later...
+ if ($errno == 175002 || $errno == 160013) {
+ return;
+ }
+ croak "Error from SVN, ($errno): ", $err->expanded_message,"\n";
+};
+
+# Tie::File seems to be prone to offset errors if revisions get sparse,
+# it's not that fast, either. Tie::File is also not in Perl 5.6. So
+# one of my favorite modules is out :< Next up would be one of the DBM
+# modules, but I'm not sure which is most portable... So I'll just
+# go with something that's plain-text, but still capable of
+# being randomly accessed. So here's my ultra-simple fixed-width
+# database. All records are 40 characters + "\n", so it's easy to seek
+# to a revision: (41 * rev) is the byte offset.
+# A record of 40 0s denotes an empty revision.
+# And yes, it's still pretty fast (faster than Tie::File).
+sub revdb_set {
+ my ($file, $rev, $commit) = @_;
+ length $commit == 40 or croak "arg3 must be a full SHA1 hexsum\n";
+ open my $fh, '+<', $file or croak $!;
+ my $offset = $rev * 41;
+ # assume that append is the common case:
+ seek $fh, 0, 2 or croak $!;
+ my $pos = tell $fh;
+ if ($pos < $offset) {
+ print $fh (('0' x 40),"\n") x (($offset - $pos) / 41);
+ }
+ seek $fh, $offset, 0 or croak $!;
+ print $fh $commit,"\n";
+ close $fh or croak $!;
+}
+
+sub revdb_get {
+ my ($file, $rev) = @_;
+ my $ret;
+ my $offset = $rev * 41;
+ open my $fh, '<', $file or croak $!;
+ seek $fh, $offset, 0;
+ if (tell $fh == $offset) {
+ $ret = readline $fh;
+ if (defined $ret) {
+ chomp $ret;
+ $ret = undef if ($ret =~ /^0{40}$/);
+ }
+ }
+ close $fh or croak $!;
+ return $ret;
+}
+
+sub copy_remote_ref {
+ my $origin = $_cp_remote ? $_cp_remote : 'origin';
+ my $ref = "refs/remotes/$GIT_SVN";
+ if (safe_qx('git-ls-remote', $origin, $ref)) {
+ sys(qw/git fetch/, $origin, "$ref:$ref");
+ } else {
+ die "Unable to find remote reference: ",
+ "refs/remotes/$GIT_SVN on $origin\n";
+ }
+}
+
+package SVN::Git::Editor;
+use vars qw/@ISA/;
+use strict;
+use warnings;
+use Carp qw/croak/;
+use IO::File;
+
+sub new {
+ my $class = shift;
+ my $git_svn = shift;
+ my $self = SVN::Delta::Editor->new(@_);
+ bless $self, $class;
+ foreach (qw/svn_path c r ra /) {
+ die "$_ required!\n" unless (defined $git_svn->{$_});
+ $self->{$_} = $git_svn->{$_};
+ }
+ $self->{pool} = SVN::Pool->new;
+ $self->{bat} = { '' => $self->open_root($self->{r}, $self->{pool}) };
+ $self->{rm} = { };
+ require Digest::MD5;
+ return $self;
+}
+
+sub split_path {
+ return ($_[0] =~ m#^(.*?)/?([^/]+)$#);
+}
+
+sub repo_path {
+ (defined $_[1] && length $_[1]) ? "$_[0]->{svn_path}/$_[1]"
+ : $_[0]->{svn_path}
+}
+
+sub url_path {
+ my ($self, $path) = @_;
+ $self->{ra}->{url} . '/' . $self->repo_path($path);
+}
+
+sub rmdirs {
+ my ($self) = @_;
+ my $rm = $self->{rm};
+ delete $rm->{''}; # we never delete the url we're tracking
+ return unless %$rm;
+
+ foreach (keys %$rm) {
+ my @d = split m#/#, $_;
+ my $c = shift @d;
+ $rm->{$c} = 1;
+ while (@d) {
+ $c .= '/' . shift @d;
+ $rm->{$c} = 1;
+ }
+ }
+ delete $rm->{$self->{svn_path}};
+ delete $rm->{''}; # we never delete the url we're tracking
+ return unless %$rm;
+
+ defined(my $pid = open my $fh,'-|') or croak $!;
+ if (!$pid) {
+ exec qw/git-ls-tree --name-only -r -z/, $self->{c} or croak $!;
+ }
+ local $/ = "\0";
+ while (<$fh>) {
+ chomp;
+ $_ = $self->{svn_path} . '/' . $_;
+ my ($dn) = ($_ =~ m#^(.*?)/?(?:[^/]+)$#);
+ delete $rm->{$dn};
+ last unless %$rm;
+ }
+ my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat});
+ foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) {
+ $self->close_directory($bat->{$d}, $p);
+ my ($dn) = ($d =~ m#^(.*?)/?(?:[^/]+)$#);
+ $self->SUPER::delete_entry($d, $r, $bat->{$dn}, $p);
+ delete $bat->{$d};
+ }
+}
+
+sub open_or_add_dir {
+ my ($self, $full_path, $baton) = @_;
+ my $p = SVN::Pool->new;
+ my $t = $self->{ra}->check_path($full_path, $self->{r}, $p);
+ $p->clear;
+ if ($t == $SVN::Node::none) {
+ return $self->add_directory($full_path, $baton,
+ undef, -1, $self->{pool});
+ } elsif ($t == $SVN::Node::dir) {
+ return $self->open_directory($full_path, $baton,
+ $self->{r}, $self->{pool});
+ }
+ print STDERR "$full_path already exists in repository at ",
+ "r$self->{r} and it is not a directory (",
+ ($t == $SVN::Node::file ? 'file' : 'unknown'),"/$t)\n";
+ exit 1;
+}
+
+sub ensure_path {
+ my ($self, $path) = @_;
+ my $bat = $self->{bat};
+ $path = $self->repo_path($path);
+ return $bat->{''} unless (length $path);
+ my @p = split m#/+#, $path;
+ my $c = shift @p;
+ $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{''});
+ while (@p) {
+ my $c0 = $c;
+ $c .= '/' . shift @p;
+ $bat->{$c} ||= $self->open_or_add_dir($c, $bat->{$c0});
+ }
+ return $bat->{$c};
+}
+
+sub A {
+ my ($self, $m) = @_;
+ my ($dir, $file) = split_path($m->{file_b});
+ my $pbat = $self->ensure_path($dir);
+ my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
+ undef, -1);
+ $self->chg_file($fbat, $m);
+ $self->close_file($fbat,undef,$self->{pool});
+}
+
+sub C {
+ my ($self, $m) = @_;
+ my ($dir, $file) = split_path($m->{file_b});
+ my $pbat = $self->ensure_path($dir);
+ my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
+ $self->url_path($m->{file_a}), $self->{r});
+ $self->chg_file($fbat, $m);
+ $self->close_file($fbat,undef,$self->{pool});
+}
+
+sub delete_entry {
+ my ($self, $path, $pbat) = @_;
+ my $rpath = $self->repo_path($path);
+ my ($dir, $file) = split_path($rpath);
+ $self->{rm}->{$dir} = 1;
+ $self->SUPER::delete_entry($rpath, $self->{r}, $pbat, $self->{pool});
+}
+
+sub R {
+ my ($self, $m) = @_;
+ my ($dir, $file) = split_path($m->{file_b});
+ my $pbat = $self->ensure_path($dir);
+ my $fbat = $self->add_file($self->repo_path($m->{file_b}), $pbat,
+ $self->url_path($m->{file_a}), $self->{r});
+ $self->chg_file($fbat, $m);
+ $self->close_file($fbat,undef,$self->{pool});
+
+ ($dir, $file) = split_path($m->{file_a});
+ $pbat = $self->ensure_path($dir);
+ $self->delete_entry($m->{file_a}, $pbat);
+}
+
+sub M {
+ my ($self, $m) = @_;
+ my ($dir, $file) = split_path($m->{file_b});
+ my $pbat = $self->ensure_path($dir);
+ my $fbat = $self->open_file($self->repo_path($m->{file_b}),
+ $pbat,$self->{r},$self->{pool});
+ $self->chg_file($fbat, $m);
+ $self->close_file($fbat,undef,$self->{pool});
+}
+
+sub T { shift->M(@_) }
+
+sub change_file_prop {
+ my ($self, $fbat, $pname, $pval) = @_;
+ $self->SUPER::change_file_prop($fbat, $pname, $pval, $self->{pool});
+}
+
+sub chg_file {
+ my ($self, $fbat, $m) = @_;
+ if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
+ $self->change_file_prop($fbat,'svn:executable','*');
+ } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
+ $self->change_file_prop($fbat,'svn:executable',undef);
+ }
+ my $fh = IO::File->new_tmpfile or croak $!;
+ if ($m->{mode_b} =~ /^120/) {
+ print $fh 'link ' or croak $!;
+ $self->change_file_prop($fbat,'svn:special','*');
+ } elsif ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) {
+ $self->change_file_prop($fbat,'svn:special',undef);
+ }
+ defined(my $pid = fork) or croak $!;
+ if (!$pid) {
+ open STDOUT, '>&', $fh or croak $!;
+ exec qw/git-cat-file blob/, $m->{sha1_b} or croak $!;
+ }
+ waitpid $pid, 0;
+ croak $? if $?;
+ $fh->flush == 0 or croak $!;
+ seek $fh, 0, 0 or croak $!;
+
+ my $md5 = Digest::MD5->new;
+ $md5->addfile($fh) or croak $!;
+ seek $fh, 0, 0 or croak $!;
+
+ my $exp = $md5->hexdigest;
+ my $atd = $self->apply_textdelta($fbat, undef, $self->{pool});
+ my $got = SVN::TxDelta::send_stream($fh, @$atd, $self->{pool});
+ die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp);
+
+ close $fh or croak $!;
+}
+
+sub D {
+ my ($self, $m) = @_;
+ my ($dir, $file) = split_path($m->{file_b});
+ my $pbat = $self->ensure_path($dir);
+ $self->delete_entry($m->{file_b}, $pbat);
+}
+
+sub close_edit {
+ my ($self) = @_;
+ my ($p,$bat) = ($self->{pool}, $self->{bat});
+ foreach (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$bat) {
+ $self->close_directory($bat->{$_}, $p);
+ }
+ $self->SUPER::close_edit($p);
+ $p->clear;
+}
+
+sub abort_edit {
+ my ($self) = @_;
+ $self->SUPER::abort_edit($self->{pool});
+ $self->{pool}->clear;
+}
+
__END__
Data structures:
file_b => new/current file name of a file (any chg)
}
;
+
+Notes:
+ I don't trust the each() function on unless I created %hash myself
+ because the internal iterator may not have started at base.
. ./test-lib.sh
GIT_DIR=$PWD/.git
-GIT_SVN_DIR=$GIT_DIR/git-svn
-SVN_TREE=$GIT_SVN_DIR/tree
+GIT_SVN_DIR=$GIT_DIR/svn/git-svn
+SVN_TREE=$GIT_SVN_DIR/svn-tree
svnadmin >/dev/null 2>&1
if test $? != 1
#
test_description='git-svn tests'
+GIT_SVN_LC_ALL=$LC_ALL
. ./lib-git-svn.sh
mkdir import
cd import
echo foo > foo
-ln -s foo foo.link
+if test -z "$NO_SYMLINK"
+then
+ ln -s foo foo.link
+fi
mkdir -p dir/a/b/c/d/e
echo 'deep dir' > dir/a/b/c/d/e/file
mkdir -p bar
'import an SVN revision into git' \
'git-svn fetch'
+test_expect_success "checkout from svn" "svn co $svnrepo $SVN_TREE"
name='try a deep --rmdir with a commit'
-git checkout -b mybranch remotes/git-svn
+git checkout -f -b mybranch remotes/git-svn
mv dir/a/b/c/d/e/file dir/file
cp dir/file file
git update-index --add --remove dir/a/b/c/d/e/file dir/file file
test_expect_success "$name" \
"git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch &&
+ svn up $SVN_TREE &&
test -d $SVN_TREE/dir && test ! -d $SVN_TREE/dir/a"
git update-index --add dir/file/file
git commit -m "$name"
-test_expect_code 1 "$name" \
+test_expect_failure "$name" \
'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch' \
|| true
name='detect node change from directory to file #1'
rm -rf dir $GIT_DIR/index
-git checkout -b mybranch2 remotes/git-svn
+git checkout -f -b mybranch2 remotes/git-svn
mv bar/zzz zzz
rm -rf bar
mv zzz bar
git update-index --add -- bar
git commit -m "$name"
-test_expect_code 1 "$name" \
+test_expect_failure "$name" \
'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch2' \
|| true
name='detect node change from file to directory #2'
rm -f $GIT_DIR/index
-git checkout -b mybranch3 remotes/git-svn
+git checkout -f -b mybranch3 remotes/git-svn
rm bar/zzz
git-update-index --remove bar/zzz
mkdir bar/zzz
git-update-index --add bar/zzz/yyy
git commit -m "$name"
-test_expect_code 1 "$name" \
+test_expect_failure "$name" \
'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch3' \
|| true
name='detect node change from directory to file #2'
rm -f $GIT_DIR/index
-git checkout -b mybranch4 remotes/git-svn
+git checkout -f -b mybranch4 remotes/git-svn
rm -rf dir
git update-index --remove -- dir/file
touch dir
git update-index --add -- dir
git commit -m "$name"
-test_expect_code 1 "$name" \
+test_expect_failure "$name" \
'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch4' \
|| true
name='remove executable bit from a file'
rm -f $GIT_DIR/index
-git checkout -b mybranch5 remotes/git-svn
+git checkout -f -b mybranch5 remotes/git-svn
chmod -x exec.sh
git update-index exec.sh
git commit -m "$name"
test_expect_success "$name" \
"git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 &&
+ svn up $SVN_TREE &&
test ! -x $SVN_TREE/exec.sh"
test_expect_success "$name" \
"git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 &&
+ svn up $SVN_TREE &&
test -x $SVN_TREE/exec.sh"
-name='executable file becomes a symlink to bar/zzz (file)'
-rm exec.sh
-ln -s bar/zzz exec.sh
-git update-index exec.sh
-git commit -m "$name"
-
-test_expect_success "$name" \
- "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 &&
- test -L $SVN_TREE/exec.sh"
-
-
-
-name='new symlink is added to a file that was also just made executable'
-chmod +x bar/zzz
-ln -s bar/zzz exec-2.sh
-git update-index --add bar/zzz exec-2.sh
-git commit -m "$name"
-
-test_expect_success "$name" \
- "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 &&
- test -x $SVN_TREE/bar/zzz &&
- test -L $SVN_TREE/exec-2.sh"
-
-
-
-name='modify a symlink to become a file'
-git help > help || true
-rm exec-2.sh
-cp help exec-2.sh
-git update-index exec-2.sh
-git commit -m "$name"
-
-test_expect_success "$name" \
- "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 &&
- test -f $SVN_TREE/exec-2.sh &&
- test ! -L $SVN_TREE/exec-2.sh &&
- diff -u help $SVN_TREE/exec-2.sh"
-
-
+if test -z "$NO_SYMLINK"
+then
+ name='executable file becomes a symlink to bar/zzz (file)'
+ rm exec.sh
+ ln -s bar/zzz exec.sh
+ git update-index exec.sh
+ git commit -m "$name"
+
+ test_expect_success "$name" \
+ "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 &&
+ svn up $SVN_TREE &&
+ test -L $SVN_TREE/exec.sh"
+
+ name='new symlink is added to a file that was also just made executable'
+ chmod +x bar/zzz
+ ln -s bar/zzz exec-2.sh
+ git update-index --add bar/zzz exec-2.sh
+ git commit -m "$name"
+
+ test_expect_success "$name" \
+ "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 &&
+ svn up $SVN_TREE &&
+ test -x $SVN_TREE/bar/zzz &&
+ test -L $SVN_TREE/exec-2.sh"
+
+ name='modify a symlink to become a file'
+ git help > help || true
+ rm exec-2.sh
+ cp help exec-2.sh
+ git update-index exec-2.sh
+ git commit -m "$name"
+
+ test_expect_success "$name" \
+ "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 &&
+ svn up $SVN_TREE &&
+ test -f $SVN_TREE/exec-2.sh &&
+ test ! -L $SVN_TREE/exec-2.sh &&
+ diff -u help $SVN_TREE/exec-2.sh"
+fi
+
+
+if test -n "$GIT_SVN_LC_ALL" && echo $GIT_SVN_LC_ALL | grep -q '\.UTF-8$'
+then
+ name="commit with UTF-8 message: locale: $GIT_SVN_LC_ALL"
+ echo '# hello' >> exec-2.sh
+ git update-index exec-2.sh
+ git commit -m 'éï∏'
+ export LC_ALL="$GIT_SVN_LC_ALL"
+ test_expect_success "$name" "git-svn commit HEAD"
+ unset LC_ALL
+else
+ echo "UTF-8 locale not set, test skipped ($GIT_SVN_LC_ALL)"
+fi
name='test fetch functionality (svn => git) with alternate GIT_SVN_ID'
GIT_SVN_ID=alt
git-rev-list --pretty=raw remotes/alt | grep ^tree | uniq > b &&
diff -u a b"
+if test -n "$NO_SYMLINK"
+then
+ test_done
+ exit 0
+fi
+
+name='check imported tree checksums expected tree checksums'
+rm -f expected
+if test -n "$GIT_SVN_LC_ALL" && echo $GIT_SVN_LC_ALL | grep -q '\.UTF-8$'
+then
+ echo tree f735671b89a7eb30cab1d8597de35bd4271ab813 > expected
+fi
+cat >> expected <<\EOF
+tree 4b9af72bb861eaed053854ec502cf7df72618f0f
+tree 031b8d557afc6fea52894eaebb45bec52f1ba6d1
+tree 0b094cbff17168f24c302e297f55bfac65eb8bd3
+tree d667270a1f7b109f5eb3aaea21ede14b56bfdd6e
+tree 56a30b966619b863674f5978696f4a3594f2fca9
+tree d667270a1f7b109f5eb3aaea21ede14b56bfdd6e
+tree 8f51f74cf0163afc9ad68a4b1537288c4558b5a4
+EOF
+test_expect_success "$name" "diff -u a expected"
+
test_done
a_empty_crlf=
cd import
- cat >> kw.c <<''
+ cat >> kw.c <<\EOF
/* Make it look like somebody copied a file from CVS into SVN: */
/* $Id: kw.c,v 1.1.1.1 1994/03/06 00:00:00 eric Exp $ */
+EOF
printf "Hello\r\nWorld\r\n" > crlf
a_crlf=`git-hash-object -w crlf`
cd ..
rm -rf import
-svn co "$svnrepo" test_wc
+test_expect_success 'checkout working copy from svn' "svn co $svnrepo test_wc"
+test_expect_success 'setup some commits to svn' \
+ 'cd test_wc &&
+ echo Greetings >> kw.c &&
+ svn commit -m "Not yet an Id" &&
+ svn up &&
+ echo Hello world >> kw.c &&
+ svn commit -m "Modified file, but still not yet an Id" &&
+ svn up &&
+ svn propset svn:keywords Id kw.c &&
+ svn commit -m "Propset Id" &&
+ svn up &&
+ cd ..'
+
+test_expect_success 'initialize git-svn' "git-svn init $svnrepo"
+test_expect_success 'fetch revisions from svn' 'git-svn fetch'
-cd test_wc
- echo 'Greetings' >> kw.c
- svn commit -m 'Not yet an $Id$'
- svn up
-
- echo 'Hello world' >> kw.c
- svn commit -m 'Modified file, but still not yet an $Id$'
- svn up
-
- svn propset svn:keywords Id kw.c
- svn commit -m 'Propset $Id$'
- svn up
-cd ..
-
-git-svn init "$svnrepo"
-git-svn fetch
-
-git checkout -b mybranch remotes/git-svn
-echo 'Hi again' >> kw.c
name='test svn:keywords ignoring'
-
-git commit -a -m "$name"
-git-svn commit remotes/git-svn..mybranch
-git pull . remotes/git-svn
+test_expect_success "$name" \
+ 'git checkout -b mybranch remotes/git-svn &&
+ echo Hi again >> kw.c &&
+ git commit -a -m "test keywoards ignoring" &&
+ git-svn commit remotes/git-svn..mybranch &&
+ git pull . remotes/git-svn'
expect='/* $Id$ */'
got="`sed -ne 2p kw.c`"
test_expect_success 'raw $Id$ found in kw.c' "test '$expect' = '$got'"
-cd test_wc
- svn propset svn:eol-style CR empty
- svn propset svn:eol-style CR crlf
- svn propset svn:eol-style CR ne_crlf
- svn commit -m 'propset CR on crlf files'
- svn up
-cd ..
+test_expect_success "propset CR on crlf files" \
+ 'cd test_wc &&
+ svn propset svn:eol-style CR empty &&
+ svn propset svn:eol-style CR crlf &&
+ svn propset svn:eol-style CR ne_crlf &&
+ svn commit -m "propset CR on crlf files" &&
+ svn up &&
+ cd ..'
-git-svn fetch
-git pull . remotes/git-svn
+test_expect_success 'fetch and pull latest from svn and checkout a new wc' \
+ "git-svn fetch &&
+ git pull . remotes/git-svn &&
+ svn co $svnrepo new_wc"
-svn co "$svnrepo" new_wc
for i in crlf ne_crlf lf ne_lf cr ne_cr empty_cr empty_lf empty empty_crlf
do
test_expect_success "Comparing $i" "cmp $i new_wc/$i"
printf '$Id$\rHello\rWorld' > ne_cr
a_cr=`printf '$Id$\r\nHello\r\nWorld\r\n' | git-hash-object --stdin`
a_ne_cr=`printf '$Id$\r\nHello\r\nWorld' | git-hash-object --stdin`
- svn propset svn:eol-style CRLF cr
- svn propset svn:eol-style CRLF ne_cr
- svn propset svn:keywords Id cr
- svn propset svn:keywords Id ne_cr
- svn commit -m 'propset CRLF on cr files'
- svn up
+ test_expect_success 'Set CRLF on cr files' \
+ 'svn propset svn:eol-style CRLF cr &&
+ svn propset svn:eol-style CRLF ne_cr &&
+ svn propset svn:keywords Id cr &&
+ svn propset svn:keywords Id ne_cr &&
+ svn commit -m "propset CRLF on cr files" &&
+ svn up'
cd ..
-
-git-svn fetch
-git pull . remotes/git-svn
+test_expect_success 'fetch and pull latest from svn' \
+ 'git-svn fetch && git pull . remotes/git-svn'
b_cr="`git-hash-object cr`"
b_ne_cr="`git-hash-object ne_cr`"
class GitView:
""" This is the main class
"""
- version = "0.7"
+ version = "0.8"
def __init__(self, with_diff=0):
self.with_diff = with_diff
self.accel_group = gtk.AccelGroup()
self.window.add_accel_group(self.accel_group)
+ self.accel_group.connect_group(0xffc2, 0, gtk.ACCEL_LOCKED, self.refresh);
+ self.accel_group.connect_group(0xffc1, 0, gtk.ACCEL_LOCKED, self.maximize);
+ self.accel_group.connect_group(0xffc8, 0, gtk.ACCEL_LOCKED, self.fullscreen);
+ self.accel_group.connect_group(0xffc9, 0, gtk.ACCEL_LOCKED, self.unfullscreen);
- self.construct()
+ self.window.add(self.construct())
+
+ def refresh(self, widget, event=None, *arguments, **keywords):
+ self.get_encoding()
+ self.get_bt_sha1()
+ Commit.children_sha1 = {}
+ self.set_branch(sys.argv[without_diff:])
+ self.window.show()
+ return True
+
+ def maximize(self, widget, event=None, *arguments, **keywords):
+ self.window.maximize()
+ return True
+
+ def fullscreen(self, widget, event=None, *arguments, **keywords):
+ self.window.fullscreen()
+ return True
+
+ def unfullscreen(self, widget, event=None, *arguments, **keywords):
+ self.window.unfullscreen()
+ return True
def get_bt_sha1(self):
""" Update the bt_sha1 dictionary with the
menu_bar.show()
vbox.pack_start(menu_bar, expand=False, fill=True)
vbox.pack_start(paned, expand=True, fill=True)
- self.window.add(vbox)
paned.show()
vbox.show()
+ return vbox
def construct_top(self):
try:
self.treeview.set_cursor(self.index[revid])
except KeyError:
- print "Revision %s not present in the list" % revid
+ dialog = gtk.MessageDialog(parent=None, flags=0,
+ type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_CLOSE,
+ message_format=None)
+ dialog.set_markup("Revision <b>%s</b> not present in the list" % revid)
# revid == 0 is the parent of the first commit
if (revid != 0 ):
- print "Try running gitview without any options"
+ dialog.format_secondary_text("Try running gitview without any options")
+ dialog.run()
+ dialog.destroy()
self.treeview.grab_focus()
window.set_diff(commit_sha1, parent_sha1, encoding)
self.treeview.grab_focus()
+without_diff = 0
if __name__ == "__main__":
- without_diff = 0
if (len(sys.argv) > 1 ):
if (sys.argv[1] == "--without-diff"):
<args>
All the valid option for git-rev-list(1)
+ Key Bindings:
+ F4:
+ To maximize the window
+ F5:
+ To reread references.
+ F11:
+ Full screen
+ F12:
+ Leave full screen
EXAMPLES
------
or drivers/scsi subdirectories
gitview --since=2.weeks.ago
- Show the changes during the last two weeks
+ Show the changes during the last two weeks
-
static int execute(void)
{
static char line[1000];
- int len;
+ int pktlen, len;
alarm(init_timeout ? init_timeout : timeout);
- len = packet_read_line(0, line, sizeof(line));
+ pktlen = packet_read_line(0, line, sizeof(line));
alarm(0);
+ len = strlen(line);
+ if (pktlen != len)
+ loginfo("Extended attributes (%d bytes) exist <%.*s>",
+ (int) pktlen - len,
+ (int) pktlen - len, line + len + 1);
if (len && line[len-1] == '\n')
line[--len] = 0;
/* Four-digit year or a timezone? */
if (n == 4) {
- if (num <= 1200 && *offset == -1) {
+ if (num <= 1400 && *offset == -1) {
unsigned int minutes = num % 100;
unsigned int hours = num / 100;
*offset = hours*60 + minutes;
--- /dev/null
+#include "cache.h"
+#include "tree.h"
+#include "cache-tree.h"
+
+
+static void dump_one(struct cache_tree *it, const char *pfx, const char *x)
+{
+ if (it->entry_count < 0)
+ printf("%-40s %s%s (%d subtrees)\n",
+ "invalid", x, pfx, it->subtree_nr);
+ else
+ printf("%s %s%s (%d entries, %d subtrees)\n",
+ sha1_to_hex(it->sha1), x, pfx,
+ it->entry_count, it->subtree_nr);
+}
+
+static int dump_cache_tree(struct cache_tree *it,
+ struct cache_tree *ref,
+ const char *pfx)
+{
+ int i;
+ int errs = 0;
+
+ if (!it || !ref)
+ /* missing in either */
+ return 0;
+
+ if (it->entry_count < 0) {
+ dump_one(it, pfx, "");
+ dump_one(ref, pfx, "#(ref) ");
+ if (it->subtree_nr != ref->subtree_nr)
+ errs = 1;
+ }
+ else {
+ dump_one(it, pfx, "");
+ if (memcmp(it->sha1, ref->sha1, 20) ||
+ ref->entry_count != it->entry_count ||
+ ref->subtree_nr != it->subtree_nr) {
+ dump_one(ref, pfx, "#(ref) ");
+ errs = 1;
+ }
+ }
+
+ for (i = 0; i < it->subtree_nr; i++) {
+ char path[PATH_MAX];
+ struct cache_tree_sub *down = it->down[i];
+ struct cache_tree_sub *rdwn;
+
+ rdwn = cache_tree_sub(ref, down->name);
+ sprintf(path, "%s%.*s/", pfx, down->namelen, down->name);
+ if (dump_cache_tree(down->cache_tree, rdwn->cache_tree, path))
+ errs = 1;
+ }
+ return errs;
+}
+
+int main(int ac, char **av)
+{
+ struct cache_tree *another = cache_tree();
+ if (read_cache() < 0)
+ die("unable to read index file");
+ cache_tree_update(another, active_cache, active_nr, 0, 1);
+ return dump_cache_tree(active_cache_tree, another, "");
+}
int trust_executable_bit = 1;
int assume_unchanged = 0;
int prefer_symlink_refs = 0;
+int log_all_ref_updates = 0;
int warn_ambiguous_refs = 1;
int repository_format_version = 0;
char git_commit_encoding[MAX_ENCODING_LENGTH] = "utf-8";
return current_exec_path;
env = getenv("GIT_EXEC_PATH");
- if (env) {
+ if (env && *env) {
return env;
}
int execv_git_cmd(const char **argv)
{
char git_command[PATH_MAX + 1];
- int len, i;
+ int i;
const char *paths[] = { current_exec_path,
getenv("GIT_EXEC_PATH"),
builtin_exec_path };
for (i = 0; i < ARRAY_SIZE(paths); ++i) {
+ size_t len;
+ int rc;
const char *exec_dir = paths[i];
const char *tmp;
- if (!exec_dir) continue;
+ if (!exec_dir || !*exec_dir) continue;
if (*exec_dir != '/') {
if (!getcwd(git_command, sizeof(git_command))) {
fprintf(stderr, "git: cannot determine "
- "current directory\n");
- exit(1);
+ "current directory: %s\n",
+ strerror(errno));
+ break;
}
len = strlen(git_command);
while (*exec_dir == '/')
exec_dir++;
}
- snprintf(git_command + len, sizeof(git_command) - len,
- "/%s", exec_dir);
+
+ rc = snprintf(git_command + len,
+ sizeof(git_command) - len, "/%s",
+ exec_dir);
+ if (rc < 0 || rc >= sizeof(git_command) - len) {
+ fprintf(stderr, "git: command name given "
+ "is too long.\n");
+ break;
+ }
} else {
+ if (strlen(exec_dir) + 1 > sizeof(git_command)) {
+ fprintf(stderr, "git: command name given "
+ "is too long.\n");
+ break;
+ }
strcpy(git_command, exec_dir);
}
len = strlen(git_command);
- len += snprintf(git_command + len, sizeof(git_command) - len,
- "/git-%s", argv[0]);
-
- if (sizeof(git_command) <= len) {
+ rc = snprintf(git_command + len, sizeof(git_command) - len,
+ "/git-%s", argv[0]);
+ if (rc < 0 || rc >= sizeof(git_command) - len) {
fprintf(stderr,
"git: command name given is too long.\n");
break;
#include "cache.h"
#include "commit.h"
#include "tree.h"
+#include "tree-walk.h"
#include "tag.h"
#include "blob.h"
#include "refs.h"
const char *write_ref = NULL;
+const char *write_ref_log_details = NULL;
int get_tree = 0;
int get_history = 0;
static int process_tree(struct tree *tree)
{
- struct tree_entry_list *entry;
+ struct tree_desc desc;
+ struct name_entry entry;
if (parse_tree(tree))
return -1;
- entry = tree->entries;
- tree->entries = NULL;
- while (entry) {
- struct tree_entry_list *next = entry->next;
- if (process(entry->item.any))
+ desc.buf = tree->buffer;
+ desc.size = tree->size;
+ while (tree_entry(&desc, &entry)) {
+ struct object *obj = NULL;
+
+ if (S_ISDIR(entry.mode)) {
+ struct tree *tree = lookup_tree(entry.sha1);
+ if (tree)
+ obj = &tree->object;
+ }
+ else {
+ struct blob *blob = lookup_blob(entry.sha1);
+ if (blob)
+ obj = &blob->object;
+ }
+ if (!obj || process(obj))
return -1;
- free(entry->name);
- free(entry);
- entry = next;
}
+ free(tree->buffer);
+ tree->buffer = NULL;
+ tree->size = 0;
return 0;
}
if (has_sha1_file(obj->sha1)) {
/* We already have it, so we should scan it now. */
obj->flags |= TO_SCAN;
- } else {
+ }
+ else {
if (obj->flags & COMPLETE)
return 0;
prefetch(obj->sha1);
int pull(char *target)
{
+ struct ref_lock *lock = NULL;
unsigned char sha1[20];
+ char *msg;
+ int ret;
save_commit_buffer = 0;
track_object_refs = 0;
+ if (write_ref) {
+ lock = lock_ref_sha1(write_ref, NULL, 0);
+ if (!lock) {
+ error("Can't lock ref %s", write_ref);
+ return -1;
+ }
+ }
if (!get_recover)
for_each_ref(mark_complete);
- if (interpret_target(target, sha1))
- return error("Could not interpret %s as something to pull",
- target);
- if (process(lookup_unknown_object(sha1)))
+ if (interpret_target(target, sha1)) {
+ error("Could not interpret %s as something to pull", target);
+ if (lock)
+ unlock_ref(lock);
return -1;
- if (loop())
+ }
+ if (process(lookup_unknown_object(sha1))) {
+ if (lock)
+ unlock_ref(lock);
+ return -1;
+ }
+ if (loop()) {
+ if (lock)
+ unlock_ref(lock);
return -1;
-
- if (write_ref)
- write_ref_sha1_unlocked(write_ref, sha1);
+ }
+
+ if (write_ref) {
+ if (write_ref_log_details) {
+ msg = xmalloc(strlen(write_ref_log_details) + 12);
+ sprintf(msg, "fetch from %s", write_ref_log_details);
+ }
+ else
+ msg = NULL;
+ ret = write_ref_sha1(lock, sha1, msg ? msg : "fetch (unknown)");
+ if (msg)
+ free(msg);
+ return ret;
+ }
return 0;
}
/* If set, the ref filename to write the target value to. */
extern const char *write_ref;
+/* If set additional text will appear in the ref log. */
+extern const char *write_ref_log_details;
+
/* Set to fetch the target tree. */
extern int get_tree;
#include "tag.h"
#include "refs.h"
#include "pack.h"
+#include "cache-tree.h"
+#include "tree-walk.h"
#define REACHABLE 0x0001
+#define SEEN 0x0002
static int show_root = 0;
static int show_tags = 0;
#define TREE_UNORDERED (-1)
#define TREE_HAS_DUPS (-2)
-static int verify_ordered(struct tree_entry_list *a, struct tree_entry_list *b)
+static int verify_ordered(unsigned mode1, const char *name1, unsigned mode2, const char *name2)
{
- int len1 = strlen(a->name);
- int len2 = strlen(b->name);
+ int len1 = strlen(name1);
+ int len2 = strlen(name2);
int len = len1 < len2 ? len1 : len2;
unsigned char c1, c2;
int cmp;
- cmp = memcmp(a->name, b->name, len);
+ cmp = memcmp(name1, name2, len);
if (cmp < 0)
return 0;
if (cmp > 0)
* Now we need to order the next one, but turn
* a '\0' into a '/' for a directory entry.
*/
- c1 = a->name[len];
- c2 = b->name[len];
+ c1 = name1[len];
+ c2 = name2[len];
if (!c1 && !c2)
/*
* git-write-tree used to write out a nonsense tree that has
* sure we do not have duplicate entries.
*/
return TREE_HAS_DUPS;
- if (!c1 && a->directory)
+ if (!c1 && S_ISDIR(mode1))
c1 = '/';
- if (!c2 && b->directory)
+ if (!c2 && S_ISDIR(mode2))
c2 = '/';
return c1 < c2 ? 0 : TREE_UNORDERED;
}
int has_bad_modes = 0;
int has_dup_entries = 0;
int not_properly_sorted = 0;
- struct tree_entry_list *entry, *last;
+ struct tree_desc desc;
+ unsigned o_mode;
+ const char *o_name;
+ const unsigned char *o_sha1;
- last = NULL;
- for (entry = item->entries; entry; entry = entry->next) {
- if (strchr(entry->name, '/'))
+ desc.buf = item->buffer;
+ desc.size = item->size;
+
+ o_mode = 0;
+ o_name = NULL;
+ o_sha1 = NULL;
+ while (desc.size) {
+ unsigned mode;
+ const char *name;
+ const unsigned char *sha1;
+
+ sha1 = tree_entry_extract(&desc, &name, &mode);
+
+ if (strchr(name, '/'))
has_full_path = 1;
- has_zero_pad |= entry->zeropad;
+ has_zero_pad |= *(char *)desc.buf == '0';
+ update_tree_entry(&desc);
- switch (entry->mode) {
+ switch (mode) {
/*
- * Standard modes..
+ * Standard modes..
*/
case S_IFREG | 0755:
case S_IFREG | 0644:
has_bad_modes = 1;
}
- if (last) {
- switch (verify_ordered(last, entry)) {
+ if (o_name) {
+ switch (verify_ordered(o_mode, o_name, mode, name)) {
case TREE_UNORDERED:
not_properly_sorted = 1;
break;
default:
break;
}
- free(last->name);
- free(last);
}
- last = entry;
+ o_mode = mode;
+ o_name = name;
+ o_sha1 = sha1;
}
- if (last) {
- free(last->name);
- free(last);
- }
- item->entries = NULL;
+ free(item->buffer);
+ item->buffer = NULL;
retval = 0;
if (has_full_path) {
struct object *obj = parse_object(sha1);
if (!obj)
return error("%s: object not found", sha1_to_hex(sha1));
+ if (obj->flags & SEEN)
+ return 0;
+ obj->flags |= SEEN;
if (obj->type == blob_type)
return 0;
if (obj->type == tree_type)
return 0;
}
+static int fsck_cache_tree(struct cache_tree *it)
+{
+ int i;
+ int err = 0;
+
+ if (0 <= it->entry_count) {
+ struct object *obj = parse_object(it->sha1);
+ if (!obj) {
+ error("%s: invalid sha1 pointer in cache-tree",
+ sha1_to_hex(it->sha1));
+ return 1;
+ }
+ mark_reachable(obj, REACHABLE);
+ obj->used = 1;
+ if (obj->type != tree_type)
+ err |= objerror(obj, "non-tree in cache-tree");
+ }
+ for (i = 0; i < it->subtree_nr; i++)
+ err |= fsck_cache_tree(it->down[i]->cache_tree);
+ return err;
+}
+
int main(int argc, char **argv)
{
int i, heads;
+ track_object_refs = 1;
setup_git_directory();
for (i = 1; i < argc; i++) {
obj->used = 1;
mark_reachable(obj, REACHABLE);
}
+ if (active_cache_tree)
+ fsck_cache_tree(active_cache_tree);
}
check_connectivity();
+++ /dev/null
-/*
- * Copyright (C) 2005 Rene Scharfe
- */
-#include <stdio.h>
-#include <string.h>
-#include <unistd.h>
-
-#define HEADERSIZE 1024
-
-int main(int argc, char **argv)
-{
- char buffer[HEADERSIZE];
- ssize_t n;
-
- n = read(0, buffer, HEADERSIZE);
- if (n < HEADERSIZE) {
- fprintf(stderr, "read error\n");
- return 3;
- }
- if (buffer[156] != 'g')
- return 1;
- if (memcmp(&buffer[512], "52 comment=", 11))
- return 1;
- n = write(1, &buffer[523], 41);
- if (n < 41) {
- fprintf(stderr, "write error\n");
- return 2;
- }
- return 0;
-}
parent=$(git-rev-parse --verify HEAD) &&
commit=$(git-commit-tree $tree -p $parent <"$dotest/final-commit") &&
echo Committed: $commit &&
- git-update-ref HEAD $commit $parent ||
+ git-update-ref -m "am: $SUBJECT" HEAD $commit $parent ||
stop_here $this
if test -x "$GIT_DIR"/hooks/post-applypatch
parent=$(git-rev-parse --verify HEAD) &&
commit=$(git-commit-tree $tree -p $parent <"$final") || exit 1
echo Committed: $commit
-git-update-ref HEAD $commit $parent || exit
+git-update-ref -m "applypatch: $SUBJECT" HEAD $commit $parent || exit
if test -x "$GIT_DIR"/hooks/post-applypatch
then
#!/bin/sh
-USAGE='[(-d | -D) <branchname>] | [[-f] <branchname> [<start-point>]] | -r'
+USAGE='[-l] [(-d | -D) <branchname>] | [[-f] <branchname> [<start-point>]] | -r'
LONG_USAGE='If no arguments, show available branches and mark current branch with a star.
If one argument, create a new branch <branchname> based off of current HEAD.
If two arguments, create a new branch <branchname> based off of <start-point>.'
esac
;;
esac
+ rm -f "$GIT_DIR/logs/refs/heads/$branch_name"
rm -f "$GIT_DIR/refs/heads/$branch_name"
echo "Deleted branch $branch_name."
done
}
force=
+create_log=
while case "$#,$1" in 0,*) break ;; *,-*) ;; *) break ;; esac
do
case "$1" in
-f)
force="$1"
;;
+ -l)
+ create_log="yes"
+ ;;
--)
shift
break
die "cannot force-update the current branch."
fi
fi
-git update-ref "refs/heads/$branchname" $rev
+if test "$create_log" = 'yes'
+then
+ mkdir -p $(dirname "$GIT_DIR/logs/refs/heads/$branchname")
+ touch "$GIT_DIR/logs/refs/heads/$branchname"
+fi
+git update-ref -m "branch: Created from $head" "refs/heads/$branchname" $rev
. git-sh-setup
old=$(git-rev-parse HEAD)
+old_name=HEAD
new=
+new_name=
force=
branch=
newbranch=
+newbranch_log=
merge=
while [ "$#" != "0" ]; do
arg="$1"
git-check-ref-format "heads/$newbranch" ||
die "git checkout: we do not like '$newbranch' as a branch name."
;;
+ "-l")
+ newbranch_log=1
+ ;;
"-f")
force=1
;;
exit 1
fi
new="$rev"
+ new_name="$arg^0"
if [ -f "$GIT_DIR/refs/heads/$arg" ]; then
branch="$arg"
fi
then
# checking out selected paths from a tree-ish.
new="$rev"
+ new_name="$arg^{tree}"
branch=
else
new=
+ new_name=
branch=
set x "$arg" "$@"
shift
cd "$cdup"
fi
-[ -z "$new" ] && new=$old
+[ -z "$new" ] && new=$old && new_name="$old_name"
# If we don't have an old branch that we're switching to,
# and we don't have a new branch name for the target we
#
if [ "$?" -eq 0 ]; then
if [ "$newbranch" ]; then
- leading=`expr "refs/heads/$newbranch" : '\(.*\)/'` &&
- mkdir -p "$GIT_DIR/$leading" &&
- echo $new >"$GIT_DIR/refs/heads/$newbranch" || exit
+ if [ "$newbranch_log" ]; then
+ mkdir -p $(dirname "$GIT_DIR/logs/refs/heads/$newbranch")
+ touch "$GIT_DIR/logs/refs/heads/$newbranch"
+ fi
+ git-update-ref -m "checkout: Created from $new_name" "refs/heads/$newbranch" $new || exit
branch="$newbranch"
fi
[ "$branch" ] &&
ignoredonly=
cleandir=
quiet=
-rmf="rm -f"
-rmrf="rm -rf"
+rmf="rm -f --"
+rmrf="rm -rf --"
rm_refuse="echo Not removing"
echo1="echo"
unset CDPATH
usage() {
- echo >&2 "Usage: $0 [--use-separate-remote] [--reference <reference-repo>] [--bare] [-l [-s]] [-q] [-u <upload-pack>] [--origin <name>] [-n] <repo> [<dir>]"
+ echo >&2 "Usage: $0 [--template=<template_directory>] [--use-separate-remote] [--reference <reference-repo>] [--bare] [-l [-s]] [-q] [-u <upload-pack>] [--origin <name>] [-n] <repo> [<dir>]"
exit 1
}
clone_dumb_http () {
# $1 - remote, $2 - local
cd "$2" &&
- clone_tmp='.git/clone-tmp' &&
+ clone_tmp="$GIT_DIR/clone-tmp" &&
mkdir -p "$clone_tmp" || exit 1
http_fetch "$1/info/refs" "$clone_tmp/refs" || {
echo >&2 "Cannot get remote repository information.
local=no
use_local=no
local_shared=no
+unset template
no_checkout=
upload_pack=
bare=
*,-l|*,--l|*,--lo|*,--loc|*,--loca|*,--local) use_local=yes ;;
*,-s|*,--s|*,--sh|*,--sha|*,--shar|*,--share|*,--shared)
local_shared=yes; use_local=yes ;;
+ 1,--template) usage ;;
+ *,--template)
+ shift; template="--template=$1" ;;
+ *,--template=*)
+ template="$1" ;;
*,-q|*,--quiet) quiet=-q ;;
*,--use-separate-remote)
use_separate_remote=t ;;
D=$(cd "$dir" && pwd) &&
trap 'err=$?; cd ..; rm -r "$D"; exit $err' 0
case "$bare" in
-yes) GIT_DIR="$D" ;;
-*) GIT_DIR="$D/.git" ;;
-esac && export GIT_DIR && git-init-db || usage
-case "$bare" in
yes)
GIT_DIR="$D" ;;
*)
GIT_DIR="$D/.git" ;;
-esac
+esac && export GIT_DIR && git-init-db ${template+"$template"} || usage
if test -n "$reference"
then
(cd "$GIT_DIR/$remote_top" && find . -type f -print) |
while read dotslref
do
- name=`expr "$dotslref" : './\(.*\)'` &&
- test "$use_separate_remote" = '' && {
- test "$head_points_at" = "$name" ||
- test "$origin" = "$name"
- } ||
+ name=`expr "$dotslref" : './\(.*\)'`
+ if test "z$head_points_at" = "z$name"
+ then
+ continue
+ fi
+ if test "$use_separate_remote" = '' &&
+ test "z$origin" = "z$name"
+ then
+ continue
+ fi
echo "Pull: refs/heads/${name}:$remote_top/${name}"
done >>"$GIT_DIR/remotes/$origin" &&
case "$use_separate_remote" in
-m|--m|--me|--mes|--mess|--messa|--messag|--message)
case "$#" in 1) usage ;; esac
shift
- log_given=t$log_given
- log_message="$1"
+ log_given=m$log_given
+ if test "$log_message" = ''
+ then
+ log_message="$1"
+ else
+ log_message="$log_message
+
+$1"
+ fi
no_edit=t
shift
;;
-m*)
- log_given=t$log_given
- log_message=`expr "$1" : '-m\(.*\)'`
+ log_given=m$log_given
+ if test "$log_message" = ''
+ then
+ log_message=`expr "$1" : '-m\(.*\)'`
+ else
+ log_message="$log_message
+
+`expr "$1" : '-m\(.*\)'`"
+ fi
no_edit=t
shift
;;
--m=*|--me=*|--mes=*|--mess=*|--messa=*|--messag=*|--message=*)
- log_given=t$log_given
- log_message=`expr "$1" : '-[^=]*=\(.*\)'`
+ log_given=m$log_given
+ if test "$log_message" = ''
+ then
+ log_message=`expr "$1" : '-[^=]*=\(.*\)'`
+ else
+ log_message="$log_message
+
+`expr "$1" : '-[^=]*=\(.*\)'`"
+ fi
no_edit=t
shift
;;
case "$log_given" in
tt*)
- die "Only one of -c/-C/-F/-m can be used." ;;
+ die "Only one of -c/-C/-F can be used." ;;
+*tm*|*mt*)
+ die "Option -m cannot be combined with -c/-C/-F." ;;
esac
case "$#,$also,$only,$amend" in
if test -z "$no_edit"
then
{
+ echo ""
+ echo "# Please enter the commit message for your changes."
+ echo "# (Comment lines starting with '#' will not be included)"
test -z "$only_include_assumed" || echo "$only_include_assumed"
run_status
} >>"$GIT_DIR"/COMMIT_EDITMSG
rm -f "$TMP_INDEX"
fi &&
commit=$(cat "$GIT_DIR"/COMMIT_MSG | git-commit-tree $tree $PARENTS) &&
- git-update-ref HEAD $commit $current &&
+ rlogm=$(sed -e 1q "$GIT_DIR"/COMMIT_MSG) &&
+ git-update-ref -m "commit: $rlogm" HEAD $commit $current &&
rm -f -- "$GIT_DIR/MERGE_HEAD" &&
if test -f "$NEXT_INDEX"
then
#!/usr/bin/perl -w
+# Known limitations:
+# - cannot add or remove binary files
+# - does not propagate permissions
+# - tells "ready for commit" even when things could not be completed
+# (eg addition of a binary file)
+
use strict;
use Getopt::Std;
use File::Temp qw(tempdir);
use Data::Dumper;
-use File::Basename qw(basename);
+use File::Basename qw(basename dirname);
unless ($ENV{GIT_DIR} && -r $ENV{GIT_DIR}){
die "GIT_DIR is not defined or is unreadable";
`git-cat-file commit $commit | sed -e '1,/^\$/d' >> .msg`;
$? && die "Error extracting the commit message";
-my (@afiles, @dfiles, @mfiles);
+my (@afiles, @dfiles, @mfiles, @dirs);
my @files = safe_pipe_capture('git-diff-tree', '-r', $parent, $commit);
#print @files;
$? && die "Error in git-diff-tree";
chomp $f;
my @fields = split(m!\s+!, $f);
if ($fields[4] eq 'A') {
- push @afiles, $fields[5];
+ my $path = $fields[5];
+ push @afiles, $path;
+ # add any needed parent directories
+ $path = dirname $path;
+ while (!-d $path and ! grep { $_ eq $path } @dirs) {
+ unshift @dirs, $path;
+ $path = dirname $path;
+ }
}
if ($fields[4] eq 'M') {
push @mfiles, $fields[5];
# check that the files are clean and up to date according to cvs
my $dirty;
+foreach my $d (@dirs) {
+ if (-e $d) {
+ $dirty = 1;
+ warn "$d exists and is not a directory!\n";
+ }
+}
foreach my $f (@afiles) {
# This should return only one value
my @status = grep(m/^File/, safe_pipe_capture('cvs', '-q', 'status' ,$f));
if (@status > 1) { warn 'Strange! cvs status returned more than one line?'};
- unless ($status[0] =~ m/Status: Unknown$/) {
+ if (-d dirname $f and $status[0] !~ m/Status: Unknown$/
+ and $status[0] !~ m/^File: no file /) {
$dirty = 1;
warn "File $f is already known in your CVS checkout -- perhaps it has been added by another user. Or this may indicate that it exists on a different branch. If this is the case, use -f to force the merge.\n";
+ warn "Status was: $status[0]\n";
}
}
foreach my $f (@mfiles, @dfiles) {
###
+print "Creating new directories\n";
+foreach my $d (@dirs) {
+ unless (mkdir $d) {
+ warn "Could not mkdir $d: $!";
+ $dirty = 1;
+ }
+ `cvs add $d`;
+ if ($?) {
+ $dirty = 1;
+ warn "Failed to cvs add directory $d -- you may need to do it manually";
+ }
+}
+
print "'Patching' binary files\n";
my @bfiles = grep(m/^Binary/, safe_pipe_capture('git-diff-tree', '-p', $parent, $commit));
my $blob = `git-ls-tree $tree "$f" | cut -f 1 | cut -d ' ' -f 3`;
chomp $blob;
`git-cat-file blob $blob > $tmpdir/blob`;
- if (system('cmp', '-q', $f, "$tmpdir/blob")) {
+ if (system('cmp', '-s', $f, "$tmpdir/blob")) {
warn "Binary file $f in CVS does not match parent.\n";
$dirty = 1;
next;
use Time::Local;
use IO::Socket;
use IO::Pipe;
-use POSIX qw(strftime dup2 :errno_h);
+use POSIX qw(strftime dup2 ENOENT);
use IPC::Open2;
$SIG{'PIPE'}="IGNORE";
$ENV{"GIT_DIR"} = $git_dir;
my $orig_git_index;
$orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE};
-my ($git_ih, $git_index) = tempfile('gitXXXXXX', SUFFIX => '.idx',
- DIR => File::Spec->tmpdir());
-close ($git_ih);
-$ENV{GIT_INDEX_FILE} = $git_index;
+
+my %index; # holds filenames of one index per branch
+{ # init with an index for origin
+ my ($fh, $fn) = tempfile('gitXXXXXX', SUFFIX => '.idx',
+ DIR => File::Spec->tmpdir());
+ close ($fh);
+ $index{$opt_o} = $fn;
+}
+$ENV{GIT_INDEX_FILE} = $index{$opt_o};
unless(-d $git_dir) {
system("git-init-db");
die "Cannot init the GIT db at $git_tree: $?\n" if $?;
$tip_at_start = `git-rev-parse --verify HEAD`;
# populate index
+ unless ($index{$last_branch}) {
+ my ($fh, $fn) = tempfile('gitXXXXXX', SUFFIX => '.idx',
+ DIR => File::Spec->tmpdir());
+ close ($fh);
+ $index{$last_branch} = $fn;
+ }
+ $ENV{GIT_INDEX_FILE} = $index{$last_branch};
system('git-read-tree', $last_branch);
die "read-tree failed: $?\n" if $?;
write_author_info("$git_dir/cvs-authors");
}
-my $pid = open(CVS,"-|");
-die "Cannot fork: $!\n" unless defined $pid;
-unless($pid) {
- my @opt;
- @opt = split(/,/,$opt_p) if defined $opt_p;
- unshift @opt, '-z', $opt_z if defined $opt_z;
- unshift @opt, '-q' unless defined $opt_v;
- unless (defined($opt_p) && $opt_p =~ m/--no-cvs-direct/) {
- push @opt, '--cvs-direct';
+
+#
+# run cvsps into a file unless we are getting
+# it passed as a file via $opt_P
+#
+unless ($opt_P) {
+ print "Running cvsps...\n" if $opt_v;
+ my $pid = open(CVSPS,"-|");
+ die "Cannot fork: $!\n" unless defined $pid;
+ unless($pid) {
+ my @opt;
+ @opt = split(/,/,$opt_p) if defined $opt_p;
+ unshift @opt, '-z', $opt_z if defined $opt_z;
+ unshift @opt, '-q' unless defined $opt_v;
+ unless (defined($opt_p) && $opt_p =~ m/--no-cvs-direct/) {
+ push @opt, '--cvs-direct';
+ }
+ exec("cvsps","--norc",@opt,"-u","-A",'--root',$opt_d,$cvs_tree);
+ die "Could not start cvsps: $!\n";
}
- if ($opt_P) {
- exec("cat", $opt_P);
- } else {
- exec("cvsps","--norc",@opt,"-u","-A",'--root',$opt_d,$cvs_tree);
- die "Could not start cvsps: $!\n";
+ my ($cvspsfh, $cvspsfile) = tempfile('gitXXXXXX', SUFFIX => '.cvsps',
+ DIR => File::Spec->tmpdir());
+ while (<CVSPS>) {
+ print $cvspsfh $_;
}
+ close CVSPS;
+ close $cvspsfh;
+ $opt_P = $cvspsfile;
}
+open(CVS, "<$opt_P") or die $!;
+
## cvsps output:
#---------------------
#PatchSet 314
}
my($patchset,$date,$author_name,$author_email,$branch,$ancestor,$tag,$logmsg);
-my(@old,@new,@skipped);
+my(@old,@new,@skipped,%ignorebranch);
+
+# commits that cvsps cannot place anywhere...
+$ignorebranch{'#CVSPS_NO_BRANCH'} = 1;
+
sub commit {
update_index(@old, @new);
@old = @new = ();
$state = 11;
next;
}
+ if (exists $ignorebranch{$branch}) {
+ print STDERR "Skipping $branch\n";
+ $state = 11;
+ next;
+ }
if($ancestor) {
+ if($ancestor eq $branch) {
+ print STDERR "Branch $branch erroneously stems from itself -- changed ancestor to $opt_o\n";
+ $ancestor = $opt_o;
+ }
if(-f "$git_dir/refs/heads/$branch") {
print STDERR "Branch $branch already exists!\n";
$state=11;
}
unless(open(H,"$git_dir/refs/heads/$ancestor")) {
print STDERR "Branch $ancestor does not exist!\n";
+ $ignorebranch{$branch} = 1;
$state=11;
next;
}
close(H);
unless(open(H,"> $git_dir/refs/heads/$branch")) {
print STDERR "Could not create branch $branch: $!\n";
+ $ignorebranch{$branch} = 1;
$state=11;
next;
}
}
if(($ancestor || $branch) ne $last_branch) {
print "Switching from $last_branch to $branch\n" if $opt_v;
- system("git-read-tree", $branch);
- die "read-tree failed: $?\n" if $?;
+ unless ($index{$branch}) {
+ my ($fh, $fn) = tempfile('gitXXXXXX', SUFFIX => '.idx',
+ DIR => File::Spec->tmpdir());
+ close ($fh);
+ $index{$branch} = $fn;
+ $ENV{GIT_INDEX_FILE} = $index{$branch};
+ system("git-read-tree", $branch);
+ die "read-tree failed: $?\n" if $?;
+ } else {
+ $ENV{GIT_INDEX_FILE} = $index{$branch};
+ }
}
$last_branch = $branch if $branch ne $last_branch;
$state = 9;
}
commit() if $branch and $state != 11;
-unlink($git_index);
+foreach my $git_index (values %index) {
+ unlink($git_index);
+}
if (defined $orig_git_index) {
$ENV{GIT_INDEX_FILE} = $orig_git_index;
mb=$(git-merge-base "$local" "$2") &&
case "$2,$mb" in
$local,*)
- echo >&2 "* $1: same as $3"
+ if test -n "$verbose"
+ then
+ echo >&2 "* $1: same as $3"
+ fi
;;
*,$local)
echo >&2 "* $1: fast forward to $3"
reflist=$(get_remote_refs_for_fetch "$@")
if test "$tags"
then
- taglist=$(IFS=" " &&
+ taglist=`IFS=" " &&
git-ls-remote $upload_pack --tags "$remote" |
while read sha1 name
do
case "$name" in
- (*^*) continue ;;
+ *^*) continue ;;
esac
if git-check-ref-format "$name"
then
else
echo >&2 "warning: tag ${name} ignored"
fi
- done)
+ done`
if test "$#" -gt 1
then
# remote URL plus explicit refspecs; we need to merge them.
--- /dev/null
+#!/usr/bin/python
+#
+# This tool is copyright (c) 2006, Sean Estabrooks.
+# It is released under the Gnu Public License, version 2.
+#
+# Import Perforce branches into Git repositories.
+# Checking out the files is done by calling the standard p4
+# client which you must have properly configured yourself
+#
+
+import marshal
+import os
+import sys
+import time
+import getopt
+
+from signal import signal, \
+ SIGPIPE, SIGINT, SIG_DFL, \
+ default_int_handler
+
+signal(SIGPIPE, SIG_DFL)
+s = signal(SIGINT, SIG_DFL)
+if s != default_int_handler:
+ signal(SIGINT, s)
+
+def die(msg, *args):
+ for a in args:
+ msg = "%s %s" % (msg, a)
+ print "git-p4import fatal error:", msg
+ sys.exit(1)
+
+def usage():
+ print "USAGE: git-p4import [-q|-v] [--authors=<file>] [-t <timezone>] [//p4repo/path <branch>]"
+ sys.exit(1)
+
+verbosity = 1
+logfile = "/dev/null"
+ignore_warnings = False
+stitch = 0
+tagall = True
+
+def report(level, msg, *args):
+ global verbosity
+ global logfile
+ for a in args:
+ msg = "%s %s" % (msg, a)
+ fd = open(logfile, "a")
+ fd.writelines(msg)
+ fd.close()
+ if level <= verbosity:
+ print msg
+
+class p4_command:
+ def __init__(self, _repopath):
+ try:
+ global logfile
+ self.userlist = {}
+ if _repopath[-1] == '/':
+ self.repopath = _repopath[:-1]
+ else:
+ self.repopath = _repopath
+ if self.repopath[-4:] != "/...":
+ self.repopath= "%s/..." % self.repopath
+ f=os.popen('p4 -V 2>>%s'%logfile, 'rb')
+ a = f.readlines()
+ if f.close():
+ raise
+ except:
+ die("Could not find the \"p4\" command")
+
+ def p4(self, cmd, *args):
+ global logfile
+ cmd = "%s %s" % (cmd, ' '.join(args))
+ report(2, "P4:", cmd)
+ f=os.popen('p4 -G %s 2>>%s' % (cmd,logfile), 'rb')
+ list = []
+ while 1:
+ try:
+ list.append(marshal.load(f))
+ except EOFError:
+ break
+ self.ret = f.close()
+ return list
+
+ def sync(self, id, force=False, trick=False, test=False):
+ if force:
+ ret = self.p4("sync -f %s@%s"%(self.repopath, id))[0]
+ elif trick:
+ ret = self.p4("sync -k %s@%s"%(self.repopath, id))[0]
+ elif test:
+ ret = self.p4("sync -n %s@%s"%(self.repopath, id))[0]
+ else:
+ ret = self.p4("sync %s@%s"%(self.repopath, id))[0]
+ if ret['code'] == "error":
+ data = ret['data'].upper()
+ if data.find('VIEW') > 0:
+ die("Perforce reports %s is not in client view"% self.repopath)
+ elif data.find('UP-TO-DATE') < 0:
+ die("Could not sync files from perforce", self.repopath)
+
+ def changes(self, since=0):
+ try:
+ list = []
+ for rec in self.p4("changes %s@%s,#head" % (self.repopath, since+1)):
+ list.append(rec['change'])
+ list.reverse()
+ return list
+ except:
+ return []
+
+ def authors(self, filename):
+ f=open(filename)
+ for l in f.readlines():
+ self.userlist[l[:l.find('=')].rstrip()] = \
+ (l[l.find('=')+1:l.find('<')].rstrip(),l[l.find('<')+1:l.find('>')])
+ f.close()
+ for f,e in self.userlist.items():
+ report(2, f, ":", e[0], " <", e[1], ">")
+
+ def _get_user(self, id):
+ if not self.userlist.has_key(id):
+ try:
+ user = self.p4("users", id)[0]
+ self.userlist[id] = (user['FullName'], user['Email'])
+ except:
+ self.userlist[id] = (id, "")
+ return self.userlist[id]
+
+ def _format_date(self, ticks):
+ symbol='+'
+ name = time.tzname[0]
+ offset = time.timezone
+ if ticks[8]:
+ name = time.tzname[1]
+ offset = time.altzone
+ if offset < 0:
+ offset *= -1
+ symbol = '-'
+ localo = "%s%02d%02d %s" % (symbol, offset / 3600, offset % 3600, name)
+ tickso = time.strftime("%a %b %d %H:%M:%S %Y", ticks)
+ return "%s %s" % (tickso, localo)
+
+ def where(self):
+ try:
+ return self.p4("where %s" % self.repopath)[-1]['path']
+ except:
+ return ""
+
+ def describe(self, num):
+ desc = self.p4("describe -s", num)[0]
+ self.msg = desc['desc']
+ self.author, self.email = self._get_user(desc['user'])
+ self.date = self._format_date(time.localtime(long(desc['time'])))
+ return self
+
+class git_command:
+ def __init__(self):
+ try:
+ self.version = self.git("--version")[0][12:].rstrip()
+ except:
+ die("Could not find the \"git\" command")
+ try:
+ self.gitdir = self.get_single("rev-parse --git-dir")
+ report(2, "gdir:", self.gitdir)
+ except:
+ die("Not a git repository... did you forget to \"git init-db\" ?")
+ try:
+ self.cdup = self.get_single("rev-parse --show-cdup")
+ if self.cdup != "":
+ os.chdir(self.cdup)
+ self.topdir = os.getcwd()
+ report(2, "topdir:", self.topdir)
+ except:
+ die("Could not find top git directory")
+
+ def git(self, cmd):
+ global logfile
+ report(2, "GIT:", cmd)
+ f=os.popen('git %s 2>>%s' % (cmd,logfile), 'rb')
+ r=f.readlines()
+ self.ret = f.close()
+ return r
+
+ def get_single(self, cmd):
+ return self.git(cmd)[0].rstrip()
+
+ def current_branch(self):
+ try:
+ testit = self.git("rev-parse --verify HEAD")[0]
+ return self.git("symbolic-ref HEAD")[0][11:].rstrip()
+ except:
+ return None
+
+ def get_config(self, variable):
+ try:
+ return self.git("repo-config --get %s" % variable)[0].rstrip()
+ except:
+ return None
+
+ def set_config(self, variable, value):
+ try:
+ self.git("repo-config %s %s"%(variable, value) )
+ except:
+ die("Could not set %s to " % variable, value)
+
+ def make_tag(self, name, head):
+ self.git("tag -f %s %s"%(name,head))
+
+ def top_change(self, branch):
+ try:
+ a=self.get_single("name-rev --tags refs/heads/%s" % branch)
+ loc = a.find(' tags/') + 6
+ if a[loc:loc+3] != "p4/":
+ raise
+ return int(a[loc+3:][:-2])
+ except:
+ return 0
+
+ def update_index(self):
+ self.git("ls-files -m -d -o -z | git update-index --add --remove -z --stdin")
+
+ def checkout(self, branch):
+ self.git("checkout %s" % branch)
+
+ def repoint_head(self, branch):
+ self.git("symbolic-ref HEAD refs/heads/%s" % branch)
+
+ def remove_files(self):
+ self.git("ls-files | xargs rm")
+
+ def clean_directories(self):
+ self.git("clean -d")
+
+ def fresh_branch(self, branch):
+ report(1, "Creating new branch", branch)
+ self.git("ls-files | xargs rm")
+ os.remove(".git/index")
+ self.repoint_head(branch)
+ self.git("clean -d")
+
+ def basedir(self):
+ return self.topdir
+
+ def commit(self, author, email, date, msg, id):
+ self.update_index()
+ fd=open(".msg", "w")
+ fd.writelines(msg)
+ fd.close()
+ try:
+ current = self.get_single("rev-parse --verify HEAD")
+ head = "-p HEAD"
+ except:
+ current = ""
+ head = ""
+ tree = self.get_single("write-tree")
+ for r,l in [('DATE',date),('NAME',author),('EMAIL',email)]:
+ os.environ['GIT_AUTHOR_%s'%r] = l
+ os.environ['GIT_COMMITTER_%s'%r] = l
+ commit = self.get_single("commit-tree %s %s < .msg" % (tree,head))
+ os.remove(".msg")
+ self.make_tag("p4/%s"%id, commit)
+ self.git("update-ref HEAD %s %s" % (commit, current) )
+
+try:
+ opts, args = getopt.getopt(sys.argv[1:], "qhvt:",
+ ["authors=","help","stitch=","timezone=","log=","ignore","notags"])
+except getopt.GetoptError:
+ usage()
+
+for o, a in opts:
+ if o == "-q":
+ verbosity = 0
+ if o == "-v":
+ verbosity += 1
+ if o in ("--log"):
+ logfile = a
+ if o in ("--notags"):
+ tagall = False
+ if o in ("-h", "--help"):
+ usage()
+ if o in ("--ignore"):
+ ignore_warnings = True
+
+git = git_command()
+branch=git.current_branch()
+
+for o, a in opts:
+ if o in ("-t", "--timezone"):
+ git.set_config("perforce.timezone", a)
+ if o in ("--stitch"):
+ git.set_config("perforce.%s.path" % branch, a)
+ stitch = 1
+
+if len(args) == 2:
+ branch = args[1]
+ git.checkout(branch)
+ if branch == git.current_branch():
+ die("Branch %s already exists!" % branch)
+ report(1, "Setting perforce to ", args[0])
+ git.set_config("perforce.%s.path" % branch, args[0])
+elif len(args) != 0:
+ die("You must specify the perforce //depot/path and git branch")
+
+p4path = git.get_config("perforce.%s.path" % branch)
+if p4path == None:
+ die("Do not know Perforce //depot/path for git branch", branch)
+
+p4 = p4_command(p4path)
+
+for o, a in opts:
+ if o in ("-a", "--authors"):
+ p4.authors(a)
+
+localdir = git.basedir()
+if p4.where()[:len(localdir)] != localdir:
+ report(1, "**WARNING** Appears p4 client is misconfigured")
+ report(1, " for sync from %s to %s" % (p4.repopath, localdir))
+ if ignore_warnings != True:
+ die("Reconfigure or use \"--ignore\" on command line")
+
+if stitch == 0:
+ top = git.top_change(branch)
+else:
+ top = 0
+changes = p4.changes(top)
+count = len(changes)
+if count == 0:
+ report(1, "Already up to date...")
+ sys.exit(0)
+
+ptz = git.get_config("perforce.timezone")
+if ptz:
+ report(1, "Setting timezone to", ptz)
+ os.environ['TZ'] = ptz
+ time.tzset()
+
+if stitch == 1:
+ git.remove_files()
+ git.clean_directories()
+ p4.sync(changes[0], force=True)
+elif top == 0 and branch != git.current_branch():
+ p4.sync(changes[0], test=True)
+ report(1, "Creating new initial commit");
+ git.fresh_branch(branch)
+ p4.sync(changes[0], force=True)
+else:
+ p4.sync(changes[0], trick=True)
+
+report(1, "processing %s changes from p4 (%s) to git (%s)" % (count, p4.repopath, branch))
+for id in changes:
+ report(1, "Importing changeset", id)
+ change = p4.describe(id)
+ p4.sync(id)
+ if tagall :
+ git.commit(change.author, change.email, change.date, change.msg, id)
+ else:
+ git.commit(change.author, change.email, change.date, change.msg, "import")
+ if stitch == 1:
+ git.clean_directories()
+ stitch = 0
+
else
rm -f "$GIT_DIR/ORIG_HEAD"
fi
-git-update-ref HEAD "$rev"
+git-update-ref -m "reset $reset_type $@" HEAD "$rev"
case "$reset_type" in
--hard )
use Term::ReadLine;
use Getopt::Long;
use Data::Dumper;
-use Net::SMTP;
# most mail servers generate the Date: header, but not all...
$ENV{LC_ALL} = 'C';
my $compose_filename = ".msg.$$";
# Variables we fill in automatically, or via prompting:
-my (@to,@cc,@initial_cc,$initial_reply_to,$initial_subject,@files,$from,$compose,$time);
+my (@to,@cc,@initial_cc,@bcclist,
+ $initial_reply_to,$initial_subject,@files,$from,$compose,$time);
# Behavior modification variables
my ($chain_reply_to, $quiet, $suppress_from, $no_signed_off_cc) = (1, 0, 0, 0);
"subject=s" => \$initial_subject,
"to=s" => \@to,
"cc=s" => \@initial_cc,
+ "bcc=s" => \@bcclist,
"chain-reply-to!" => \$chain_reply_to,
"smtp-server=s" => \$smtp_server,
"compose" => \$compose,
@to = expand_aliases(@to);
@initial_cc = expand_aliases(@initial_cc);
+@bcclist = expand_aliases(@bcclist);
if (!defined $initial_subject && $compose) {
do {
--cc Specify an initial "Cc:" list for the entire series
of emails.
+ --bcc Specify a list of email addresses that should be Bcc:
+ on all the emails.
+
--compose Use \$EDITOR to edit an introductory message for the
patch series.
}
# Variables we set as part of the loop over files
-our ($message_id, $cc, %mail, $subject, $reply_to, $message);
+our ($message_id, $cc, %mail, $subject, $reply_to, $references, $message);
sub extract_valid_address {
my $address = shift;
+ my $local_part_regexp = '[^<>"\s@]+';
+ my $domain_regexp = '[^.<>"\s@]+(?:\.[^.<>"\s@]+)+';
# check for a local address:
- return $address if ($address =~ /^([\w\-]+)$/);
+ return $address if ($address =~ /^($local_part_regexp)$/);
if ($have_email_valid) {
- return Email::Valid->address($address);
+ return scalar Email::Valid->address($address);
} else {
# less robust/correct than the monster regexp in Email::Valid,
# but still does a 99% job, and one less dependency
- return ($address =~ /([^\"<>\s]+@[^<>\s]+)/);
+ $address =~ /($local_part_regexp\@$domain_regexp)/;
+ return $1;
}
}
{
my @recipients = unique_email_list(@to);
my $to = join (",\n\t", @recipients);
- @recipients = unique_email_list(@recipients,@cc);
+ @recipients = unique_email_list(@recipients,@cc,@bcclist);
my $date = strftime('%a, %d %b %Y %H:%M:%S %z', localtime($time++));
my $gitversion = '@@GIT_VERSION@@';
if ($gitversion =~ m/..GIT_VERSION../) {
Message-Id: $message_id
X-Mailer: git-send-email $gitversion
";
- $header .= "In-Reply-To: $reply_to\n" if $reply_to;
+ if ($reply_to) {
+
+ $header .= "In-Reply-To: $reply_to\n";
+ $header .= "References: $references\n";
+ }
if ($smtp_server =~ m#^/#) {
my $pid = open my $sm, '|-';
defined $pid or die $!;
if (!$pid) {
- exec($smtp_server,'-i',@recipients) or die $!;
+ exec($smtp_server,'-i',
+ map { extract_valid_address($_) }
+ @recipients) or die $!;
}
print $sm "$header\n$message";
close $sm or die $?;
} else {
+ require Net::SMTP;
$smtp ||= Net::SMTP->new( $smtp_server );
$smtp->mail( $from ) or die $smtp->message;
$smtp->to( @recipients ) or die $smtp->message;
}
$reply_to = $initial_reply_to;
+$references = $initial_reply_to || '';
make_message_id();
$subject = $initial_subject;
# set up for the next message
if ($chain_reply_to || length($reply_to) == 0) {
$reply_to = $message_id;
+ if (length $references > 0) {
+ $references .= " $message_id";
+ } else {
+ $references = "$message_id";
+ }
}
make_message_id();
}
our @mergerx = ();
if ($opt_m) {
- @mergerx = ( qr/\W(?:from|of|merge|merging|merged) (\w+)/i );
+ my $branch_esc = quotemeta ($branch_name);
+ my $trunk_esc = quotemeta ($trunk_name);
+ @mergerx =
+ (
+ qr!\b(?:merg(?:ed?|ing))\b.*?\b((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i,
+ qr!\b(?:from|of)\W+((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i,
+ qr!\b(?:from|of)\W+(?:the )?([\w\.\-]+)[-\s]branch\b!i
+ );
}
if ($opt_M) {
- push (@mergerx, qr/$opt_M/);
+ unshift (@mergerx, qr/$opt_M/);
}
# Absolutize filename now, since we will have chdir'ed by the time we
#include <stdarg.h>
#include "git-compat-util.h"
#include "exec_cmd.h"
+#include "cache.h"
#include "builtin.h"
setenv("PATH", path, 1);
}
+static const char *alias_command;
+static char *alias_string = NULL;
+
+static int git_alias_config(const char *var, const char *value)
+{
+ if (!strncmp(var, "alias.", 6) && !strcmp(var + 6, alias_command)) {
+ alias_string = strdup(value);
+ }
+ return 0;
+}
+
+static int split_cmdline(char *cmdline, const char ***argv)
+{
+ int src, dst, count = 0, size = 16;
+ char quoted = 0;
+
+ *argv = malloc(sizeof(char*) * size);
+
+ /* split alias_string */
+ (*argv)[count++] = cmdline;
+ for (src = dst = 0; cmdline[src];) {
+ char c = cmdline[src];
+ if (!quoted && isspace(c)) {
+ cmdline[dst++] = 0;
+ while (cmdline[++src]
+ && isspace(cmdline[src]))
+ ; /* skip */
+ if (count >= size) {
+ size += 16;
+ *argv = realloc(*argv, sizeof(char*) * size);
+ }
+ (*argv)[count++] = cmdline + dst;
+ } else if(!quoted && (c == '\'' || c == '"')) {
+ quoted = c;
+ src++;
+ } else if (c == quoted) {
+ quoted = 0;
+ src++;
+ } else {
+ if (c == '\\' && quoted != '\'') {
+ src++;
+ c = cmdline[src];
+ if (!c) {
+ free(*argv);
+ *argv = NULL;
+ return error("cmdline ends with \\");
+ }
+ }
+ cmdline[dst++] = c;
+ src++;
+ }
+ }
+
+ cmdline[dst] = 0;
+
+ if (quoted) {
+ free(*argv);
+ *argv = NULL;
+ return error("unclosed quote");
+ }
+
+ return count;
+}
+
+static int handle_alias(int *argcp, const char ***argv)
+{
+ int nongit = 0, ret = 0;
+ const char *subdir;
+
+ subdir = setup_git_directory_gently(&nongit);
+ if (!nongit) {
+ int count;
+ const char** new_argv;
+
+ alias_command = (*argv)[0];
+ git_config(git_alias_config);
+ if (alias_string) {
+
+ count = split_cmdline(alias_string, &new_argv);
+
+ if (count < 1)
+ die("empty alias for %s", alias_command);
+
+ if (!strcmp(alias_command, new_argv[0]))
+ die("recursive alias: %s", alias_command);
+
+ /* insert after command name */
+ if (*argcp > 1) {
+ new_argv = realloc(new_argv, sizeof(char*) *
+ (count + *argcp));
+ memcpy(new_argv + count, *argv + 1,
+ sizeof(char*) * *argcp);
+ }
+
+ *argv = new_argv;
+ *argcp += count - 1;
+
+ ret = 1;
+ }
+ }
+
+ if (subdir)
+ chdir(subdir);
+
+ return ret;
+}
+
const char git_version_string[] = GIT_VERSION;
static void handle_internal_command(int argc, const char **argv, char **envp)
{ "add", cmd_add },
{ "rev-list", cmd_rev_list },
{ "init-db", cmd_init_db },
- { "tar-tree", cmd_tar_tree },
+ { "get-tar-commit-id", cmd_get_tar_commit_id },
{ "upload-tar", cmd_upload_tar },
{ "check-ref-format", cmd_check_ref_format },
{ "ls-files", cmd_ls_files },
{ "diff-index", cmd_diff_index },
{ "diff-stages", cmd_diff_stages },
{ "diff-tree", cmd_diff_tree },
- { "cat-file", cmd_cat_file }
+ { "cat-file", cmd_cat_file },
+ { "rev-parse", cmd_rev_parse }
};
int i;
char *slash = strrchr(cmd, '/');
char git_command[PATH_MAX + 1];
const char *exec_path = NULL;
+ int done_alias = 0;
/*
* Take the basename of argv[0] as the command
exec_path = git_exec_path();
prepend_to_path(exec_path, strlen(exec_path));
- /* See if it's an internal command */
- handle_internal_command(argc, argv, envp);
+ while (1) {
+ /* See if it's an internal command */
+ handle_internal_command(argc, argv, envp);
+
+ /* .. then try the external ones */
+ execv_git_cmd(argv);
- /* .. then try the external ones */
- execv_git_cmd(argv);
+ /* It could be an alias -- this works around the insanity
+ * of overriding "git log" with "git show" by having
+ * alias.log = show
+ */
+ if (done_alias || !handle_alias(&argc, &argv))
+ break;
+ done_alias = 1;
+ }
if (errno == ENOENT)
cmd_usage(0, exec_path, "'%s' is not a git-command", cmd);
set order "--date-order"
}
if {[catch {
- set fd [open [concat | git-rev-list --header $order \
+ set fd [open [concat | git rev-list --header $order \
--parents --boundary --default HEAD $args] r]
} err]} {
- puts stderr "Error executing git-rev-list: $err"
+ puts stderr "Error executing git rev-list: $err"
exit 1
}
set commfd($view) $fd
}
if {[string range $err 0 4] == "usage"} {
set err "Gitk: error reading commits$fv:\
- bad arguments to git-rev-list."
+ bad arguments to git rev-list."
if {$viewname($view) eq "Command line"} {
append err \
- " (Note: arguments to gitk are passed to git-rev-list\
+ " (Note: arguments to gitk are passed to git rev-list\
to allow selection of commits to be displayed.)"
}
} else {
if {[string length $shortcmit] > 80} {
set shortcmit "[string range $shortcmit 0 80]..."
}
- error_popup "Can't parse git-rev-list output: {$shortcmit}"
+ error_popup "Can't parse git rev-list output: {$shortcmit}"
exit 1
}
set id [lindex $ids 0]
if {$view == $curview} {
layoutmore
} elseif {[info exists hlview] && $view == $hlview} {
- highlightmore
+ vhighlightmore
}
}
if {[clock clicks -milliseconds] >= $nextupdate} {
}
proc readcommit {id} {
- if {[catch {set contents [exec git-cat-file commit $id]}]} return
+ if {[catch {set contents [exec git cat-file commit $id]}]} return
parsecommit $id $contents 0
}
proc updatecommits {} {
global viewdata curview phase displayorder
- global children commitrow
+ global children commitrow selectedline thickerline
if {$phase ne {}} {
stop_rev_list
catch {unset commitrow($n,$id)}
}
set curview -1
+ catch {unset selectedline}
+ catch {unset thickerline}
catch {unset viewdata($n)}
+ discardallcommits
readrefs
showview $n
}
set headline $comment
}
if {!$listed} {
- # git-rev-list indents the comment by 4 spaces;
- # if we got this via git-cat-file, add the indentation
+ # git rev-list indents the comment by 4 spaces;
+ # if we got this via git cat-file, add the indentation
set newcomment {}
foreach line [split $comment "\n"] {
append newcomment " "
set type {}
set tag {}
catch {
- set commit [exec git-rev-parse "$id^0"]
+ set commit [exec git rev-parse "$id^0"]
if {"$commit" != "$id"} {
set tagids($name) $commit
lappend idtags($commit) $name
}
}
catch {
- set tagcontents($name) [exec git-cat-file tag "$id"]
+ set tagcontents($name) [exec git cat-file tag "$id"]
}
} elseif { $type == "heads" } {
set headids($name) $id
close $refd
}
-proc show_error {w msg} {
+proc show_error {w top msg} {
message $w.m -text $msg -justify center -aspect 400
pack $w.m -side top -fill x -padx 20 -pady 20
- button $w.ok -text OK -command "destroy $w"
+ button $w.ok -text OK -command "destroy $top"
pack $w.ok -side bottom -fill x
- bind $w <Visibility> "grab $w; focus $w"
- bind $w <Key-Return> "destroy $w"
- tkwait window $w
+ bind $top <Visibility> "grab $top; focus $top"
+ bind $top <Key-Return> "destroy $top"
+ tkwait window $top
}
proc error_popup msg {
set w .error
toplevel $w
wm transient $w .
- show_error $w $msg
+ show_error $w $w $msg
}
proc makewindow {} {
global findtype findtypemenu findloc findstring fstring geometry
global entries sha1entry sha1string sha1but
global maincursor textcursor curtextcursor
- global rowctxmenu mergemax
+ global rowctxmenu mergemax wrapcomment
+ global highlight_files gdttype
+ global searchstring sstring
menu .bar
.bar add cascade -label "File" -menu .bar.file
.bar.edit configure -font $uifont
menu .bar.view -font $uifont
- menu .bar.view.hl -font $uifont -tearoff 0
.bar add cascade -label "View" -menu .bar.view
.bar.view add command -label "New view..." -command {newview 0}
.bar.view add command -label "Edit view..." -command editview \
-state disabled
.bar.view add command -label "Delete view" -command delview -state disabled
- .bar.view add cascade -label "Highlight" -menu .bar.view.hl
.bar.view add separator
.bar.view add radiobutton -label "All files" -command {showview 0} \
-variable selectedview -value 0
- .bar.view.hl add command -label "New view..." -command {newview 1}
- .bar.view.hl add command -label "Remove" -command delhighlight \
- -state disabled
- .bar.view.hl add separator
menu .bar.help
.bar add cascade -label "Help" -menu .bar.help
}
frame .ctop.top
frame .ctop.top.bar
+ frame .ctop.top.lbar
+ pack .ctop.top.lbar -side bottom -fill x
pack .ctop.top.bar -side bottom -fill x
set cscroll .ctop.top.csb
scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0
set findstring {}
set fstring .ctop.top.bar.findstring
lappend entries $fstring
- entry $fstring -width 30 -font $textfont -textvariable findstring -font $textfont
+ entry $fstring -width 30 -font $textfont -textvariable findstring
+ trace add variable findstring write find_change
pack $fstring -side left -expand 1 -fill x
set findtype Exact
set findtypemenu [tk_optionMenu .ctop.top.bar.findtype \
findtype Exact IgnCase Regexp]
+ trace add variable findtype write find_change
.ctop.top.bar.findtype configure -font $uifont
.ctop.top.bar.findtype.menu configure -font $uifont
set findloc "All fields"
tk_optionMenu .ctop.top.bar.findloc findloc "All fields" Headline \
- Comments Author Committer Files Pickaxe
+ Comments Author Committer
+ trace add variable findloc write find_change
.ctop.top.bar.findloc configure -font $uifont
.ctop.top.bar.findloc.menu configure -font $uifont
-
pack .ctop.top.bar.findloc -side right
pack .ctop.top.bar.findtype -side right
- # for making sure type==Exact whenever loc==Pickaxe
- trace add variable findloc write findlocchange
+
+ label .ctop.top.lbar.flabel -text "Highlight: Commits " \
+ -font $uifont
+ pack .ctop.top.lbar.flabel -side left -fill y
+ set gdttype "touching paths:"
+ set gm [tk_optionMenu .ctop.top.lbar.gdttype gdttype "touching paths:" \
+ "adding/removing string:"]
+ trace add variable gdttype write hfiles_change
+ $gm conf -font $uifont
+ .ctop.top.lbar.gdttype conf -font $uifont
+ pack .ctop.top.lbar.gdttype -side left -fill y
+ entry .ctop.top.lbar.fent -width 25 -font $textfont \
+ -textvariable highlight_files
+ trace add variable highlight_files write hfiles_change
+ lappend entries .ctop.top.lbar.fent
+ pack .ctop.top.lbar.fent -side left -fill x -expand 1
+ label .ctop.top.lbar.vlabel -text " OR in view" -font $uifont
+ pack .ctop.top.lbar.vlabel -side left -fill y
+ global viewhlmenu selectedhlview
+ set viewhlmenu [tk_optionMenu .ctop.top.lbar.vhl selectedhlview None]
+ $viewhlmenu entryconf 0 -command delvhighlight
+ $viewhlmenu conf -font $uifont
+ .ctop.top.lbar.vhl conf -font $uifont
+ pack .ctop.top.lbar.vhl -side left -fill y
+ label .ctop.top.lbar.rlabel -text " OR " -font $uifont
+ pack .ctop.top.lbar.rlabel -side left -fill y
+ global highlight_related
+ set m [tk_optionMenu .ctop.top.lbar.relm highlight_related None \
+ "Descendent" "Not descendent" "Ancestor" "Not ancestor"]
+ $m conf -font $uifont
+ .ctop.top.lbar.relm conf -font $uifont
+ trace add variable highlight_related write vrel_change
+ pack .ctop.top.lbar.relm -side left -fill y
panedwindow .ctop.cdet -orient horizontal
.ctop add .ctop.cdet
frame .ctop.cdet.left
+ frame .ctop.cdet.left.bot
+ pack .ctop.cdet.left.bot -side bottom -fill x
+ button .ctop.cdet.left.bot.search -text "Search" -command dosearch \
+ -font $uifont
+ pack .ctop.cdet.left.bot.search -side left -padx 5
+ set sstring .ctop.cdet.left.bot.sstring
+ entry $sstring -width 20 -font $textfont -textvariable searchstring
+ lappend entries $sstring
+ trace add variable searchstring write incrsearch
+ pack $sstring -side left -expand 1 -fill x
set ctext .ctop.cdet.left.ctext
text $ctext -bg white -state disabled -font $textfont \
-width $geometry(ctextw) -height $geometry(ctexth) \
- -yscrollcommand {.ctop.cdet.left.sb set} -wrap none
+ -yscrollcommand scrolltext -wrap none
scrollbar .ctop.cdet.left.sb -command "$ctext yview"
pack .ctop.cdet.left.sb -side right -fill y
pack $ctext -side left -fill both -expand 1
.ctop.cdet add .ctop.cdet.left
+ $ctext tag conf comment -wrap $wrapcomment
$ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa"
$ctext tag conf hunksep -fore blue
$ctext tag conf d0 -fore red
pack $cflist -side left -fill both -expand 1
$cflist tag configure highlight \
-background [$cflist cget -selectbackground]
+ $cflist tag configure bold -font [concat $mainfont bold]
.ctop.cdet add .ctop.cdet.right
bind .ctop.cdet <Configure> {resizecdetpanes %W %w}
bindkey <End> sellastline
bind . <Key-Up> "selnextline -1"
bind . <Key-Down> "selnextline 1"
+ bind . <Shift-Key-Up> "next_highlight -1"
+ bind . <Shift-Key-Down> "next_highlight 1"
bindkey <Key-Right> "goforw"
bindkey <Key-Left> "goback"
bind . <Key-Prior> "selnextpage -1"
bind . <Control-q> doquit
bind . <Control-f> dofind
bind . <Control-g> {findnext 0}
- bind . <Control-r> findprev
+ bind . <Control-r> dosearchback
+ bind . <Control-s> dosearch
bind . <Control-equal> {incrfont 1}
bind . <Control-KP_Add> {incrfont 1}
bind . <Control-minus> {incrfont -1}
proc scrollcanv {cscroll f0 f1} {
$cscroll set $f0 $f1
drawfrac $f0 $f1
+ flushhighlights
}
# when we make a key binding for the toplevel, make sure
proc savestuff {w} {
global canv canv2 canv3 ctext cflist mainfont textfont uifont
global stuffsaved findmergefiles maxgraphpct
- global maxwidth
+ global maxwidth showneartags
global viewname viewfiles viewargs viewperm nextviewnum
- global cmitmode
+ global cmitmode wrapcomment
if {$stuffsaved} return
if {![winfo viewable .]} return
puts $f [list set maxgraphpct $maxgraphpct]
puts $f [list set maxwidth $maxwidth]
puts $f [list set cmitmode $cmitmode]
+ puts $f [list set wrapcomment $wrapcomment]
+ puts $f [list set showneartags $showneartags]
puts $f "set geometry(width) [winfo width .ctop]"
puts $f "set geometry(height) [winfo height .ctop]"
puts $f "set geometry(canv1) [expr {[winfo width $canv]-2}]"
<Ctrl-Down> Scroll commit list down one line
<Ctrl-PageUp> Scroll commit list up one page
<Ctrl-PageDown> Scroll commit list down one page
+<Shift-Up> Move to previous highlighted line
+<Shift-Down> Move to next highlighted line
<Delete>, b Scroll diff view up one page
<Backspace> Scroll diff view up one page
<Space> Scroll diff view down one page
d Scroll diff view down 18 lines
<Ctrl-F> Find
<Ctrl-G> Move to next find hit
-<Ctrl-R> Move to previous find hit
<Return> Move to next find hit
/ Move to next find hit, or redo find
? Move to previous find hit
f Scroll diff view to next file
+<Ctrl-S> Search for next hit in diff view
+<Ctrl-R> Search for previous hit in diff view
<Ctrl-KP+> Increase font size
<Ctrl-plus> Increase font size
<Ctrl-KP-> Decrease font size
$w insert end $str
$w image create end -align center -image $bm -padx 1 \
-name a:$ix
- $w insert end $d
+ $w insert end $d [highlight_tag $prefix]
$w mark set s:$ix "end -1c"
$w mark gravity s:$ix left
}
set str "\n"
for {set i 0} {$i < $lev} {incr i} {append str "\t"}
$w insert end $str
- $w insert end $tail
+ $w insert end $tail [highlight_tag $f]
}
lappend treecontents($prefix) $tail
}
}
}
+proc highlight_tree {y prefix} {
+ global treeheight treecontents cflist
+
+ foreach e $treecontents($prefix) {
+ set path $prefix$e
+ if {[highlight_tag $path] ne {}} {
+ $cflist tag add bold $y.0 "$y.0 lineend"
+ }
+ incr y
+ if {[string index $e end] eq "/" && $treeheight($path) > 1} {
+ set y [highlight_tree $y $path]
+ }
+ }
+ return $y
+}
+
proc treeclosedir {w dir} {
global treediropen treeheight treeparent treeindex
incr treeheight($x) $n
}
foreach e $treecontents($dir) {
+ set de $dir$e
if {[string index $e end] eq "/"} {
- set de $dir$e
set iy $treeindex($de)
$w mark set d:$iy e:$ix
$w mark gravity d:$iy left
set treediropen($de) 0
$w image create e:$ix -align center -image tri-rt -padx 1 \
-name a:$iy
- $w insert e:$ix $e
+ $w insert e:$ix $e [highlight_tag $de]
$w mark set s:$iy e:$ix
$w mark gravity s:$iy left
set treeheight($de) 1
} else {
$w insert e:$ix $str
- $w insert e:$ix $e
+ $w insert e:$ix $e [highlight_tag $de]
}
}
$w mark gravity e:$ix left
set difffilestart {}
}
-proc add_flist {fl} {
- global flistmode cflist
+proc highlight_tag {f} {
+ global highlight_paths
+
+ foreach p $highlight_paths {
+ if {[string match $p $f]} {
+ return "bold"
+ }
+ }
+ return {}
+}
+
+proc highlight_filelist {} {
+ global cmitmode cflist
$cflist conf -state normal
- if {$flistmode eq "flat"} {
- foreach f $fl {
- $cflist insert end "\n$f"
+ if {$cmitmode ne "tree"} {
+ set end [lindex [split [$cflist index end] .] 0]
+ for {set l 2} {$l < $end} {incr l} {
+ set line [$cflist get $l.0 "$l.0 lineend"]
+ if {[highlight_tag $line] ne {}} {
+ $cflist tag add bold $l.0 "$l.0 lineend"
+ }
}
+ } else {
+ highlight_tree 2 {}
+ }
+ $cflist conf -state disabled
+}
+
+proc unhighlight_filelist {} {
+ global cflist
+
+ $cflist conf -state normal
+ $cflist tag remove bold 1.0 end
+ $cflist conf -state disabled
+}
+
+proc add_flist {fl} {
+ global cflist
+
+ $cflist conf -state normal
+ foreach f $fl {
+ $cflist insert end "\n"
+ $cflist insert end $f [highlight_tag $f]
}
$cflist conf -state disabled
}
proc sel_flist {w x y} {
- global flistmode ctext difffilestart cflist cflist_top cmitmode
+ global ctext difffilestart cflist cflist_top cmitmode
if {$cmitmode eq "tree"} return
if {![info exists cflist_top]} return
checkbutton $top.perm -text "Remember this view" -variable newviewperm($n)
grid $top.perm - -pady 5 -sticky w
message $top.al -aspect 1000 -font $uifont \
- -text "Commits to include (arguments to git-rev-list):"
+ -text "Commits to include (arguments to git rev-list):"
grid $top.al - -sticky w -pady 5
entry $top.args -width 50 -textvariable newviewargs($n) \
-background white
focus $top.t
}
-proc doviewmenu {m first cmd op args} {
+proc doviewmenu {m first cmd op argv} {
set nmenu [$m index end]
for {set i $first} {$i <= $nmenu} {incr i} {
if {[$m entrycget $i -command] eq $cmd} {
- eval $m $op $i $args
+ eval $m $op $i $argv
break
}
}
}
proc allviewmenus {n op args} {
+ global viewhlmenu
+
doviewmenu .bar.view 7 [list showview $n] $op $args
- doviewmenu .bar.view.hl 3 [list addhighlight $n] $op $args
+ doviewmenu $viewhlmenu 1 [list addvhighlight $n] $op $args
}
proc newviewok {top n} {
global nextviewnum newviewperm newviewname newishighlight
global viewname viewfiles viewperm selectedview curview
- global viewargs newviewargs
+ global viewargs newviewargs viewhlmenu
if {[catch {
set newargs [shellsplit $newviewargs($n)]
if {!$newishighlight} {
after idle showview $n
} else {
- after idle addhighlight $n
+ after idle addvhighlight $n
}
} else {
# editing an existing view
set viewperm($n) $newviewperm($n)
if {$newviewname($n) ne $viewname($n)} {
set viewname($n) $newviewname($n)
- allviewmenus $n entryconf -label $viewname($n)
+ doviewmenu .bar.view 7 [list showview $n] \
+ entryconf [list -label $viewname($n)]
+ doviewmenu $viewhlmenu 1 [list addvhighlight $n] \
+ entryconf [list -label $viewname($n) -value $viewname($n)]
}
if {$files ne $viewfiles($n) || $newargs ne $viewargs($n)} {
set viewfiles($n) $files
}
proc delview {} {
- global curview viewdata viewperm
+ global curview viewdata viewperm hlview selectedhlview
if {$curview == 0} return
+ if {[info exists hlview] && $hlview == $curview} {
+ set selectedhlview None
+ unset hlview
+ }
allviewmenus $curview delete
set viewdata($curview) {}
set viewperm($curview) 0
}
proc addviewmenu {n} {
- global viewname
+ global viewname viewhlmenu
.bar.view add radiobutton -label $viewname($n) \
-command [list showview $n] -variable selectedview -value $n
- .bar.view.hl add radiobutton -label $viewname($n) \
- -command [list addhighlight $n] -variable selectedhlview -value $n
+ $viewhlmenu add radiobutton -label $viewname($n) \
+ -command [list addvhighlight $n] -variable selectedhlview
}
proc flatten {var} {
global pending_select phase
global commitidx rowlaidout rowoptim linesegends
global commfd nextupdate
- global selectedview hlview selectedhlview
+ global selectedview
global vparentlist vchildlist vdisporder vcmitlisted
+ global hlview selectedhlview
if {$n == $curview} return
set selid {}
catch {unset matchinglines}
catch {unset treediffs}
clear_display
+ if {[info exists hlview] && $hlview == $n} {
+ unset hlview
+ set selectedhlview None
+ }
set curview $n
set selectedview $n
- set selectedhlview -1
.bar.view entryconf 2 -state [expr {$n == 0? "disabled": "normal"}]
.bar.view entryconf 3 -state [expr {$n == 0? "disabled": "normal"}]
- catch {unset hlview}
- .bar.view.hl entryconf 1 -state disabled
if {![info exists viewdata($n)]} {
set pending_select $selid
}
}
-proc addhighlight {n} {
- global hlview curview viewdata highlighted highlightedrows
- global selectedhlview
+# Stuff relating to the highlighting facility
+
+proc ishighlighted {row} {
+ global vhighlights fhighlights nhighlights rhighlights
+
+ if {[info exists nhighlights($row)] && $nhighlights($row) > 0} {
+ return $nhighlights($row)
+ }
+ if {[info exists vhighlights($row)] && $vhighlights($row) > 0} {
+ return $vhighlights($row)
+ }
+ if {[info exists fhighlights($row)] && $fhighlights($row) > 0} {
+ return $fhighlights($row)
+ }
+ if {[info exists rhighlights($row)] && $rhighlights($row) > 0} {
+ return $rhighlights($row)
+ }
+ return 0
+}
+
+proc bolden {row font} {
+ global canv linehtag selectedline boldrows
+
+ lappend boldrows $row
+ $canv itemconf $linehtag($row) -font $font
+ if {[info exists selectedline] && $row == $selectedline} {
+ $canv delete secsel
+ set t [eval $canv create rect [$canv bbox $linehtag($row)] \
+ -outline {{}} -tags secsel \
+ -fill [$canv cget -selectbackground]]
+ $canv lower $t
+ }
+}
+
+proc bolden_name {row font} {
+ global canv2 linentag selectedline boldnamerows
+
+ lappend boldnamerows $row
+ $canv2 itemconf $linentag($row) -font $font
+ if {[info exists selectedline] && $row == $selectedline} {
+ $canv2 delete secsel
+ set t [eval $canv2 create rect [$canv2 bbox $linentag($row)] \
+ -outline {{}} -tags secsel \
+ -fill [$canv2 cget -selectbackground]]
+ $canv2 lower $t
+ }
+}
+
+proc unbolden {} {
+ global mainfont boldrows
+
+ set stillbold {}
+ foreach row $boldrows {
+ if {![ishighlighted $row]} {
+ bolden $row $mainfont
+ } else {
+ lappend stillbold $row
+ }
+ }
+ set boldrows $stillbold
+}
+
+proc addvhighlight {n} {
+ global hlview curview viewdata vhl_done vhighlights commitidx
if {[info exists hlview]} {
- delhighlight
+ delvhighlight
}
set hlview $n
- set selectedhlview $n
- .bar.view.hl entryconf 1 -state normal
- set highlighted($n) 0
- set highlightedrows {}
if {$n != $curview && ![info exists viewdata($n)]} {
set viewdata($n) [list getcommits {{}} {{}} {} {} {} 0 0 0 {}]
set vparentlist($n) {}
set vdisporder($n) {}
set vcmitlisted($n) {}
start_rev_list $n
- } else {
- highlightmore
+ }
+ set vhl_done $commitidx($hlview)
+ if {$vhl_done > 0} {
+ drawvisible
}
}
-proc delhighlight {} {
- global hlview highlightedrows canv linehtag mainfont
- global selectedhlview selectedline
+proc delvhighlight {} {
+ global hlview vhighlights
if {![info exists hlview]} return
unset hlview
- set selectedhlview {}
- .bar.view.hl entryconf 1 -state disabled
- foreach l $highlightedrows {
- $canv itemconf $linehtag($l) -font $mainfont
- if {$l == $selectedline} {
- $canv delete secsel
- set t [eval $canv create rect [$canv bbox $linehtag($l)] \
- -outline {{}} -tags secsel \
- -fill [$canv cget -selectbackground]]
- $canv lower $t
- }
- }
+ catch {unset vhighlights}
+ unbolden
}
-proc highlightmore {} {
- global hlview highlighted commitidx highlightedrows linehtag mainfont
- global displayorder vdisporder curview canv commitrow selectedline
+proc vhighlightmore {} {
+ global hlview vhl_done commitidx vhighlights
+ global displayorder vdisporder curview mainfont
set font [concat $mainfont bold]
set max $commitidx($hlview)
} else {
set disp $vdisporder($hlview)
}
- for {set i $highlighted($hlview)} {$i < $max} {incr i} {
+ set vr [visiblerows]
+ set r0 [lindex $vr 0]
+ set r1 [lindex $vr 1]
+ for {set i $vhl_done} {$i < $max} {incr i} {
set id [lindex $disp $i]
if {[info exists commitrow($curview,$id)]} {
set row $commitrow($curview,$id)
- if {[info exists linehtag($row)]} {
- $canv itemconf $linehtag($row) -font $font
- lappend highlightedrows $row
- if {$row == $selectedline} {
- $canv delete secsel
- set t [eval $canv create rect \
- [$canv bbox $linehtag($row)] \
- -outline {{}} -tags secsel \
- -fill [$canv cget -selectbackground]]
- $canv lower $t
+ if {$r0 <= $row && $row <= $r1} {
+ if {![highlighted $row]} {
+ bolden $row $font
+ }
+ set vhighlights($row) 1
+ }
+ }
+ }
+ set vhl_done $max
+}
+
+proc askvhighlight {row id} {
+ global hlview vhighlights commitrow iddrawn mainfont
+
+ if {[info exists commitrow($hlview,$id)]} {
+ if {[info exists iddrawn($id)] && ![ishighlighted $row]} {
+ bolden $row [concat $mainfont bold]
+ }
+ set vhighlights($row) 1
+ } else {
+ set vhighlights($row) 0
+ }
+}
+
+proc hfiles_change {name ix op} {
+ global highlight_files filehighlight fhighlights fh_serial
+ global mainfont highlight_paths
+
+ if {[info exists filehighlight]} {
+ # delete previous highlights
+ catch {close $filehighlight}
+ unset filehighlight
+ catch {unset fhighlights}
+ unbolden
+ unhighlight_filelist
+ }
+ set highlight_paths {}
+ after cancel do_file_hl $fh_serial
+ incr fh_serial
+ if {$highlight_files ne {}} {
+ after 300 do_file_hl $fh_serial
+ }
+}
+
+proc makepatterns {l} {
+ set ret {}
+ foreach e $l {
+ set ee [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} $e]
+ if {[string index $ee end] eq "/"} {
+ lappend ret "$ee*"
+ } else {
+ lappend ret $ee
+ lappend ret "$ee/*"
+ }
+ }
+ return $ret
+}
+
+proc do_file_hl {serial} {
+ global highlight_files filehighlight highlight_paths gdttype fhl_list
+
+ if {$gdttype eq "touching paths:"} {
+ if {[catch {set paths [shellsplit $highlight_files]}]} return
+ set highlight_paths [makepatterns $paths]
+ highlight_filelist
+ set gdtargs [concat -- $paths]
+ } else {
+ set gdtargs [list "-S$highlight_files"]
+ }
+ set cmd [concat | git-diff-tree -r -s --stdin $gdtargs]
+ set filehighlight [open $cmd r+]
+ fconfigure $filehighlight -blocking 0
+ fileevent $filehighlight readable readfhighlight
+ set fhl_list {}
+ drawvisible
+ flushhighlights
+}
+
+proc flushhighlights {} {
+ global filehighlight fhl_list
+
+ if {[info exists filehighlight]} {
+ lappend fhl_list {}
+ puts $filehighlight ""
+ flush $filehighlight
+ }
+}
+
+proc askfilehighlight {row id} {
+ global filehighlight fhighlights fhl_list
+
+ lappend fhl_list $id
+ set fhighlights($row) -1
+ puts $filehighlight $id
+}
+
+proc readfhighlight {} {
+ global filehighlight fhighlights commitrow curview mainfont iddrawn
+ global fhl_list
+
+ while {[gets $filehighlight line] >= 0} {
+ set line [string trim $line]
+ set i [lsearch -exact $fhl_list $line]
+ if {$i < 0} continue
+ for {set j 0} {$j < $i} {incr j} {
+ set id [lindex $fhl_list $j]
+ if {[info exists commitrow($curview,$id)]} {
+ set fhighlights($commitrow($curview,$id)) 0
+ }
+ }
+ set fhl_list [lrange $fhl_list [expr {$i+1}] end]
+ if {$line eq {}} continue
+ if {![info exists commitrow($curview,$line)]} continue
+ set row $commitrow($curview,$line)
+ if {[info exists iddrawn($line)] && ![ishighlighted $row]} {
+ bolden $row [concat $mainfont bold]
+ }
+ set fhighlights($row) 1
+ }
+ if {[eof $filehighlight]} {
+ # strange...
+ puts "oops, git-diff-tree died"
+ catch {close $filehighlight}
+ unset filehighlight
+ }
+ next_hlcont
+}
+
+proc find_change {name ix op} {
+ global nhighlights mainfont boldnamerows
+ global findstring findpattern findtype
+
+ # delete previous highlights, if any
+ foreach row $boldnamerows {
+ bolden_name $row $mainfont
+ }
+ set boldnamerows {}
+ catch {unset nhighlights}
+ unbolden
+ if {$findtype ne "Regexp"} {
+ set e [string map {"*" "\\*" "?" "\\?" "\[" "\\\[" "\\" "\\\\"} \
+ $findstring]
+ set findpattern "*$e*"
+ }
+ drawvisible
+}
+
+proc askfindhighlight {row id} {
+ global nhighlights commitinfo iddrawn mainfont
+ global findstring findtype findloc findpattern
+
+ if {![info exists commitinfo($id)]} {
+ getcommit $id
+ }
+ set info $commitinfo($id)
+ set isbold 0
+ set fldtypes {Headline Author Date Committer CDate Comments}
+ foreach f $info ty $fldtypes {
+ if {$findloc ne "All fields" && $findloc ne $ty} {
+ continue
+ }
+ if {$findtype eq "Regexp"} {
+ set doesmatch [regexp $findstring $f]
+ } elseif {$findtype eq "IgnCase"} {
+ set doesmatch [string match -nocase $findpattern $f]
+ } else {
+ set doesmatch [string match $findpattern $f]
+ }
+ if {$doesmatch} {
+ if {$ty eq "Author"} {
+ set isbold 2
+ } else {
+ set isbold 1
+ }
+ }
+ }
+ if {[info exists iddrawn($id)]} {
+ if {$isbold && ![ishighlighted $row]} {
+ bolden $row [concat $mainfont bold]
+ }
+ if {$isbold >= 2} {
+ bolden_name $row [concat $mainfont bold]
+ }
+ }
+ set nhighlights($row) $isbold
+}
+
+proc vrel_change {name ix op} {
+ global highlight_related
+
+ rhighlight_none
+ if {$highlight_related ne "None"} {
+ after idle drawvisible
+ }
+}
+
+# prepare for testing whether commits are descendents or ancestors of a
+proc rhighlight_sel {a} {
+ global descendent desc_todo ancestor anc_todo
+ global highlight_related rhighlights
+
+ catch {unset descendent}
+ set desc_todo [list $a]
+ catch {unset ancestor}
+ set anc_todo [list $a]
+ if {$highlight_related ne "None"} {
+ rhighlight_none
+ after idle drawvisible
+ }
+}
+
+proc rhighlight_none {} {
+ global rhighlights
+
+ catch {unset rhighlights}
+ unbolden
+}
+
+proc is_descendent {a} {
+ global curview children commitrow descendent desc_todo
+
+ set v $curview
+ set la $commitrow($v,$a)
+ set todo $desc_todo
+ set leftover {}
+ set done 0
+ for {set i 0} {$i < [llength $todo]} {incr i} {
+ set do [lindex $todo $i]
+ if {$commitrow($v,$do) < $la} {
+ lappend leftover $do
+ continue
+ }
+ foreach nk $children($v,$do) {
+ if {![info exists descendent($nk)]} {
+ set descendent($nk) 1
+ lappend todo $nk
+ if {$nk eq $a} {
+ set done 1
}
}
}
+ if {$done} {
+ set desc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
+ return
+ }
}
- set highlighted($hlview) $max
+ set descendent($a) 0
+ set desc_todo $leftover
+}
+
+proc is_ancestor {a} {
+ global curview parentlist commitrow ancestor anc_todo
+
+ set v $curview
+ set la $commitrow($v,$a)
+ set todo $anc_todo
+ set leftover {}
+ set done 0
+ for {set i 0} {$i < [llength $todo]} {incr i} {
+ set do [lindex $todo $i]
+ if {![info exists commitrow($v,$do)] || $commitrow($v,$do) > $la} {
+ lappend leftover $do
+ continue
+ }
+ foreach np [lindex $parentlist $commitrow($v,$do)] {
+ if {![info exists ancestor($np)]} {
+ set ancestor($np) 1
+ lappend todo $np
+ if {$np eq $a} {
+ set done 1
+ }
+ }
+ }
+ if {$done} {
+ set anc_todo [concat $leftover [lrange $todo [expr {$i+1}] end]]
+ return
+ }
+ }
+ set ancestor($a) 0
+ set anc_todo $leftover
+}
+
+proc askrelhighlight {row id} {
+ global descendent highlight_related iddrawn mainfont rhighlights
+ global selectedline ancestor
+
+ if {![info exists selectedline]} return
+ set isbold 0
+ if {$highlight_related eq "Descendent" ||
+ $highlight_related eq "Not descendent"} {
+ if {![info exists descendent($id)]} {
+ is_descendent $id
+ }
+ if {$descendent($id) == ($highlight_related eq "Descendent")} {
+ set isbold 1
+ }
+ } elseif {$highlight_related eq "Ancestor" ||
+ $highlight_related eq "Not ancestor"} {
+ if {![info exists ancestor($id)]} {
+ is_ancestor $id
+ }
+ if {$ancestor($id) == ($highlight_related eq "Ancestor")} {
+ set isbold 1
+ }
+ }
+ if {[info exists iddrawn($id)]} {
+ if {$isbold && ![ishighlighted $row]} {
+ bolden $row [concat $mainfont bold]
+ }
+ }
+ set rhighlights($row) $isbold
+}
+
+proc next_hlcont {} {
+ global fhl_row fhl_dirn displayorder numcommits
+ global vhighlights fhighlights nhighlights rhighlights
+ global hlview filehighlight findstring highlight_related
+
+ if {![info exists fhl_dirn] || $fhl_dirn == 0} return
+ set row $fhl_row
+ while {1} {
+ if {$row < 0 || $row >= $numcommits} {
+ bell
+ set fhl_dirn 0
+ return
+ }
+ set id [lindex $displayorder $row]
+ if {[info exists hlview]} {
+ if {![info exists vhighlights($row)]} {
+ askvhighlight $row $id
+ }
+ if {$vhighlights($row) > 0} break
+ }
+ if {$findstring ne {}} {
+ if {![info exists nhighlights($row)]} {
+ askfindhighlight $row $id
+ }
+ if {$nhighlights($row) > 0} break
+ }
+ if {$highlight_related ne "None"} {
+ if {![info exists rhighlights($row)]} {
+ askrelhighlight $row $id
+ }
+ if {$rhighlights($row) > 0} break
+ }
+ if {[info exists filehighlight]} {
+ if {![info exists fhighlights($row)]} {
+ # ask for a few more while we're at it...
+ set r $row
+ for {set n 0} {$n < 100} {incr n} {
+ if {![info exists fhighlights($r)]} {
+ askfilehighlight $r [lindex $displayorder $r]
+ }
+ incr r $fhl_dirn
+ if {$r < 0 || $r >= $numcommits} break
+ }
+ flushhighlights
+ }
+ if {$fhighlights($row) < 0} {
+ set fhl_row $row
+ return
+ }
+ if {$fhighlights($row) > 0} break
+ }
+ incr row $fhl_dirn
+ }
+ set fhl_dirn 0
+ selectline $row 1
+}
+
+proc next_highlight {dirn} {
+ global selectedline fhl_row fhl_dirn
+ global hlview filehighlight findstring highlight_related
+
+ if {![info exists selectedline]} return
+ if {!([info exists hlview] || $findstring ne {} ||
+ $highlight_related ne "None" || [info exists filehighlight])} return
+ set fhl_row [expr {$selectedline + $dirn}]
+ set fhl_dirn $dirn
+ next_hlcont
+}
+
+proc cancel_next_highlight {} {
+ global fhl_dirn
+
+ set fhl_dirn 0
}
# Graph layout functions
global commitlisted commitinfo rowidlist
global rowtextx idpos idtags idheads idotherrefs
global linehtag linentag linedtag
- global mainfont canvxmax
- global hlview commitrow highlightedrows
+ global mainfont canvxmax boldrows boldnamerows
set ofill [expr {[lindex $commitlisted $row]? "blue": "white"}]
set x [xc $row $col]
set date [lindex $commitinfo($id) 2]
set date [formatdate $date]
set font $mainfont
- if {[info exists hlview] && [info exists commitrow($hlview,$id)]} {
+ set nfont $mainfont
+ set isbold [ishighlighted $row]
+ if {$isbold > 0} {
+ lappend boldrows $row
lappend font bold
- lappend highlightedrows $row
+ if {$isbold > 1} {
+ lappend boldnamerows $row
+ lappend nfont bold
+ }
}
set linehtag($row) [$canv create text $xt $y -anchor w \
-text $headline -font $font]
$canv bind $linehtag($row) <Button-3> "rowmenu %X %Y $id"
set linentag($row) [$canv2 create text 3 $y -anchor w \
- -text $name -font $mainfont]
+ -text $name -font $nfont]
set linedtag($row) [$canv3 create text 3 $y -anchor w \
-text $date -font $mainfont]
set xr [expr {$xt + [font measure $mainfont $headline]}]
global displayorder rowidlist
global idrangedrawn iddrawn
global commitinfo parentlist numcommits
+ global filehighlight fhighlights findstring nhighlights
+ global hlview vhighlights
+ global highlight_related rhighlights
if {$row >= $numcommits} return
foreach id [lindex $rowidlist $row] {
}
set id [lindex $displayorder $row]
+ if {[info exists hlview] && ![info exists vhighlights($row)]} {
+ askvhighlight $row $id
+ }
+ if {[info exists filehighlight] && ![info exists fhighlights($row)]} {
+ askfilehighlight $row $id
+ }
+ if {$findstring ne {} && ![info exists nhighlights($row)]} {
+ askfindhighlight $row $id
+ }
+ if {$highlight_related ne "None" && ![info exists rhighlights($row)]} {
+ askrelhighlight $row $id
+ }
if {[info exists iddrawn($id)]} return
set col [lsearch -exact [lindex $rowidlist $row] $id]
if {$col < 0} {
proc clear_display {} {
global iddrawn idrangedrawn
+ global vhighlights fhighlights nhighlights rhighlights
allcanvs delete all
catch {unset iddrawn}
catch {unset idrangedrawn}
+ catch {unset vhighlights}
+ catch {unset fhighlights}
+ catch {unset nhighlights}
+ catch {unset rhighlights}
}
proc findcrossings {id} {
stopfindproc
unmarkmatches
+ cancel_next_highlight
focus .
set matchinglines {}
- if {$findloc == "Pickaxe"} {
- findpatches
- return
- }
if {$findtype == "IgnCase"} {
set foundstring [string tolower $findstring]
} else {
if {$foundstrlen == 0} return
regsub -all {[*?\[\\]} $foundstring {\\&} matchstring
set matchstring "*$matchstring*"
- if {$findloc == "Files"} {
- findfiles
- return
- }
if {![info exists selectedline]} {
set oldsel -1
} else {
set oldsel $selectedline
}
set didsel 0
- set fldtypes {Headline Author Date Committer CDate Comment}
+ set fldtypes {Headline Author Date Committer CDate Comments}
set l -1
foreach id $displayorder {
set d $commitdata($id)
}
}
-proc findlocchange {name ix op} {
- global findloc findtype findtypemenu
- if {$findloc == "Pickaxe"} {
- set findtype Exact
- set state disabled
- } else {
- set state normal
- }
- $findtypemenu entryconf 1 -state $state
- $findtypemenu entryconf 2 -state $state
-}
-
proc stopfindproc {{done 0}} {
global findprocpid findprocfile findids
global ctext findoldcursor phase maincursor textcursor
notbusy find
}
-proc findpatches {} {
- global findstring selectedline numcommits
- global findprocpid findprocfile
- global finddidsel ctext displayorder findinprogress
- global findinsertpos
-
- if {$numcommits == 0} return
-
- # make a list of all the ids to search, starting at the one
- # after the selected line (if any)
- if {[info exists selectedline]} {
- set l $selectedline
- } else {
- set l -1
- }
- set inputids {}
- for {set i 0} {$i < $numcommits} {incr i} {
- if {[incr l] >= $numcommits} {
- set l 0
- }
- append inputids [lindex $displayorder $l] "\n"
- }
-
- if {[catch {
- set f [open [list | git-diff-tree --stdin -s -r -S$findstring \
- << $inputids] r]
- } err]} {
- error_popup "Error starting search process: $err"
- return
- }
-
- set findinsertpos end
- set findprocfile $f
- set findprocpid [pid $f]
- fconfigure $f -blocking 0
- fileevent $f readable readfindproc
- set finddidsel 0
- nowbusy find
- set findinprogress 1
-}
-
-proc readfindproc {} {
- global findprocfile finddidsel
- global commitrow matchinglines findinsertpos curview
-
- set n [gets $findprocfile line]
- if {$n < 0} {
- if {[eof $findprocfile]} {
- stopfindproc 1
- if {!$finddidsel} {
- bell
- }
- }
- return
- }
- if {![regexp {^[0-9a-f]{40}} $line id]} {
- error_popup "Can't parse git-diff-tree output: $line"
- stopfindproc
- return
- }
- if {![info exists commitrow($curview,$id)]} {
- puts stderr "spurious id: $id"
- return
- }
- set l $commitrow($curview,$id)
- insertmatch $l $id
-}
-
-proc insertmatch {l id} {
- global matchinglines findinsertpos finddidsel
-
- if {$findinsertpos == "end"} {
- if {$matchinglines != {} && $l < [lindex $matchinglines 0]} {
- set matchinglines [linsert $matchinglines 0 $l]
- set findinsertpos 1
- } else {
- lappend matchinglines $l
- }
- } else {
- set matchinglines [linsert $matchinglines $findinsertpos $l]
- incr findinsertpos
- }
- markheadline $l $id
- if {!$finddidsel} {
- findselectline $l
- set finddidsel 1
- }
-}
-
-proc findfiles {} {
- global selectedline numcommits displayorder ctext
- global ffileline finddidsel parentlist
- global findinprogress findstartline findinsertpos
- global treediffs fdiffid fdiffsneeded fdiffpos
- global findmergefiles
-
- if {$numcommits == 0} return
-
- if {[info exists selectedline]} {
- set l [expr {$selectedline + 1}]
- } else {
- set l 0
- }
- set ffileline $l
- set findstartline $l
- set diffsneeded {}
- set fdiffsneeded {}
- while 1 {
- set id [lindex $displayorder $l]
- if {$findmergefiles || [llength [lindex $parentlist $l]] == 1} {
- if {![info exists treediffs($id)]} {
- append diffsneeded "$id\n"
- lappend fdiffsneeded $id
- }
- }
- if {[incr l] >= $numcommits} {
- set l 0
- }
- if {$l == $findstartline} break
- }
-
- # start off a git-diff-tree process if needed
- if {$diffsneeded ne {}} {
- if {[catch {
- set df [open [list | git-diff-tree -r --stdin << $diffsneeded] r]
- } err ]} {
- error_popup "Error starting search process: $err"
- return
- }
- catch {unset fdiffid}
- set fdiffpos 0
- fconfigure $df -blocking 0
- fileevent $df readable [list readfilediffs $df]
- }
-
- set finddidsel 0
- set findinsertpos end
- set id [lindex $displayorder $l]
- nowbusy find
- set findinprogress 1
- findcont
- update
-}
-
-proc readfilediffs {df} {
- global findid fdiffid fdiffs
-
- set n [gets $df line]
- if {$n < 0} {
- if {[eof $df]} {
- donefilediff
- if {[catch {close $df} err]} {
- stopfindproc
- bell
- error_popup "Error in git-diff-tree: $err"
- } elseif {[info exists findid]} {
- set id $findid
- stopfindproc
- bell
- error_popup "Couldn't find diffs for $id"
- }
- }
- return
- }
- if {[regexp {^([0-9a-f]{40})$} $line match id]} {
- # start of a new string of diffs
- donefilediff
- set fdiffid $id
- set fdiffs {}
- } elseif {[string match ":*" $line]} {
- lappend fdiffs [lindex $line 5]
- }
-}
-
-proc donefilediff {} {
- global fdiffid fdiffs treediffs findid
- global fdiffsneeded fdiffpos
-
- if {[info exists fdiffid]} {
- while {[lindex $fdiffsneeded $fdiffpos] ne $fdiffid
- && $fdiffpos < [llength $fdiffsneeded]} {
- # git-diff-tree doesn't output anything for a commit
- # which doesn't change anything
- set nullid [lindex $fdiffsneeded $fdiffpos]
- set treediffs($nullid) {}
- if {[info exists findid] && $nullid eq $findid} {
- unset findid
- findcont
- }
- incr fdiffpos
- }
- incr fdiffpos
-
- if {![info exists treediffs($fdiffid)]} {
- set treediffs($fdiffid) $fdiffs
- }
- if {[info exists findid] && $fdiffid eq $findid} {
- unset findid
- findcont
- }
- }
-}
-
-proc findcont {} {
- global findid treediffs parentlist
- global ffileline findstartline finddidsel
- global displayorder numcommits matchinglines findinprogress
- global findmergefiles
-
- set l $ffileline
- while {1} {
- set id [lindex $displayorder $l]
- if {$findmergefiles || [llength [lindex $parentlist $l]] == 1} {
- if {![info exists treediffs($id)]} {
- set findid $id
- set ffileline $l
- return
- }
- set doesmatch 0
- foreach f $treediffs($id) {
- set x [findmatches $f]
- if {$x != {}} {
- set doesmatch 1
- break
- }
- }
- if {$doesmatch} {
- insertmatch $l $id
- }
- }
- if {[incr l] >= $numcommits} {
- set l 0
- }
- if {$l == $findstartline} break
- }
- stopfindproc
- if {!$finddidsel} {
- bell
- }
-}
-
# mark a commit as matching by putting a yellow background
# behind the headline
proc markheadline {l id} {
proc commit_descriptor {p} {
global commitinfo
+ if {![info exists commitinfo($p)]} {
+ getcommit $p
+ }
set l "..."
- if {[info exists commitinfo($p)]} {
+ if {[llength $commitinfo($p)] > 1} {
set l [lindex $commitinfo($p) 0]
}
- return "$p ($l)"
+ return "$p ($l)\n"
}
# append some text to the ctext widget, and make any SHA1 ID
# that we know about be a clickable link.
-proc appendwithlinks {text} {
+proc appendwithlinks {text tags} {
global ctext commitrow linknum curview
set start [$ctext index "end - 1c"]
- $ctext insert end $text
- $ctext insert end "\n"
+ $ctext insert end $text $tags
set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
foreach l $links {
set s [lindex $l 0]
allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}]
}
+# add a list of tag or branch names at position pos
+# returns the number of names inserted
+proc appendrefs {pos l var} {
+ global ctext commitrow linknum curview idtags $var
+
+ if {[catch {$ctext index $pos}]} {
+ return 0
+ }
+ set tags {}
+ foreach id $l {
+ foreach tag [set $var\($id\)] {
+ lappend tags [concat $tag $id]
+ }
+ }
+ set tags [lsort -index 1 $tags]
+ set sep {}
+ foreach tag $tags {
+ set name [lindex $tag 0]
+ set id [lindex $tag 1]
+ set lk link$linknum
+ incr linknum
+ $ctext insert $pos $sep
+ $ctext insert $pos $name $lk
+ $ctext tag conf $lk -foreground blue
+ if {[info exists commitrow($curview,$id)]} {
+ $ctext tag bind $lk <1> \
+ [list selectline $commitrow($curview,$id) 1]
+ $ctext tag conf $lk -underline 1
+ $ctext tag bind $lk <Enter> { %W configure -cursor hand2 }
+ $ctext tag bind $lk <Leave> { %W configure -cursor $curtextcursor }
+ }
+ set sep ", "
+ }
+ return [llength $tags]
+}
+
+# called when we have finished computing the nearby tags
+proc dispneartags {} {
+ global selectedline currentid ctext anc_tags desc_tags showneartags
+ global desc_heads
+
+ if {![info exists selectedline] || !$showneartags} return
+ set id $currentid
+ $ctext conf -state normal
+ if {[info exists desc_heads($id)]} {
+ if {[appendrefs branch $desc_heads($id) idheads] > 1} {
+ $ctext insert "branch -2c" "es"
+ }
+ }
+ if {[info exists anc_tags($id)]} {
+ appendrefs follows $anc_tags($id) idtags
+ }
+ if {[info exists desc_tags($id)]} {
+ appendrefs precedes $desc_tags($id) idtags
+ }
+ $ctext conf -state disabled
+}
+
proc selectline {l isnew} {
global canv canv2 canv3 ctext commitinfo selectedline
global displayorder linehtag linentag linedtag
global currentid sha1entry
global commentend idtags linknum
global mergemax numcommits pending_select
- global cmitmode
+ global cmitmode desc_tags anc_tags showneartags allcommits desc_heads
catch {unset pending_select}
$canv delete hover
normalline
+ cancel_next_highlight
if {$l < 0 || $l >= $numcommits} return
set y [expr {$canvy0 + $l * $linespc}]
set ymax [lindex [$canv cget -scrollregion] 3]
$sha1entry insert 0 $id
$sha1entry selection from 0
$sha1entry selection to end
+ rhighlight_sel $id
$ctext conf -state normal
- $ctext delete 0.0 end
+ clear_ctext
set linknum 0
set info $commitinfo($id)
set date [formatdate [lindex $info 2]]
$ctext insert end "\n"
}
- set comment {}
+ set headers {}
set olds [lindex $parentlist $l]
if {[llength $olds] > 1} {
set np 0
set tag m$np
}
$ctext insert end "Parent: " $tag
- appendwithlinks [commit_descriptor $p]
+ appendwithlinks [commit_descriptor $p] {}
incr np
}
} else {
foreach p $olds {
- append comment "Parent: [commit_descriptor $p]\n"
+ append headers "Parent: [commit_descriptor $p]"
}
}
foreach c [lindex $childlist $l] {
- append comment "Child: [commit_descriptor $c]\n"
+ append headers "Child: [commit_descriptor $c]"
}
- append comment "\n"
- append comment [lindex $info 5]
# make anything that looks like a SHA1 ID be a clickable link
- appendwithlinks $comment
+ appendwithlinks $headers {}
+ if {$showneartags} {
+ if {![info exists allcommits]} {
+ getallcommits
+ }
+ $ctext insert end "Branch: "
+ $ctext mark set branch "end -1c"
+ $ctext mark gravity branch left
+ if {[info exists desc_heads($id)]} {
+ if {[appendrefs branch $desc_heads($id) idheads] > 1} {
+ # turn "Branch" into "Branches"
+ $ctext insert "branch -2c" "es"
+ }
+ }
+ $ctext insert end "\nFollows: "
+ $ctext mark set follows "end -1c"
+ $ctext mark gravity follows left
+ if {[info exists anc_tags($id)]} {
+ appendrefs follows $anc_tags($id) idtags
+ }
+ $ctext insert end "\nPrecedes: "
+ $ctext mark set precedes "end -1c"
+ $ctext mark gravity precedes left
+ if {[info exists desc_tags($id)]} {
+ appendrefs precedes $desc_tags($id) idtags
+ }
+ $ctext insert end "\n"
+ }
+ $ctext insert end "\n"
+ appendwithlinks [lindex $info 5] {comment}
$ctext tag delete Comments
$ctext tag remove found 1.0 end
set lpp 1
}
allcanvs yview scroll [expr {$dir * $lpp}] units
+ drawvisible
if {![info exists selectedline]} return
set l [expr {$selectedline + $dir * $lpp}]
if {$l < 0} {
catch {unset selectedline}
catch {unset currentid}
allcanvs delete secsel
+ rhighlight_none
+ cancel_next_highlight
}
proc reselectline {} {
catch {unset diffmergeid}
if {![info exists treefilelist($id)]} {
if {![info exists treepending]} {
- if {[catch {set gtf [open [concat | git-ls-tree -r $id] r]}]} {
+ if {[catch {set gtf [open [concat | git ls-tree -r $id] r]}]} {
return
}
set treepending $id
return
}
set blob [lindex $treeidlist($diffids) $i]
- if {[catch {set bf [open [concat | git-cat-file blob $blob] r]} err]} {
+ if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} {
puts "oops, error reading blob $blob: $err"
return
}
fconfigure $bf -blocking 0
fileevent $bf readable [list getblobline $bf $diffids]
$ctext config -state normal
- $ctext delete $commentend end
+ clear_ctext $commentend
$ctext insert end "\n"
$ctext insert end "$f\n" filesep
$ctext config -state disabled
set diffids $id
# this doesn't seem to actually affect anything...
set env(GIT_DIFF_OPTS) $diffopts
- set cmd [concat | git-diff-tree --no-commit-id --cc $id]
+ set cmd [concat | git diff-tree --no-commit-id --cc $id]
if {[catch {set mdf [open $cmd r]} err]} {
error_popup "Error getting merge diffs: $err"
return
set treepending $ids
set treediff {}
if {[catch \
- {set gdtf [open [concat | git-diff-tree --no-commit-id -r $ids] r]} \
+ {set gdtf [open [concat | git diff-tree --no-commit-id -r $ids] r]} \
]} return
fconfigure $gdtf -blocking 0
fileevent $gdtf readable [list gettreediffline $gdtf $ids]
global nextupdate diffinhdr treediffs
set env(GIT_DIFF_OPTS) $diffopts
- set cmd [concat | git-diff-tree --no-commit-id -r -p -C $ids]
+ set cmd [concat | git diff-tree --no-commit-id -r -p -C $ids]
if {[catch {set bdf [open $cmd r]} err]} {
puts "error getting diffs: $err"
return
}
}
+proc clear_ctext {{first 1.0}} {
+ global ctext smarktop smarkbot
+
+ set l [lindex [split $first .] 0]
+ if {![info exists smarktop] || [$ctext compare $first < $smarktop.0]} {
+ set smarktop $l
+ }
+ if {![info exists smarkbot] || [$ctext compare $first < $smarkbot.0]} {
+ set smarkbot $l
+ }
+ $ctext delete $first end
+}
+
+proc incrsearch {name ix op} {
+ global ctext searchstring searchdirn
+
+ $ctext tag remove found 1.0 end
+ if {[catch {$ctext index anchor}]} {
+ # no anchor set, use start of selection, or of visible area
+ set sel [$ctext tag ranges sel]
+ if {$sel ne {}} {
+ $ctext mark set anchor [lindex $sel 0]
+ } elseif {$searchdirn eq "-forwards"} {
+ $ctext mark set anchor @0,0
+ } else {
+ $ctext mark set anchor @0,[winfo height $ctext]
+ }
+ }
+ if {$searchstring ne {}} {
+ set here [$ctext search $searchdirn -- $searchstring anchor]
+ if {$here ne {}} {
+ $ctext see $here
+ }
+ searchmarkvisible 1
+ }
+}
+
+proc dosearch {} {
+ global sstring ctext searchstring searchdirn
+
+ focus $sstring
+ $sstring icursor end
+ set searchdirn -forwards
+ if {$searchstring ne {}} {
+ set sel [$ctext tag ranges sel]
+ if {$sel ne {}} {
+ set start "[lindex $sel 0] + 1c"
+ } elseif {[catch {set start [$ctext index anchor]}]} {
+ set start "@0,0"
+ }
+ set match [$ctext search -count mlen -- $searchstring $start]
+ $ctext tag remove sel 1.0 end
+ if {$match eq {}} {
+ bell
+ return
+ }
+ $ctext see $match
+ set mend "$match + $mlen c"
+ $ctext tag add sel $match $mend
+ $ctext mark unset anchor
+ }
+}
+
+proc dosearchback {} {
+ global sstring ctext searchstring searchdirn
+
+ focus $sstring
+ $sstring icursor end
+ set searchdirn -backwards
+ if {$searchstring ne {}} {
+ set sel [$ctext tag ranges sel]
+ if {$sel ne {}} {
+ set start [lindex $sel 0]
+ } elseif {[catch {set start [$ctext index anchor]}]} {
+ set start @0,[winfo height $ctext]
+ }
+ set match [$ctext search -backwards -count ml -- $searchstring $start]
+ $ctext tag remove sel 1.0 end
+ if {$match eq {}} {
+ bell
+ return
+ }
+ $ctext see $match
+ set mend "$match + $ml c"
+ $ctext tag add sel $match $mend
+ $ctext mark unset anchor
+ }
+}
+
+proc searchmark {first last} {
+ global ctext searchstring
+
+ set mend $first.0
+ while {1} {
+ set match [$ctext search -count mlen -- $searchstring $mend $last.end]
+ if {$match eq {}} break
+ set mend "$match + $mlen c"
+ $ctext tag add found $match $mend
+ }
+}
+
+proc searchmarkvisible {doall} {
+ global ctext smarktop smarkbot
+
+ set topline [lindex [split [$ctext index @0,0] .] 0]
+ set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0]
+ if {$doall || $botline < $smarktop || $topline > $smarkbot} {
+ # no overlap with previous
+ searchmark $topline $botline
+ set smarktop $topline
+ set smarkbot $botline
+ } else {
+ if {$topline < $smarktop} {
+ searchmark $topline [expr {$smarktop-1}]
+ set smarktop $topline
+ }
+ if {$botline > $smarkbot} {
+ searchmark [expr {$smarkbot+1}] $botline
+ set smarkbot $botline
+ }
+ }
+}
+
+proc scrolltext {f0 f1} {
+ global searchstring
+
+ .ctop.cdet.left.sb set $f0 $f1
+ if {$searchstring ne {}} {
+ searchmarkvisible 0
+ }
+}
+
proc setcoords {} {
global linespc charspc canvx0 canvy0 mainfont
global xspc1 xspc2 lthickness
}
# fill the details pane with info about this line
$ctext conf -state normal
- $ctext delete 0.0 end
+ clear_ctext
$ctext tag conf link -foreground blue -underline 1
$ctext tag bind link <Enter> { %W configure -cursor hand2 }
$ctext tag bind link <Leave> { %W configure -cursor $curtextcursor }
global commitinfo
$ctext conf -state normal
- $ctext delete 0.0 end
+ clear_ctext
init_flist "Top"
$ctext insert end "From "
$ctext tag conf link -foreground blue -underline 1
set oldid [$patchtop.fromsha1 get]
set newid [$patchtop.tosha1 get]
set fname [$patchtop.fname get]
- if {[catch {exec git-diff-tree -p $oldid $newid >$fname &} err]} {
+ if {[catch {exec git diff-tree -p $oldid $newid >$fname &} err]} {
error_popup "Error creating patch: $err"
}
catch {destroy $patchtop}
proc redrawtags {id} {
global canv linehtag commitrow idpos selectedline curview
+ global mainfont
if {![info exists commitrow($curview,$id)]} return
drawcmitrow $commitrow($curview,$id)
$canv delete tag.$id
set xt [eval drawtags $id $idpos($id)]
$canv coords $linehtag($commitrow($curview,$id)) $xt [lindex $idpos($id) 2]
+ set text [$canv itemcget $linehtag($commitrow($curview,$id)) -text]
+ set xr [expr {$xt + [font measure $mainfont $text]}]
+ if {$xr > $canvxmax} {
+ set canvxmax $xr
+ setcanvscroll
+ }
if {[info exists selectedline]
&& $selectedline == $commitrow($curview,$id)} {
selectline $selectedline 0
unset wrcomtop
}
-proc listrefs {id} {
- global idtags idheads idotherrefs
+# Stuff for finding nearby tags
+proc getallcommits {} {
+ global allcstart allcommits allcfd
- set x {}
- if {[info exists idtags($id)]} {
- set x $idtags($id)
+ set fd [open [concat | git rev-list --all --topo-order --parents] r]
+ set allcfd $fd
+ fconfigure $fd -blocking 0
+ set allcommits "reading"
+ nowbusy allcommits
+ restartgetall $fd
+}
+
+proc discardallcommits {} {
+ global allparents allchildren allcommits allcfd
+ global desc_tags anc_tags alldtags tagisdesc allids desc_heads
+
+ if {![info exists allcommits]} return
+ if {$allcommits eq "reading"} {
+ catch {close $allcfd}
}
- set y {}
- if {[info exists idheads($id)]} {
- set y $idheads($id)
+ foreach v {allcommits allchildren allparents allids desc_tags anc_tags
+ alldtags tagisdesc desc_heads} {
+ catch {unset $v}
}
- set z {}
- if {[info exists idotherrefs($id)]} {
- set z $idotherrefs($id)
+}
+
+proc restartgetall {fd} {
+ global allcstart
+
+ fileevent $fd readable [list getallclines $fd]
+ set allcstart [clock clicks -milliseconds]
+}
+
+proc combine_dtags {l1 l2} {
+ global tagisdesc notfirstd
+
+ set res [lsort -unique [concat $l1 $l2]]
+ for {set i 0} {$i < [llength $res]} {incr i} {
+ set x [lindex $res $i]
+ for {set j [expr {$i+1}]} {$j < [llength $res]} {} {
+ set y [lindex $res $j]
+ if {[info exists tagisdesc($x,$y)]} {
+ if {$tagisdesc($x,$y) > 0} {
+ # x is a descendent of y, exclude x
+ set res [lreplace $res $i $i]
+ incr i -1
+ break
+ } else {
+ # y is a descendent of x, exclude y
+ set res [lreplace $res $j $j]
+ }
+ } else {
+ # no relation, keep going
+ incr j
+ }
+ }
+ }
+ return $res
+}
+
+proc combine_atags {l1 l2} {
+ global tagisdesc
+
+ set res [lsort -unique [concat $l1 $l2]]
+ for {set i 0} {$i < [llength $res]} {incr i} {
+ set x [lindex $res $i]
+ for {set j [expr {$i+1}]} {$j < [llength $res]} {} {
+ set y [lindex $res $j]
+ if {[info exists tagisdesc($x,$y)]} {
+ if {$tagisdesc($x,$y) < 0} {
+ # x is an ancestor of y, exclude x
+ set res [lreplace $res $i $i]
+ incr i -1
+ break
+ } else {
+ # y is an ancestor of x, exclude y
+ set res [lreplace $res $j $j]
+ }
+ } else {
+ # no relation, keep going
+ incr j
+ }
+ }
+ }
+ return $res
+}
+
+proc getallclines {fd} {
+ global allparents allchildren allcommits allcstart
+ global desc_tags anc_tags idtags alldtags tagisdesc allids
+ global desc_heads idheads
+
+ while {[gets $fd line] >= 0} {
+ set id [lindex $line 0]
+ lappend allids $id
+ set olds [lrange $line 1 end]
+ set allparents($id) $olds
+ if {![info exists allchildren($id)]} {
+ set allchildren($id) {}
+ }
+ foreach p $olds {
+ lappend allchildren($p) $id
+ }
+ # compute nearest tagged descendents as we go
+ # also compute descendent heads
+ set dtags {}
+ set dheads {}
+ foreach child $allchildren($id) {
+ if {[info exists idtags($child)]} {
+ set ctags [list $child]
+ } else {
+ set ctags $desc_tags($child)
+ }
+ if {$dtags eq {}} {
+ set dtags $ctags
+ } elseif {$ctags ne $dtags} {
+ set dtags [combine_dtags $dtags $ctags]
+ }
+ set cheads $desc_heads($child)
+ if {$dheads eq {}} {
+ set dheads $cheads
+ } elseif {$cheads ne $dheads} {
+ set dheads [lsort -unique [concat $dheads $cheads]]
+ }
+ }
+ set desc_tags($id) $dtags
+ if {[info exists idtags($id)]} {
+ set adt $dtags
+ foreach tag $dtags {
+ set adt [concat $adt $alldtags($tag)]
+ }
+ set adt [lsort -unique $adt]
+ set alldtags($id) $adt
+ foreach tag $adt {
+ set tagisdesc($id,$tag) -1
+ set tagisdesc($tag,$id) 1
+ }
+ }
+ if {[info exists idheads($id)]} {
+ lappend dheads $id
+ }
+ set desc_heads($id) $dheads
+ if {[clock clicks -milliseconds] - $allcstart >= 50} {
+ fileevent $fd readable {}
+ after idle restartgetall $fd
+ return
+ }
+ }
+ if {[eof $fd]} {
+ after idle restartatags [llength $allids]
+ if {[catch {close $fd} err]} {
+ error_popup "Error reading full commit graph: $err.\n\
+ Results may be incomplete."
+ }
}
- return [list $x $y $z]
+}
+
+# walk backward through the tree and compute nearest tagged ancestors
+proc restartatags {i} {
+ global allids allparents idtags anc_tags t0
+
+ set t0 [clock clicks -milliseconds]
+ while {[incr i -1] >= 0} {
+ set id [lindex $allids $i]
+ set atags {}
+ foreach p $allparents($id) {
+ if {[info exists idtags($p)]} {
+ set ptags [list $p]
+ } else {
+ set ptags $anc_tags($p)
+ }
+ if {$atags eq {}} {
+ set atags $ptags
+ } elseif {$ptags ne $atags} {
+ set atags [combine_atags $atags $ptags]
+ }
+ }
+ set anc_tags($id) $atags
+ if {[clock clicks -milliseconds] - $t0 >= 50} {
+ after idle restartatags $i
+ return
+ }
+ }
+ set allcommits "done"
+ notbusy allcommits
+ dispneartags
}
proc rereadrefs {} {
}
}
+proc listrefs {id} {
+ global idtags idheads idotherrefs
+
+ set x {}
+ if {[info exists idtags($id)]} {
+ set x $idtags($id)
+ }
+ set y {}
+ if {[info exists idheads($id)]} {
+ set y $idheads($id)
+ }
+ set z {}
+ if {[info exists idotherrefs($id)]} {
+ set z $idotherrefs($id)
+ }
+ return [list $x $y $z]
+}
+
proc showtag {tag isnew} {
global ctext tagcontents tagids linknum
addtohistory [list showtag $tag 0]
}
$ctext conf -state normal
- $ctext delete 0.0 end
+ clear_ctext
set linknum 0
if {[info exists tagcontents($tag)]} {
set text $tagcontents($tag)
} else {
set text "Tag: $tag\nId: $tagids($tag)"
}
- appendwithlinks $text
+ appendwithlinks $text {}
$ctext conf -state disabled
init_flist {}
}
}
proc doprefs {} {
- global maxwidth maxgraphpct diffopts findmergefiles
- global oldprefs prefstop
+ global maxwidth maxgraphpct diffopts
+ global oldprefs prefstop showneartags
set top .gitkprefs
set prefstop $top
raise $top
return
}
- foreach v {maxwidth maxgraphpct diffopts findmergefiles} {
+ foreach v {maxwidth maxgraphpct diffopts showneartags} {
set oldprefs($v) [set $v]
}
toplevel $top
-font optionfont
spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct
grid x $top.maxpctl $top.maxpct -sticky w
- checkbutton $top.findm -variable findmergefiles
- label $top.findml -text "Include merges for \"Find\" in \"Files\"" \
- -font optionfont
- grid $top.findm $top.findml - -sticky w
label $top.ddisp -text "Diff display options"
grid $top.ddisp - -sticky w -pady 10
label $top.diffoptl -text "Options for diff program" \
-font optionfont
entry $top.diffopt -width 20 -textvariable diffopts
grid x $top.diffoptl $top.diffopt -sticky w
+ frame $top.ntag
+ label $top.ntag.l -text "Display nearby tags" -font optionfont
+ checkbutton $top.ntag.b -variable showneartags
+ pack $top.ntag.b $top.ntag.l -side left
+ grid x $top.ntag -sticky w
frame $top.buts
button $top.buts.ok -text "OK" -command prefsok
button $top.buts.can -text "Cancel" -command prefscan
}
proc prefscan {} {
- global maxwidth maxgraphpct diffopts findmergefiles
- global oldprefs prefstop
+ global maxwidth maxgraphpct diffopts
+ global oldprefs prefstop showneartags
- foreach v {maxwidth maxgraphpct diffopts findmergefiles} {
+ foreach v {maxwidth maxgraphpct diffopts showneartags} {
set $v $oldprefs($v)
}
catch {destroy $prefstop}
proc prefsok {} {
global maxwidth maxgraphpct
- global oldprefs prefstop
+ global oldprefs prefstop showneartags
catch {destroy $prefstop}
unset prefstop
if {$maxwidth != $oldprefs(maxwidth)
|| $maxgraphpct != $oldprefs(maxgraphpct)} {
redisplay
+ } elseif {$showneartags != $oldprefs(showneartags)} {
+ reselectline
}
}
# defaults...
set datemode 0
set diffopts "-U 5 -p"
-set wrcomcmd "git-diff-tree --stdin -p --pretty"
+set wrcomcmd "git diff-tree --stdin -p --pretty"
set gitencoding {}
catch {
- set gitencoding [exec git-repo-config --get i18n.commitencoding]
+ set gitencoding [exec git repo-config --get i18n.commitencoding]
}
if {$gitencoding == ""} {
set gitencoding "utf-8"
set uparrowlen 7
set downarrowlen 7
set mingaplen 30
-set flistmode "flat"
set cmitmode "patch"
+set wrapcomment "none"
+set showneartags 1
set colors {green red blue magenta darkgrey brown orange}
# check that we can find a .git directory somewhere...
set gitdir [gitdir]
if {![file isdirectory $gitdir]} {
- show_error . "Cannot find the git directory \"$gitdir\"."
+ show_error {} . "Cannot find the git directory \"$gitdir\"."
exit 1
}
set revtreeargs [lrange $revtreeargs 0 [expr {$i - 1}]]
} elseif {$revtreeargs ne {}} {
if {[catch {
- set f [eval exec git-rev-parse --no-revs --no-flags $revtreeargs]
+ set f [eval exec git rev-parse --no-revs --no-flags $revtreeargs]
set cmdline_files [split $f "\n"]
set n [llength $cmdline_files]
set revtreeargs [lrange $revtreeargs 0 end-$n]
# so look for "fatal:".
set i [string first "fatal:" $err]
if {$i > 0} {
- set err [string range [expr {$i + 6}] end]
+ set err [string range $err [expr {$i + 6}] end]
}
- show_error . "Bad arguments to gitk:\n$err"
+ show_error {} . "Bad arguments to gitk:\n$err"
exit 1
}
}
set history {}
set historyindex 0
+set fh_serial 0
+set nhl_names {}
+set highlight_paths {}
+set searchdirn -forwards
+set boldrows {}
+set boldnamerows {}
set optim_delay 16
set nextviewnum 1
set curview 0
set selectedview 0
-set selectedhlview {}
+set selectedhlview None
set viewfiles(0) {}
set viewperm(0) 0
set viewargs(0) {}
--- /dev/null
+GIT web Interface
+
+The one working on:
+ http://www.kernel.org/git/
+
+From the git version 1.4.0 gitweb is bundled with git.
+
+Any comment/question/concern to:
+ Kay Sievers <kay.sievers@vrfy.org>
--- /dev/null
+#!/usr/bin/perl
+
+# gitweb - simple web interface to track changes in git repositories
+#
+# (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
+# (C) 2005, Christian Gierke
+#
+# This program is licensed under the GPLv2
+
+use strict;
+use warnings;
+use CGI qw(:standard :escapeHTML -nosticky);
+use CGI::Util qw(unescape);
+use CGI::Carp qw(fatalsToBrowser);
+use Encode;
+use Fcntl ':mode';
+binmode STDOUT, ':utf8';
+
+my $cgi = new CGI;
+my $version = "267";
+my $my_url = $cgi->url();
+my $my_uri = $cgi->url(-absolute => 1);
+my $rss_link = "";
+
+# absolute fs-path which will be prepended to the project path
+#my $projectroot = "/pub/scm";
+my $projectroot = "/home/kay/public_html/pub/scm";
+
+# location of the git-core binaries
+my $gitbin = "/usr/bin";
+
+# location for temporary files needed for diffs
+my $git_temp = "/tmp/gitweb";
+
+# target of the home link on top of all pages
+my $home_link = $my_uri;
+
+# html text to include at home page
+my $home_text = "indextext.html";
+
+# source of projects list
+#my $projects_list = $projectroot;
+my $projects_list = "index/index.aux";
+
+# input validation and dispatch
+my $action = $cgi->param('a');
+if (defined $action) {
+ if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
+ undef $action;
+ die_error(undef, "Invalid action parameter.");
+ }
+ if ($action eq "git-logo.png") {
+ git_logo();
+ exit;
+ } elsif ($action eq "opml") {
+ git_opml();
+ exit;
+ }
+}
+
+my $order = $cgi->param('o');
+if (defined $order) {
+ if ($order =~ m/[^0-9a-zA-Z_]/) {
+ undef $order;
+ die_error(undef, "Invalid order parameter.");
+ }
+}
+
+my $project = $cgi->param('p');
+if (defined $project) {
+ $project = validate_input($project);
+ if (!defined($project)) {
+ die_error(undef, "Invalid project parameter.");
+ }
+ if (!(-d "$projectroot/$project")) {
+ undef $project;
+ die_error(undef, "No such directory.");
+ }
+ if (!(-e "$projectroot/$project/HEAD")) {
+ undef $project;
+ die_error(undef, "No such project.");
+ }
+ $rss_link = "<link rel=\"alternate\" title=\"" . esc_param($project) . " log\" href=\"" .
+ "$my_uri?" . esc_param("p=$project;a=rss") . "\" type=\"application/rss+xml\"/>";
+ $ENV{'GIT_DIR'} = "$projectroot/$project";
+} else {
+ git_project_list();
+ exit;
+}
+
+my $file_name = $cgi->param('f');
+if (defined $file_name) {
+ $file_name = validate_input($file_name);
+ if (!defined($file_name)) {
+ die_error(undef, "Invalid file parameter.");
+ }
+}
+
+my $hash = $cgi->param('h');
+if (defined $hash) {
+ $hash = validate_input($hash);
+ if (!defined($hash)) {
+ die_error(undef, "Invalid hash parameter.");
+ }
+}
+
+my $hash_parent = $cgi->param('hp');
+if (defined $hash_parent) {
+ $hash_parent = validate_input($hash_parent);
+ if (!defined($hash_parent)) {
+ die_error(undef, "Invalid hash parent parameter.");
+ }
+}
+
+my $hash_base = $cgi->param('hb');
+if (defined $hash_base) {
+ $hash_base = validate_input($hash_base);
+ if (!defined($hash_base)) {
+ die_error(undef, "Invalid hash base parameter.");
+ }
+}
+
+my $page = $cgi->param('pg');
+if (defined $page) {
+ if ($page =~ m/[^0-9]$/) {
+ undef $page;
+ die_error(undef, "Invalid page parameter.");
+ }
+}
+
+my $searchtext = $cgi->param('s');
+if (defined $searchtext) {
+ if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
+ undef $searchtext;
+ die_error(undef, "Invalid search parameter.");
+ }
+ $searchtext = quotemeta $searchtext;
+}
+
+sub validate_input {
+ my $input = shift;
+
+ if ($input =~ m/^[0-9a-fA-F]{40}$/) {
+ return $input;
+ }
+ if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
+ return undef;
+ }
+ if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\-\+\#\~\%]/) {
+ return undef;
+ }
+ return $input;
+}
+
+if (!defined $action || $action eq "summary") {
+ git_summary();
+ exit;
+} elsif ($action eq "heads") {
+ git_heads();
+ exit;
+} elsif ($action eq "tags") {
+ git_tags();
+ exit;
+} elsif ($action eq "blob") {
+ git_blob();
+ exit;
+} elsif ($action eq "blob_plain") {
+ git_blob_plain();
+ exit;
+} elsif ($action eq "tree") {
+ git_tree();
+ exit;
+} elsif ($action eq "rss") {
+ git_rss();
+ exit;
+} elsif ($action eq "commit") {
+ git_commit();
+ exit;
+} elsif ($action eq "log") {
+ git_log();
+ exit;
+} elsif ($action eq "blobdiff") {
+ git_blobdiff();
+ exit;
+} elsif ($action eq "blobdiff_plain") {
+ git_blobdiff_plain();
+ exit;
+} elsif ($action eq "commitdiff") {
+ git_commitdiff();
+ exit;
+} elsif ($action eq "commitdiff_plain") {
+ git_commitdiff_plain();
+ exit;
+} elsif ($action eq "history") {
+ git_history();
+ exit;
+} elsif ($action eq "search") {
+ git_search();
+ exit;
+} elsif ($action eq "shortlog") {
+ git_shortlog();
+ exit;
+} elsif ($action eq "tag") {
+ git_tag();
+ exit;
+} elsif ($action eq "blame") {
+ git_blame();
+ exit;
+} else {
+ undef $action;
+ die_error(undef, "Unknown action.");
+ exit;
+}
+
+# quote unsafe chars, but keep the slash, even when it's not
+# correct, but quoted slashes look too horrible in bookmarks
+sub esc_param {
+ my $str = shift;
+ $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
+ $str =~ s/\+/%2B/g;
+ $str =~ s/ /\+/g;
+ return $str;
+}
+
+# replace invalid utf8 character with SUBSTITUTION sequence
+sub esc_html {
+ my $str = shift;
+ $str = decode("utf8", $str, Encode::FB_DEFAULT);
+ $str = escapeHTML($str);
+ return $str;
+}
+
+# git may return quoted and escaped filenames
+sub unquote {
+ my $str = shift;
+ if ($str =~ m/^"(.*)"$/) {
+ $str = $1;
+ $str =~ s/\\([0-7]{1,3})/chr(oct($1))/eg;
+ }
+ return $str;
+}
+
+sub git_header_html {
+ my $status = shift || "200 OK";
+ my $expires = shift;
+
+ my $title = "git";
+ if (defined $project) {
+ $title .= " - $project";
+ if (defined $action) {
+ $title .= "/$action";
+ }
+ }
+ print $cgi->header(-type=>'text/html', -charset => 'utf-8', -status=> $status, -expires => $expires);
+ print <<EOF;
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+<!-- git web interface v$version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
+<meta name="robots" content="index, nofollow"/>
+<title>$title</title>
+$rss_link
+<style type="text/css">
+body {
+ font-family: sans-serif; font-size: 12px; border:solid #d9d8d1; border-width:1px;
+ margin:10px; background-color:#ffffff; color:#000000;
+}
+a { color:#0000cc; }
+a:hover, a:visited, a:active { color:#880000; }
+div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
+div.page_header a:visited, a.header { color:#0000cc; }
+div.page_header a:hover { color:#880000; }
+div.page_nav { padding:8px; }
+div.page_nav a:visited { color:#0000cc; }
+div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
+div.page_footer { height:17px; padding:4px 8px; background-color: #d9d8d1; }
+div.page_footer_text { float:left; color:#555555; font-style:italic; }
+div.page_body { padding:8px; }
+div.title, a.title {
+ display:block; padding:6px 8px;
+ font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
+}
+a.title:hover { background-color: #d9d8d1; }
+div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
+div.log_body { padding:8px 8px 8px 150px; }
+span.age { position:relative; float:left; width:142px; font-style:italic; }
+div.log_link {
+ padding:0px 8px;
+ font-size:10px; font-family:sans-serif; font-style:normal;
+ position:relative; float:left; width:136px;
+}
+div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
+a.list { text-decoration:none; color:#000000; }
+a.list:hover { text-decoration:underline; color:#880000; }
+a.text { text-decoration:none; color:#0000cc; }
+a.text:visited { text-decoration:none; color:#880000; }
+a.text:hover { text-decoration:underline; color:#880000; }
+table { padding:8px 4px; }
+th { padding:2px 5px; font-size:12px; text-align:left; }
+tr.light:hover { background-color:#edece6; }
+tr.dark { background-color:#f6f6f0; }
+tr.dark:hover { background-color:#edece6; }
+td { padding:2px 5px; font-size:12px; vertical-align:top; }
+td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
+div.pre { font-family:monospace; font-size:12px; white-space:pre; }
+div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
+div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
+div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
+a.linenr { color:#999999; text-decoration:none }
+a.rss_logo {
+ float:right; padding:3px 0px; width:35px; line-height:10px;
+ border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
+ color:#ffffff; background-color:#ff6600;
+ font-weight:bold; font-family:sans-serif; font-size:10px;
+ text-align:center; text-decoration:none;
+}
+a.rss_logo:hover { background-color:#ee5500; }
+span.tag {
+ padding:0px 4px; font-size:10px; font-weight:normal;
+ background-color:#ffffaa; border:1px solid; border-color:#ffffcc #ffee00 #ffee00 #ffffcc;
+}
+</style>
+</head>
+<body>
+EOF
+ print "<div class=\"page_header\">\n" .
+ "<a href=\"http://www.kernel.org/pub/software/scm/git/docs/\" title=\"git documentation\">" .
+ "<img src=\"$my_uri?" . esc_param("a=git-logo.png") . "\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" .
+ "</a>\n";
+ print $cgi->a({-href => esc_param($home_link)}, "projects") . " / ";
+ if (defined $project) {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, esc_html($project));
+ if (defined $action) {
+ print " / $action";
+ }
+ print "\n";
+ if (!defined $searchtext) {
+ $searchtext = "";
+ }
+ my $search_hash;
+ if (defined $hash) {
+ $search_hash = $hash;
+ } else {
+ $search_hash = "HEAD";
+ }
+ $cgi->param("a", "search");
+ $cgi->param("h", $search_hash);
+ print $cgi->startform(-method => "get", -action => $my_uri) .
+ "<div class=\"search\">\n" .
+ $cgi->hidden(-name => "p") . "\n" .
+ $cgi->hidden(-name => "a") . "\n" .
+ $cgi->hidden(-name => "h") . "\n" .
+ $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
+ "</div>" .
+ $cgi->end_form() . "\n";
+ }
+ print "</div>\n";
+}
+
+sub git_footer_html {
+ print "<div class=\"page_footer\">\n";
+ if (defined $project) {
+ my $descr = git_read_description($project);
+ if (defined $descr) {
+ print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
+ }
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=rss"), -class => "rss_logo"}, "RSS") . "\n";
+ } else {
+ print $cgi->a({-href => "$my_uri?" . esc_param("a=opml"), -class => "rss_logo"}, "OPML") . "\n";
+ }
+ print "</div>\n" .
+ "</body>\n" .
+ "</html>";
+}
+
+sub die_error {
+ my $status = shift || "403 Forbidden";
+ my $error = shift || "Malformed query, file missing or permission denied";
+
+ git_header_html($status);
+ print "<div class=\"page_body\">\n" .
+ "<br/><br/>\n" .
+ "$status - $error\n" .
+ "<br/>\n" .
+ "</div>\n";
+ git_footer_html();
+ exit;
+}
+
+sub git_get_type {
+ my $hash = shift;
+
+ open my $fd, "-|", "$gitbin/git-cat-file -t $hash" or return;
+ my $type = <$fd>;
+ close $fd or return;
+ chomp $type;
+ return $type;
+}
+
+sub git_read_head {
+ my $project = shift;
+ my $oENV = $ENV{'GIT_DIR'};
+ my $retval = undef;
+ $ENV{'GIT_DIR'} = "$projectroot/$project";
+ if (open my $fd, "-|", "$gitbin/git-rev-parse", "--verify", "HEAD") {
+ my $head = <$fd>;
+ close $fd;
+ if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
+ $retval = $1;
+ }
+ }
+ if (defined $oENV) {
+ $ENV{'GIT_DIR'} = $oENV;
+ }
+ return $retval;
+}
+
+sub git_read_hash {
+ my $path = shift;
+
+ open my $fd, "$projectroot/$path" or return undef;
+ my $head = <$fd>;
+ close $fd;
+ chomp $head;
+ if ($head =~ m/^[0-9a-fA-F]{40}$/) {
+ return $head;
+ }
+}
+
+sub git_read_description {
+ my $path = shift;
+
+ open my $fd, "$projectroot/$path/description" or return undef;
+ my $descr = <$fd>;
+ close $fd;
+ chomp $descr;
+ return $descr;
+}
+
+sub git_read_tag {
+ my $tag_id = shift;
+ my %tag;
+ my @comment;
+
+ open my $fd, "-|", "$gitbin/git-cat-file tag $tag_id" or return;
+ $tag{'id'} = $tag_id;
+ while (my $line = <$fd>) {
+ chomp $line;
+ if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
+ $tag{'object'} = $1;
+ } elsif ($line =~ m/^type (.+)$/) {
+ $tag{'type'} = $1;
+ } elsif ($line =~ m/^tag (.+)$/) {
+ $tag{'name'} = $1;
+ } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
+ $tag{'author'} = $1;
+ $tag{'epoch'} = $2;
+ $tag{'tz'} = $3;
+ } elsif ($line =~ m/--BEGIN/) {
+ push @comment, $line;
+ last;
+ } elsif ($line eq "") {
+ last;
+ }
+ }
+ push @comment, <$fd>;
+ $tag{'comment'} = \@comment;
+ close $fd or return;
+ if (!defined $tag{'name'}) {
+ return
+ };
+ return %tag
+}
+
+sub age_string {
+ my $age = shift;
+ my $age_str;
+
+ if ($age > 60*60*24*365*2) {
+ $age_str = (int $age/60/60/24/365);
+ $age_str .= " years ago";
+ } elsif ($age > 60*60*24*(365/12)*2) {
+ $age_str = int $age/60/60/24/(365/12);
+ $age_str .= " months ago";
+ } elsif ($age > 60*60*24*7*2) {
+ $age_str = int $age/60/60/24/7;
+ $age_str .= " weeks ago";
+ } elsif ($age > 60*60*24*2) {
+ $age_str = int $age/60/60/24;
+ $age_str .= " days ago";
+ } elsif ($age > 60*60*2) {
+ $age_str = int $age/60/60;
+ $age_str .= " hours ago";
+ } elsif ($age > 60*2) {
+ $age_str = int $age/60;
+ $age_str .= " min ago";
+ } elsif ($age > 2) {
+ $age_str = int $age;
+ $age_str .= " sec ago";
+ } else {
+ $age_str .= " right now";
+ }
+ return $age_str;
+}
+
+sub git_read_commit {
+ my $commit_id = shift;
+ my $commit_text = shift;
+
+ my @commit_lines;
+ my %co;
+
+ if (defined $commit_text) {
+ @commit_lines = @$commit_text;
+ } else {
+ $/ = "\0";
+ open my $fd, "-|", "$gitbin/git-rev-list --header --parents --max-count=1 $commit_id" or return;
+ @commit_lines = split '\n', <$fd>;
+ close $fd or return;
+ $/ = "\n";
+ pop @commit_lines;
+ }
+ my $header = shift @commit_lines;
+ if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
+ return;
+ }
+ ($co{'id'}, my @parents) = split ' ', $header;
+ $co{'parents'} = \@parents;
+ $co{'parent'} = $parents[0];
+ while (my $line = shift @commit_lines) {
+ last if $line eq "\n";
+ if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
+ $co{'tree'} = $1;
+ } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
+ $co{'author'} = $1;
+ $co{'author_epoch'} = $2;
+ $co{'author_tz'} = $3;
+ if ($co{'author'} =~ m/^([^<]+) </) {
+ $co{'author_name'} = $1;
+ } else {
+ $co{'author_name'} = $co{'author'};
+ }
+ } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
+ $co{'committer'} = $1;
+ $co{'committer_epoch'} = $2;
+ $co{'committer_tz'} = $3;
+ $co{'committer_name'} = $co{'committer'};
+ $co{'committer_name'} =~ s/ <.*//;
+ }
+ }
+ if (!defined $co{'tree'}) {
+ return;
+ };
+
+ foreach my $title (@commit_lines) {
+ $title =~ s/^ //;
+ if ($title ne "") {
+ $co{'title'} = chop_str($title, 80, 5);
+ # remove leading stuff of merges to make the interesting part visible
+ if (length($title) > 50) {
+ $title =~ s/^Automatic //;
+ $title =~ s/^merge (of|with) /Merge ... /i;
+ if (length($title) > 50) {
+ $title =~ s/(http|rsync):\/\///;
+ }
+ if (length($title) > 50) {
+ $title =~ s/(master|www|rsync)\.//;
+ }
+ if (length($title) > 50) {
+ $title =~ s/kernel.org:?//;
+ }
+ if (length($title) > 50) {
+ $title =~ s/\/pub\/scm//;
+ }
+ }
+ $co{'title_short'} = chop_str($title, 50, 5);
+ last;
+ }
+ }
+ # remove added spaces
+ foreach my $line (@commit_lines) {
+ $line =~ s/^ //;
+ }
+ $co{'comment'} = \@commit_lines;
+
+ my $age = time - $co{'committer_epoch'};
+ $co{'age'} = $age;
+ $co{'age_string'} = age_string($age);
+ my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
+ if ($age > 60*60*24*7*2) {
+ $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
+ $co{'age_string_age'} = $co{'age_string'};
+ } else {
+ $co{'age_string_date'} = $co{'age_string'};
+ $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
+ }
+ return %co;
+}
+
+sub git_diff_print {
+ my $from = shift;
+ my $from_name = shift;
+ my $to = shift;
+ my $to_name = shift;
+ my $format = shift || "html";
+
+ my $from_tmp = "/dev/null";
+ my $to_tmp = "/dev/null";
+ my $pid = $$;
+
+ # create tmp from-file
+ if (defined $from) {
+ $from_tmp = "$git_temp/gitweb_" . $$ . "_from";
+ open my $fd2, "> $from_tmp";
+ open my $fd, "-|", "$gitbin/git-cat-file blob $from";
+ my @file = <$fd>;
+ print $fd2 @file;
+ close $fd2;
+ close $fd;
+ }
+
+ # create tmp to-file
+ if (defined $to) {
+ $to_tmp = "$git_temp/gitweb_" . $$ . "_to";
+ open my $fd2, "> $to_tmp";
+ open my $fd, "-|", "$gitbin/git-cat-file blob $to";
+ my @file = <$fd>;
+ print $fd2 @file;
+ close $fd2;
+ close $fd;
+ }
+
+ open my $fd, "-|", "/usr/bin/diff -u -p -L \'$from_name\' -L \'$to_name\' $from_tmp $to_tmp";
+ if ($format eq "plain") {
+ undef $/;
+ print <$fd>;
+ $/ = "\n";
+ } else {
+ while (my $line = <$fd>) {
+ chomp($line);
+ my $char = substr($line, 0, 1);
+ my $color = "";
+ if ($char eq '+') {
+ $color = " style=\"color:#008800;\"";
+ } elsif ($char eq "-") {
+ $color = " style=\"color:#cc0000;\"";
+ } elsif ($char eq "@") {
+ $color = " style=\"color:#990099;\"";
+ } elsif ($char eq "\\") {
+ # skip errors
+ next;
+ }
+ while ((my $pos = index($line, "\t")) != -1) {
+ if (my $count = (8 - (($pos-1) % 8))) {
+ my $spaces = ' ' x $count;
+ $line =~ s/\t/$spaces/;
+ }
+ }
+ print "<div class=\"pre\"$color>" . esc_html($line) . "</div>\n";
+ }
+ }
+ close $fd;
+
+ if (defined $from) {
+ unlink($from_tmp);
+ }
+ if (defined $to) {
+ unlink($to_tmp);
+ }
+}
+
+sub mode_str {
+ my $mode = oct shift;
+
+ if (S_ISDIR($mode & S_IFMT)) {
+ return 'drwxr-xr-x';
+ } elsif (S_ISLNK($mode)) {
+ return 'lrwxrwxrwx';
+ } elsif (S_ISREG($mode)) {
+ # git cares only about the executable bit
+ if ($mode & S_IXUSR) {
+ return '-rwxr-xr-x';
+ } else {
+ return '-rw-r--r--';
+ };
+ } else {
+ return '----------';
+ }
+}
+
+sub chop_str {
+ my $str = shift;
+ my $len = shift;
+ my $add_len = shift || 10;
+
+ # allow only $len chars, but don't cut a word if it would fit in $add_len
+ # if it doesn't fit, cut it if it's still longer than the dots we would add
+ $str =~ m/^(.{0,$len}[^ \/\-_:\.@]{0,$add_len})(.*)/;
+ my $body = $1;
+ my $tail = $2;
+ if (length($tail) > 4) {
+ $tail = " ...";
+ }
+ return "$body$tail";
+}
+
+sub file_type {
+ my $mode = oct shift;
+
+ if (S_ISDIR($mode & S_IFMT)) {
+ return "directory";
+ } elsif (S_ISLNK($mode)) {
+ return "symlink";
+ } elsif (S_ISREG($mode)) {
+ return "file";
+ } else {
+ return "unknown";
+ }
+}
+
+sub format_log_line_html {
+ my $line = shift;
+
+ $line = esc_html($line);
+ $line =~ s/ / /g;
+ if ($line =~ m/([0-9a-fA-F]{40})/) {
+ my $hash_text = $1;
+ if (git_get_type($hash_text) eq "commit") {
+ my $link = $cgi->a({-class => "text", -href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_text")}, $hash_text);
+ $line =~ s/$hash_text/$link/;
+ }
+ }
+ return $line;
+}
+
+sub date_str {
+ my $epoch = shift;
+ my $tz = shift || "-0000";
+
+ my %date;
+ my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
+ my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
+ my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
+ $date{'hour'} = $hour;
+ $date{'minute'} = $min;
+ $date{'mday'} = $mday;
+ $date{'day'} = $days[$wday];
+ $date{'month'} = $months[$mon];
+ $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000", $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
+ $date{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min;
+
+ $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
+ my $local = $epoch + ((int $1 + ($2/60)) * 3600);
+ ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
+ $date{'hour_local'} = $hour;
+ $date{'minute_local'} = $min;
+ $date{'tz_local'} = $tz;
+ return %date;
+}
+
+# git-logo (cached in browser for one day)
+sub git_logo {
+ binmode STDOUT, ':raw';
+ print $cgi->header(-type => 'image/png', -expires => '+1d');
+ # cat git-logo.png | hexdump -e '16/1 " %02x" "\n"' | sed 's/ /\\x/g'
+ print "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" .
+ "\x00\x00\x00\x48\x00\x00\x00\x1b\x04\x03\x00\x00\x00\x2d\xd9\xd4" .
+ "\x2d\x00\x00\x00\x18\x50\x4c\x54\x45\xff\xff\xff\x60\x60\x5d\xb0" .
+ "\xaf\xaa\x00\x80\x00\xce\xcd\xc7\xc0\x00\x00\xe8\xe8\xe6\xf7\xf7" .
+ "\xf6\x95\x0c\xa7\x47\x00\x00\x00\x73\x49\x44\x41\x54\x28\xcf\x63" .
+ "\x48\x67\x20\x04\x4a\x5c\x18\x0a\x08\x2a\x62\x53\x61\x20\x02\x08" .
+ "\x0d\x69\x45\xac\xa1\xa1\x01\x30\x0c\x93\x60\x36\x26\x52\x91\xb1" .
+ "\x01\x11\xd6\xe1\x55\x64\x6c\x6c\xcc\x6c\x6c\x0c\xa2\x0c\x70\x2a" .
+ "\x62\x06\x2a\xc1\x62\x1d\xb3\x01\x02\x53\xa4\x08\xe8\x00\x03\x18" .
+ "\x26\x56\x11\xd4\xe1\x20\x97\x1b\xe0\xb4\x0e\x35\x24\x71\x29\x82" .
+ "\x99\x30\xb8\x93\x0a\x11\xb9\x45\x88\xc1\x8d\xa0\xa2\x44\x21\x06" .
+ "\x27\x41\x82\x40\x85\xc1\x45\x89\x20\x70\x01\x00\xa4\x3d\x21\xc5" .
+ "\x12\x1c\x9a\xfe\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82";
+}
+
+sub get_file_owner {
+ my $path = shift;
+
+ my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
+ my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
+ if (!defined $gcos) {
+ return undef;
+ }
+ my $owner = $gcos;
+ $owner =~ s/[,;].*$//;
+ return decode("utf8", $owner, Encode::FB_DEFAULT);
+}
+
+sub git_read_projects {
+ my @list;
+
+ if (-d $projects_list) {
+ # search in directory
+ my $dir = $projects_list;
+ opendir my $dh, $dir or return undef;
+ while (my $dir = readdir($dh)) {
+ if (-e "$projectroot/$dir/HEAD") {
+ my $pr = {
+ path => $dir,
+ };
+ push @list, $pr
+ }
+ }
+ closedir($dh);
+ } elsif (-f $projects_list) {
+ # read from file(url-encoded):
+ # 'git%2Fgit.git Linus+Torvalds'
+ # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
+ # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
+ open my $fd , $projects_list or return undef;
+ while (my $line = <$fd>) {
+ chomp $line;
+ my ($path, $owner) = split ' ', $line;
+ $path = unescape($path);
+ $owner = unescape($owner);
+ if (!defined $path) {
+ next;
+ }
+ if (-e "$projectroot/$path/HEAD") {
+ my $pr = {
+ path => $path,
+ owner => decode("utf8", $owner, Encode::FB_DEFAULT),
+ };
+ push @list, $pr
+ }
+ }
+ close $fd;
+ }
+ @list = sort {$a->{'path'} cmp $b->{'path'}} @list;
+ return @list;
+}
+
+sub git_get_project_config {
+ my $key = shift;
+
+ return unless ($key);
+ $key =~ s/^gitweb\.//;
+ return if ($key =~ m/\W/);
+
+ my $val = qx(git-repo-config --get gitweb.$key);
+ return ($val);
+}
+
+sub git_get_project_config_bool {
+ my $val = git_get_project_config (@_);
+ if ($val and $val =~ m/true|yes|on/) {
+ return (1);
+ }
+ return; # implicit false
+}
+
+sub git_project_list {
+ my @list = git_read_projects();
+ my @projects;
+ if (!@list) {
+ die_error(undef, "No project found.");
+ }
+ foreach my $pr (@list) {
+ my $head = git_read_head($pr->{'path'});
+ if (!defined $head) {
+ next;
+ }
+ $ENV{'GIT_DIR'} = "$projectroot/$pr->{'path'}";
+ my %co = git_read_commit($head);
+ if (!%co) {
+ next;
+ }
+ $pr->{'commit'} = \%co;
+ if (!defined $pr->{'descr'}) {
+ my $descr = git_read_description($pr->{'path'}) || "";
+ $pr->{'descr'} = chop_str($descr, 25, 5);
+ }
+ if (!defined $pr->{'owner'}) {
+ $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
+ }
+ push @projects, $pr;
+ }
+ git_header_html();
+ if (-f $home_text) {
+ print "<div class=\"index_include\">\n";
+ open (my $fd, $home_text);
+ print <$fd>;
+ close $fd;
+ print "</div>\n";
+ }
+ print "<table cellspacing=\"0\">\n" .
+ "<tr>\n";
+ if (!defined($order) || (defined($order) && ($order eq "project"))) {
+ @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
+ print "<th>Project</th>\n";
+ } else {
+ print "<th>" . $cgi->a({-class => "header", -href => "$my_uri?" . esc_param("o=project")}, "Project") . "</th>\n";
+ }
+ if (defined($order) && ($order eq "descr")) {
+ @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
+ print "<th>Description</th>\n";
+ } else {
+ print "<th>" . $cgi->a({-class => "header", -href => "$my_uri?" . esc_param("o=descr")}, "Description") . "</th>\n";
+ }
+ if (defined($order) && ($order eq "owner")) {
+ @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
+ print "<th>Owner</th>\n";
+ } else {
+ print "<th>" . $cgi->a({-class => "header", -href => "$my_uri?" . esc_param("o=owner")}, "Owner") . "</th>\n";
+ }
+ if (defined($order) && ($order eq "age")) {
+ @projects = sort {$a->{'commit'}{'age'} <=> $b->{'commit'}{'age'}} @projects;
+ print "<th>Last Change</th>\n";
+ } else {
+ print "<th>" . $cgi->a({-class => "header", -href => "$my_uri?" . esc_param("o=age")}, "Last Change") . "</th>\n";
+ }
+ print "<th></th>\n" .
+ "</tr>\n";
+ my $alternate = 0;
+ foreach my $pr (@projects) {
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ print "<td>" . $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=summary"), -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
+ "<td>$pr->{'descr'}</td>\n" .
+ "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
+ my $colored_age;
+ if ($pr->{'commit'}{'age'} < 60*60*2) {
+ $colored_age = "<span style =\"color: #009900;\"><b><i>$pr->{'commit'}{'age_string'}</i></b></span>";
+ } elsif ($pr->{'commit'}{'age'} < 60*60*24*2) {
+ $colored_age = "<span style =\"color: #009900;\"><i>$pr->{'commit'}{'age_string'}</i></span>";
+ } else {
+ $colored_age = "<i>$pr->{'commit'}{'age_string'}</i>";
+ }
+ print "<td>$colored_age</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=log")}, "log") .
+ "</td>\n" .
+ "</tr>\n";
+ }
+ print "</table>\n";
+ git_footer_html();
+}
+
+sub read_info_ref {
+ my $type = shift || "";
+ my %refs;
+ # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
+ # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
+ open my $fd, "$projectroot/$project/info/refs" or return;
+ while (my $line = <$fd>) {
+ chomp($line);
+ if ($line =~ m/^([0-9a-fA-F]{40})\t.*$type\/([^\^]+)/) {
+ if (defined $refs{$1}) {
+ $refs{$1} .= " / $2";
+ } else {
+ $refs{$1} = $2;
+ }
+ }
+ }
+ close $fd or return;
+ return \%refs;
+}
+
+sub git_read_refs {
+ my $ref_dir = shift;
+ my @reflist;
+
+ my @refs;
+ opendir my $dh, "$projectroot/$project/$ref_dir";
+ while (my $dir = readdir($dh)) {
+ if ($dir =~ m/^\./) {
+ next;
+ }
+ if (-d "$projectroot/$project/$ref_dir/$dir") {
+ opendir my $dh2, "$projectroot/$project/$ref_dir/$dir";
+ my @subdirs = grep !m/^\./, readdir $dh2;
+ closedir($dh2);
+ foreach my $subdir (@subdirs) {
+ push @refs, "$dir/$subdir"
+ }
+ next;
+ }
+ push @refs, $dir;
+ }
+ closedir($dh);
+ foreach my $ref_file (@refs) {
+ my $ref_id = git_read_hash("$project/$ref_dir/$ref_file");
+ my $type = git_get_type($ref_id) || next;
+ my %ref_item;
+ my %co;
+ $ref_item{'type'} = $type;
+ $ref_item{'id'} = $ref_id;
+ $ref_item{'epoch'} = 0;
+ $ref_item{'age'} = "unknown";
+ if ($type eq "tag") {
+ my %tag = git_read_tag($ref_id);
+ $ref_item{'comment'} = $tag{'comment'};
+ if ($tag{'type'} eq "commit") {
+ %co = git_read_commit($tag{'object'});
+ $ref_item{'epoch'} = $co{'committer_epoch'};
+ $ref_item{'age'} = $co{'age_string'};
+ } elsif (defined($tag{'epoch'})) {
+ my $age = time - $tag{'epoch'};
+ $ref_item{'epoch'} = $tag{'epoch'};
+ $ref_item{'age'} = age_string($age);
+ }
+ $ref_item{'reftype'} = $tag{'type'};
+ $ref_item{'name'} = $tag{'name'};
+ $ref_item{'refid'} = $tag{'object'};
+ } elsif ($type eq "commit"){
+ %co = git_read_commit($ref_id);
+ $ref_item{'reftype'} = "commit";
+ $ref_item{'name'} = $ref_file;
+ $ref_item{'title'} = $co{'title'};
+ $ref_item{'refid'} = $ref_id;
+ $ref_item{'epoch'} = $co{'committer_epoch'};
+ $ref_item{'age'} = $co{'age_string'};
+ }
+
+ push @reflist, \%ref_item;
+ }
+ # sort tags by age
+ @reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist;
+ return \@reflist;
+}
+
+sub git_summary {
+ my $descr = git_read_description($project) || "none";
+ my $head = git_read_head($project);
+ my %co = git_read_commit($head);
+ my %cd = date_str($co{'committer_epoch'}, $co{'committer_tz'});
+
+ my $owner;
+ if (-f $projects_list) {
+ open (my $fd , $projects_list);
+ while (my $line = <$fd>) {
+ chomp $line;
+ my ($pr, $ow) = split ' ', $line;
+ $pr = unescape($pr);
+ $ow = unescape($ow);
+ if ($pr eq $project) {
+ $owner = decode("utf8", $ow, Encode::FB_DEFAULT);
+ last;
+ }
+ }
+ close $fd;
+ }
+ if (!defined $owner) {
+ $owner = get_file_owner("$projectroot/$project");
+ }
+
+ my $refs = read_info_ref();
+ git_header_html();
+ print "<div class=\"page_nav\">\n" .
+ "summary".
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$head")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$head")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree")}, "tree") .
+ "<br/><br/>\n" .
+ "</div>\n";
+ print "<div class=\"title\"> </div>\n";
+ print "<table cellspacing=\"0\">\n" .
+ "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
+ "<tr><td>owner</td><td>$owner</td></tr>\n" .
+ "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n" .
+ "</table>\n";
+ open my $fd, "-|", "$gitbin/git-rev-list --max-count=17 " . git_read_head($project) or die_error(undef, "Open failed.");
+ my (@revlist) = map { chomp; $_ } <$fd>;
+ close $fd;
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog"), -class => "title"}, "shortlog") .
+ "</div>\n";
+ my $i = 16;
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ foreach my $commit (@revlist) {
+ my %co = git_read_commit($commit);
+ my %ad = date_str($co{'author_epoch'});
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ if ($i-- > 0) {
+ my $ref = "";
+ if (defined $refs->{$commit}) {
+ $ref = " <span class=\"tag\">" . esc_html($refs->{$commit}) . "</span>";
+ }
+ print "<td><i>$co{'age_string'}</i></td>\n" .
+ "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
+ "<td>";
+ if (length($co{'title_short'}) < length($co{'title'})) {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit"), -class => "list", -title => "$co{'title'}"},
+ "<b>" . esc_html($co{'title_short'}) . "$ref</b>");
+ } else {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit"), -class => "list"},
+ "<b>" . esc_html($co{'title'}) . "$ref</b>");
+ }
+ print "</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
+ "</td>\n" .
+ "</tr>";
+ } else {
+ print "<td>" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "...") . "</td>\n" .
+ "</tr>";
+ last;
+ }
+ }
+ print "</table\n>";
+
+ my $taglist = git_read_refs("refs/tags");
+ if (defined @$taglist) {
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tags"), -class => "title"}, "tags") .
+ "</div>\n";
+ my $i = 16;
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ foreach my $entry (@$taglist) {
+ my %tag = %$entry;
+ my $comment_lines = $tag{'comment'};
+ my $comment = shift @$comment_lines;
+ if (defined($comment)) {
+ $comment = chop_str($comment, 30, 5);
+ }
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ if ($i-- > 0) {
+ print "<td><i>$tag{'age'}</i></td>\n" .
+ "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$tag{'reftype'};h=$tag{'refid'}"), -class => "list"},
+ "<b>" . esc_html($tag{'name'}) . "</b>") .
+ "</td>\n" .
+ "<td>";
+ if (defined($comment)) {
+ print $cgi->a({-class => "list", -href => "$my_uri?" . esc_param("p=$project;a=tag;h=$tag{'id'}")}, $comment);
+ }
+ print "</td>\n" .
+ "<td class=\"link\">";
+ if ($tag{'type'} eq "tag") {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tag;h=$tag{'id'}")}, "tag") . " | ";
+ }
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$tag{'reftype'};h=$tag{'refid'}")}, $tag{'reftype'});
+ if ($tag{'reftype'} eq "commit") {
+ print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$tag{'refid'}")}, "log");
+ }
+ print "</td>\n" .
+ "</tr>";
+ } else {
+ print "<td>" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tags")}, "...") . "</td>\n" .
+ "</tr>";
+ last;
+ }
+ }
+ print "</table\n>";
+ }
+
+ my $headlist = git_read_refs("refs/heads");
+ if (defined @$headlist) {
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=heads"), -class => "title"}, "heads") .
+ "</div>\n";
+ my $i = 16;
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ foreach my $entry (@$headlist) {
+ my %tag = %$entry;
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ if ($i-- > 0) {
+ print "<td><i>$tag{'age'}</i></td>\n" .
+ "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$tag{'name'}"), -class => "list"},
+ "<b>" . esc_html($tag{'name'}) . "</b>") .
+ "</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$tag{'name'}")}, "log") .
+ "</td>\n" .
+ "</tr>";
+ } else {
+ print "<td>" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=heads")}, "...") . "</td>\n" .
+ "</tr>";
+ last;
+ }
+ }
+ print "</table\n>";
+ }
+ git_footer_html();
+}
+
+sub git_tag {
+ my $head = git_read_head($project);
+ git_header_html();
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$head")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$head")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;hb=$head")}, "tree") . "<br/>\n" .
+ "<br/>\n" .
+ "</div>\n";
+ my %tag = git_read_tag($hash);
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash"), -class => "title"}, esc_html($tag{'name'})) . "\n" .
+ "</div>\n";
+ print "<div class=\"title_text\">\n" .
+ "<table cellspacing=\"0\">\n" .
+ "<tr>\n" .
+ "<td>object</td>\n" .
+ "<td>" . $cgi->a({-class => "list", -href => "$my_uri?" . esc_param("p=$project;a=$tag{'type'};h=$tag{'object'}")}, $tag{'object'}) . "</td>\n" .
+ "<td class=\"link\">" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$tag{'type'};h=$tag{'object'}")}, $tag{'type'}) . "</td>\n" .
+ "</tr>\n";
+ if (defined($tag{'author'})) {
+ my %ad = date_str($tag{'epoch'}, $tag{'tz'});
+ print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
+ print "<tr><td></td><td>" . $ad{'rfc2822'} . sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) . "</td></tr>\n";
+ }
+ print "</table>\n\n" .
+ "</div>\n";
+ print "<div class=\"page_body\">";
+ my $comment = $tag{'comment'};
+ foreach my $line (@$comment) {
+ print esc_html($line) . "<br/>\n";
+ }
+ print "</div>\n";
+ git_footer_html();
+}
+
+sub git_blame {
+ my $fd;
+ die_error('403 Permission denied', "Permission denied.") if (!git_get_project_config_bool ('blame'));
+ die_error('404 Not Found', "What file will it be, master?") if (!$file_name);
+ $hash_base ||= git_read_head($project);
+ die_error(undef, "Reading commit failed.") unless ($hash_base);
+ my %co = git_read_commit($hash_base)
+ or die_error(undef, "Reading commit failed.");
+ if (!defined $hash) {
+ $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
+ or die_error(undef, "Error lookup file.");
+ }
+ open ($fd, "-|", "$gitbin/git-annotate", '-l', '-t', '-r', $file_name, $hash_base)
+ or die_error(undef, "Open failed.");
+ git_header_html();
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash_base")}, "tree") . "<br/>\n";
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name")}, "blob") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;f=$file_name")}, "head") . "<br/>\n";
+ print "</div>\n".
+ "<div>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base"), -class => "title"}, esc_html($co{'title'})) .
+ "</div>\n";
+ print "<div class=\"page_path\"><b>" . esc_html($file_name) . "</b></div>\n";
+ print "<div class=\"page_body\">\n";
+ print <<HTML;
+<table style="border-collapse: collapse;">
+ <tr>
+ <th>Commit</th>
+ <th>Age</th>
+ <th>Author</th>
+ <th>Line</th>
+ <th>Data</th>
+ </tr>
+HTML
+ my @line_class = (qw(light dark));
+ my $line_class_len = scalar (@line_class);
+ my $line_class_num = $#line_class;
+ while (my $line = <$fd>) {
+ my $long_rev;
+ my $short_rev;
+ my $author;
+ my $time;
+ my $lineno;
+ my $data;
+ my $age;
+ my $age_str;
+ my $age_style;
+
+ chomp $line;
+ $line_class_num = ($line_class_num + 1) % $line_class_len;
+
+ if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) \+\d\d\d\d\t(\d+)\)(.*)$/) {
+ $long_rev = $1;
+ $author = $2;
+ $time = $3;
+ $lineno = $4;
+ $data = $5;
+ } else {
+ print qq( <tr><td colspan="5" style="color: red; background-color: yellow;">Unable to parse: $line</td></tr>\n);
+ next;
+ }
+ $short_rev = substr ($long_rev, 0, 8);
+ $age = time () - $time;
+ $age_str = age_string ($age);
+ $age_str =~ s/ / /g;
+ $age_style = 'font-style: italic;';
+ $age_style .= ' color: #009900; background: transparent;' if ($age < 60*60*24*2);
+ $age_style .= ' font-weight: bold;' if ($age < 60*60*2);
+ $author = esc_html ($author);
+ $author =~ s/ / /g;
+ # escape tabs
+ while ((my $pos = index($data, "\t")) != -1) {
+ if (my $count = (8 - ($pos % 8))) {
+ my $spaces = ' ' x $count;
+ $data =~ s/\t/$spaces/;
+ }
+ }
+ $data = esc_html ($data);
+ $data =~ s/ / /g;
+
+ print <<HTML;
+ <tr class="$line_class[$line_class_num]">
+ <td style="font-family: monospace;"><a href="$my_uri?${\esc_param ("p=$project;a=commit;h=$long_rev")}" class="text">$short_rev..</a></td>
+ <td style="$age_style">$age_str</td>
+ <td>$author</td>
+ <td style="text-align: right;"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td>
+ <td style="font-family: monospace;">$data</td>
+ </tr>
+HTML
+ } # while (my $line = <$fd>)
+ print "</table>\n\n";
+ close $fd or print "Reading blob failed.\n";
+ print "</div>";
+ git_footer_html();
+}
+
+sub git_tags {
+ my $head = git_read_head($project);
+ git_header_html();
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$head")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$head")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;hb=$head")}, "tree") . "<br/>\n" .
+ "<br/>\n" .
+ "</div>\n";
+ my $taglist = git_read_refs("refs/tags");
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary"), -class => "title"}, " ") .
+ "</div>\n";
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ if (defined @$taglist) {
+ foreach my $entry (@$taglist) {
+ my %tag = %$entry;
+ my $comment_lines = $tag{'comment'};
+ my $comment = shift @$comment_lines;
+ if (defined($comment)) {
+ $comment = chop_str($comment, 30, 5);
+ }
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ print "<td><i>$tag{'age'}</i></td>\n" .
+ "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$tag{'reftype'};h=$tag{'refid'}"), -class => "list"},
+ "<b>" . esc_html($tag{'name'}) . "</b>") .
+ "</td>\n" .
+ "<td>";
+ if (defined($comment)) {
+ print $cgi->a({-class => "list", -href => "$my_uri?" . esc_param("p=$project;a=tag;h=$tag{'id'}")}, $comment);
+ }
+ print "</td>\n" .
+ "<td class=\"link\">";
+ if ($tag{'type'} eq "tag") {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tag;h=$tag{'id'}")}, "tag") . " | ";
+ }
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=$tag{'reftype'};h=$tag{'refid'}")}, $tag{'reftype'});
+ if ($tag{'reftype'} eq "commit") {
+ print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$tag{'refid'}")}, "log");
+ }
+ print "</td>\n" .
+ "</tr>";
+ }
+ }
+ print "</table\n>";
+ git_footer_html();
+}
+
+sub git_heads {
+ my $head = git_read_head($project);
+ git_header_html();
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$head")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$head")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;hb=$head")}, "tree") . "<br/>\n" .
+ "<br/>\n" .
+ "</div>\n";
+ my $taglist = git_read_refs("refs/heads");
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary"), -class => "title"}, " ") .
+ "</div>\n";
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ if (defined @$taglist) {
+ foreach my $entry (@$taglist) {
+ my %tag = %$entry;
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ print "<td><i>$tag{'age'}</i></td>\n" .
+ "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$tag{'name'}"), -class => "list"}, "<b>" . esc_html($tag{'name'}) . "</b>") .
+ "</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$tag{'name'}")}, "log") .
+ "</td>\n" .
+ "</tr>";
+ }
+ }
+ print "</table\n>";
+ git_footer_html();
+}
+
+sub git_get_hash_by_path {
+ my $base = shift;
+ my $path = shift || return undef;
+
+ my $tree = $base;
+ my @parts = split '/', $path;
+ while (my $part = shift @parts) {
+ open my $fd, "-|", "$gitbin/git-ls-tree $tree" or die_error(undef, "Open git-ls-tree failed.");
+ my (@entries) = map { chomp; $_ } <$fd>;
+ close $fd or return undef;
+ foreach my $line (@entries) {
+ #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
+ $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
+ my $t_mode = $1;
+ my $t_type = $2;
+ my $t_hash = $3;
+ my $t_name = validate_input(unquote($4));
+ if ($t_name eq $part) {
+ if (!(@parts)) {
+ return $t_hash;
+ }
+ if ($t_type eq "tree") {
+ $tree = $t_hash;
+ }
+ last;
+ }
+ }
+ }
+}
+
+sub git_blob {
+ if (!defined $hash && defined $file_name) {
+ my $base = $hash_base || git_read_head($project);
+ $hash = git_get_hash_by_path($base, $file_name, "blob") || die_error(undef, "Error lookup file.");
+ }
+ my $have_blame = git_get_project_config_bool ('blame');
+ open my $fd, "-|", "$gitbin/git-cat-file blob $hash" or die_error(undef, "Open failed.");
+ git_header_html();
+ if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash_base")}, "tree") . "<br/>\n";
+ if (defined $file_name) {
+ if ($have_blame) {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$hash;hb=$hash_base;f=$file_name")}, "blame") . " | ";
+ }
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash;f=$file_name")}, "plain") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;hb=HEAD;f=$file_name")}, "head") . "<br/>\n";
+ } else {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$hash")}, "plain") . "<br/>\n";
+ }
+ print "</div>\n".
+ "<div>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base"), -class => "title"}, esc_html($co{'title'})) .
+ "</div>\n";
+ } else {
+ print "<div class=\"page_nav\">\n" .
+ "<br/><br/></div>\n" .
+ "<div class=\"title\">$hash</div>\n";
+ }
+ if (defined $file_name) {
+ print "<div class=\"page_path\"><b>" . esc_html($file_name) . "</b></div>\n";
+ }
+ print "<div class=\"page_body\">\n";
+ my $nr;
+ while (my $line = <$fd>) {
+ chomp $line;
+ $nr++;
+ while ((my $pos = index($line, "\t")) != -1) {
+ if (my $count = (8 - ($pos % 8))) {
+ my $spaces = ' ' x $count;
+ $line =~ s/\t/$spaces/;
+ }
+ }
+ printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n", $nr, $nr, $nr, esc_html($line);
+ }
+ close $fd or print "Reading blob failed.\n";
+ print "</div>";
+ git_footer_html();
+}
+
+sub git_blob_plain {
+ my $save_as = "$hash.txt";
+ if (defined $file_name) {
+ $save_as = $file_name;
+ }
+ print $cgi->header(-type => "text/plain", -charset => 'utf-8', '-content-disposition' => "inline; filename=\"$save_as\"");
+ open my $fd, "-|", "$gitbin/git-cat-file blob $hash" or return;
+ undef $/;
+ print <$fd>;
+ $/ = "\n";
+ close $fd;
+}
+
+sub git_tree {
+ if (!defined $hash) {
+ $hash = git_read_head($project);
+ if (defined $file_name) {
+ my $base = $hash_base || $hash;
+ $hash = git_get_hash_by_path($base, $file_name, "tree");
+ }
+ if (!defined $hash_base) {
+ $hash_base = $hash;
+ }
+ }
+ $/ = "\0";
+ open my $fd, "-|", "$gitbin/git-ls-tree -z $hash" or die_error(undef, "Open git-ls-tree failed.");
+ chomp (my (@entries) = <$fd>);
+ close $fd or die_error(undef, "Reading tree failed.");
+ $/ = "\n";
+
+ my $refs = read_info_ref();
+ my $ref = "";
+ if (defined $refs->{$hash_base}) {
+ $ref = " <span class=\"tag\">" . esc_html($refs->{$hash_base}) . "</span>";
+ }
+ git_header_html();
+ my $base_key = "";
+ my $base = "";
+ if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
+ $base_key = ";hb=$hash_base";
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash_base")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$hash_base")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") .
+ " | tree" .
+ "<br/><br/>\n" .
+ "</div>\n";
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base"), -class => "title"}, esc_html($co{'title'}) . $ref) . "\n" .
+ "</div>\n";
+ } else {
+ print "<div class=\"page_nav\">\n";
+ print "<br/><br/></div>\n";
+ print "<div class=\"title\">$hash</div>\n";
+ }
+ if (defined $file_name) {
+ $base = esc_html("$file_name/");
+ print "<div class=\"page_path\"><b>/" . esc_html($file_name) . "</b></div>\n";
+ } else {
+ print "<div class=\"page_path\"><b>/</b></div>\n";
+ }
+ print "<div class=\"page_body\">\n";
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ foreach my $line (@entries) {
+ #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
+ $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
+ my $t_mode = $1;
+ my $t_type = $2;
+ my $t_hash = $3;
+ my $t_name = validate_input($4);
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ print "<td style=\"font-family:monospace\">" . mode_str($t_mode) . "</td>\n";
+ if ($t_type eq "blob") {
+ print "<td class=\"list\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$t_hash$base_key;f=$base$t_name"), -class => "list"}, esc_html($t_name)) .
+ "</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$t_hash$base_key;f=$base$t_name")}, "blob") .
+# " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$t_hash$base_key;f=$base$t_name")}, "blame") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;h=$hash_base;f=$base$t_name")}, "history") .
+ "</td>\n";
+ } elsif ($t_type eq "tree") {
+ print "<td class=\"list\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$t_hash$base_key;f=$base$t_name")}, esc_html($t_name)) .
+ "</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$t_hash$base_key;f=$base$t_name")}, "tree") .
+ "</td>\n";
+ }
+ print "</tr>\n";
+ }
+ print "</table>\n" .
+ "</div>";
+ git_footer_html();
+}
+
+sub git_rss {
+ # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
+ open my $fd, "-|", "$gitbin/git-rev-list --max-count=150 " . git_read_head($project) or die_error(undef, "Open failed.");
+ my (@revlist) = map { chomp; $_ } <$fd>;
+ close $fd or die_error(undef, "Reading rev-list failed.");
+ print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
+ print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
+ "<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n";
+ print "<channel>\n";
+ print "<title>$project</title>\n".
+ "<link>" . esc_html("$my_url?p=$project;a=summary") . "</link>\n".
+ "<description>$project log</description>\n".
+ "<language>en</language>\n";
+
+ for (my $i = 0; $i <= $#revlist; $i++) {
+ my $commit = $revlist[$i];
+ my %co = git_read_commit($commit);
+ # we read 150, we always show 30 and the ones more recent than 48 hours
+ if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) {
+ last;
+ }
+ my %cd = date_str($co{'committer_epoch'});
+ open $fd, "-|", "$gitbin/git-diff-tree -r $co{'parent'} $co{'id'}" or next;
+ my @difftree = map { chomp; $_ } <$fd>;
+ close $fd or next;
+ print "<item>\n" .
+ "<title>" .
+ sprintf("%d %s %02d:%02d", $cd{'mday'}, $cd{'month'}, $cd{'hour'}, $cd{'minute'}) . " - " . esc_html($co{'title'}) .
+ "</title>\n" .
+ "<author>" . esc_html($co{'author'}) . "</author>\n" .
+ "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
+ "<guid isPermaLink=\"true\">" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</guid>\n" .
+ "<link>" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</link>\n" .
+ "<description>" . esc_html($co{'title'}) . "</description>\n" .
+ "<content:encoded>" .
+ "<![CDATA[\n";
+ my $comment = $co{'comment'};
+ foreach my $line (@$comment) {
+ $line = decode("utf8", $line, Encode::FB_DEFAULT);
+ print "$line<br/>\n";
+ }
+ print "<br/>\n";
+ foreach my $line (@difftree) {
+ if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
+ next;
+ }
+ my $file = validate_input(unquote($7));
+ $file = decode("utf8", $file, Encode::FB_DEFAULT);
+ print "$file<br/>\n";
+ }
+ print "]]>\n" .
+ "</content:encoded>\n" .
+ "</item>\n";
+ }
+ print "</channel></rss>";
+}
+
+sub git_opml {
+ my @list = git_read_projects();
+
+ print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
+ print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
+ "<opml version=\"1.0\">\n".
+ "<head>".
+ " <title>Git OPML Export</title>\n".
+ "</head>\n".
+ "<body>\n".
+ "<outline text=\"git RSS feeds\">\n";
+
+ foreach my $pr (@list) {
+ my %proj = %$pr;
+ my $head = git_read_head($proj{'path'});
+ if (!defined $head) {
+ next;
+ }
+ $ENV{'GIT_DIR'} = "$projectroot/$proj{'path'}";
+ my %co = git_read_commit($head);
+ if (!%co) {
+ next;
+ }
+
+ my $path = esc_html(chop_str($proj{'path'}, 25, 5));
+ my $rss = "$my_url?p=$proj{'path'};a=rss";
+ my $html = "$my_url?p=$proj{'path'};a=summary";
+ print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
+ }
+ print "</outline>\n".
+ "</body>\n".
+ "</opml>\n";
+}
+
+sub git_log {
+ my $head = git_read_head($project);
+ if (!defined $hash) {
+ $hash = $head;
+ }
+ if (!defined $page) {
+ $page = 0;
+ }
+ my $refs = read_info_ref();
+ git_header_html();
+ print "<div class=\"page_nav\">\n";
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash")}, "shortlog") .
+ " | log" .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$hash;hb=$hash")}, "tree") . "<br/>\n";
+
+ my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
+ open my $fd, "-|", "$gitbin/git-rev-list $limit $hash" or die_error(undef, "Open failed.");
+ my (@revlist) = map { chomp; $_ } <$fd>;
+ close $fd;
+
+ if ($hash ne $head || $page) {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "HEAD");
+ } else {
+ print "HEAD";
+ }
+ if ($page > 0) {
+ print " ⋅ " .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$hash;pg=" . ($page-1)), -accesskey => "p", -title => "Alt-p"}, "prev");
+ } else {
+ print " ⋅ prev";
+ }
+ if ($#revlist >= (100 * ($page+1)-1)) {
+ print " ⋅ " .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$hash;pg=" . ($page+1)), -accesskey => "n", -title => "Alt-n"}, "next");
+ } else {
+ print " ⋅ next";
+ }
+ print "<br/>\n" .
+ "</div>\n";
+ if (!@revlist) {
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary"), -class => "title"}, " ") .
+ "</div>\n";
+ my %co = git_read_commit($hash);
+ print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
+ }
+ for (my $i = ($page * 100); $i <= $#revlist; $i++) {
+ my $commit = $revlist[$i];
+ my $ref = "";
+ if (defined $refs->{$commit}) {
+ $ref = " <span class=\"tag\">" . esc_html($refs->{$commit}) . "</span>";
+ }
+ my %co = git_read_commit($commit);
+ next if !%co;
+ my %ad = date_str($co{'author_epoch'});
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit"), -class => "title"},
+ "<span class=\"age\">$co{'age_string'}</span>" . esc_html($co{'title'}) . $ref) . "\n";
+ print "</div>\n";
+ print "<div class=\"title_text\">\n" .
+ "<div class=\"log_link\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
+ "<br/>\n" .
+ "</div>\n" .
+ "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
+ "</div>\n" .
+ "<div class=\"log_body\">\n";
+ my $comment = $co{'comment'};
+ my $empty = 0;
+ foreach my $line (@$comment) {
+ if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
+ next;
+ }
+ if ($line eq "") {
+ if ($empty) {
+ next;
+ }
+ $empty = 1;
+ } else {
+ $empty = 0;
+ }
+ print format_log_line_html($line) . "<br/>\n";
+ }
+ if (!$empty) {
+ print "<br/>\n";
+ }
+ print "</div>\n";
+ }
+ git_footer_html();
+}
+
+sub git_commit {
+ my %co = git_read_commit($hash);
+ if (!%co) {
+ die_error(undef, "Unknown commit object.");
+ }
+ my %ad = date_str($co{'author_epoch'}, $co{'author_tz'});
+ my %cd = date_str($co{'committer_epoch'}, $co{'committer_tz'});
+
+ my @difftree;
+ my $root = "";
+ my $parent = $co{'parent'};
+ if (!defined $parent) {
+ $root = " --root";
+ $parent = "";
+ }
+ open my $fd, "-|", "$gitbin/git-diff-tree -r -M $root $parent $hash" or die_error(undef, "Open failed.");
+ @difftree = map { chomp; $_ } <$fd>;
+ close $fd or die_error(undef, "Reading diff-tree failed.");
+
+ # non-textual hash id's can be cached
+ my $expires;
+ if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
+ $expires = "+1d";
+ }
+ my $refs = read_info_ref();
+ my $ref = "";
+ if (defined $refs->{$co{'id'}}) {
+ $ref = " <span class=\"tag\">" . esc_html($refs->{$co{'id'}}) . "</span>";
+ }
+ git_header_html(undef, $expires);
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$hash")}, "log") .
+ " | commit";
+ if (defined $co{'parent'}) {
+ print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash")}, "commitdiff");
+ }
+ print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") . "\n" .
+ "<br/><br/></div>\n";
+ if (defined $co{'parent'}) {
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash"), -class => "title"}, esc_html($co{'title'}) . $ref) . "\n" .
+ "</div>\n";
+ } else {
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash"), -class => "title"}, esc_html($co{'title'})) . "\n" .
+ "</div>\n";
+ }
+ print "<div class=\"title_text\">\n" .
+ "<table cellspacing=\"0\">\n";
+ print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
+ "<tr>" .
+ "<td></td><td> $ad{'rfc2822'}";
+ if ($ad{'hour_local'} < 6) {
+ printf(" (<span style=\"color: #cc0000;\">%02d:%02d</span> %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
+ } else {
+ printf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
+ }
+ print "</td>" .
+ "</tr>\n";
+ print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
+ print "<tr><td></td><td> $cd{'rfc2822'}" . sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) . "</td></tr>\n";
+ print "<tr><td>commit</td><td style=\"font-family:monospace\">$co{'id'}</td></tr>\n";
+ print "<tr>" .
+ "<td>tree</td>" .
+ "<td style=\"font-family:monospace\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash"), class => "list"}, $co{'tree'}) .
+ "</td>" .
+ "<td class=\"link\">" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") .
+ "</td>" .
+ "</tr>\n";
+ my $parents = $co{'parents'};
+ foreach my $par (@$parents) {
+ print "<tr>" .
+ "<td>parent</td>" .
+ "<td style=\"font-family:monospace\">" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$par"), class => "list"}, $par) . "</td>" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$par")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash;hp=$par")}, "commitdiff") .
+ "</td>" .
+ "</tr>\n";
+ }
+ print "</table>".
+ "</div>\n";
+ print "<div class=\"page_body\">\n";
+ my $comment = $co{'comment'};
+ my $empty = 0;
+ my $signed = 0;
+ foreach my $line (@$comment) {
+ # print only one empty line
+ if ($line eq "") {
+ if ($empty || $signed) {
+ next;
+ }
+ $empty = 1;
+ } else {
+ $empty = 0;
+ }
+ if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
+ $signed = 1;
+ print "<span style=\"color: #888888\">" . esc_html($line) . "</span><br/>\n";
+ } else {
+ $signed = 0;
+ print format_log_line_html($line) . "<br/>\n";
+ }
+ }
+ print "</div>\n";
+ print "<div class=\"list_head\">\n";
+ if ($#difftree > 10) {
+ print(($#difftree + 1) . " files changed:\n");
+ }
+ print "</div>\n";
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ foreach my $line (@difftree) {
+ # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
+ # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
+ if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
+ next;
+ }
+ my $from_mode = $1;
+ my $to_mode = $2;
+ my $from_id = $3;
+ my $to_id = $4;
+ my $status = $5;
+ my $similarity = $6;
+ my $file = validate_input(unquote($7));
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ if ($status eq "A") {
+ my $mode_chng = "";
+ if (S_ISREG(oct $to_mode)) {
+ $mode_chng = sprintf(" with mode: %04o", (oct $to_mode) & 0777);
+ }
+ print "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file"), -class => "list"}, esc_html($file)) . "</td>\n" .
+ "<td><span style=\"color: #008000;\">[new " . file_type($to_mode) . "$mode_chng]</span></td>\n" .
+ "<td class=\"link\">" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, "blob") . "</td>\n";
+ } elsif ($status eq "D") {
+ print "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file"), -class => "list"}, esc_html($file)) . "</td>\n" .
+ "<td><span style=\"color: #c00000;\">[deleted " . file_type($from_mode). "]</span></td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, "blob") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;h=$hash;f=$file")}, "history") .
+ "</td>\n"
+ } elsif ($status eq "M" || $status eq "T") {
+ my $mode_chnge = "";
+ if ($from_mode != $to_mode) {
+ $mode_chnge = " <span style=\"color: #777777;\">[changed";
+ if (((oct $from_mode) & S_IFMT) != ((oct $to_mode) & S_IFMT)) {
+ $mode_chnge .= " from " . file_type($from_mode) . " to " . file_type($to_mode);
+ }
+ if (((oct $from_mode) & 0777) != ((oct $to_mode) & 0777)) {
+ if (S_ISREG($from_mode) && S_ISREG($to_mode)) {
+ $mode_chnge .= sprintf(" mode: %04o->%04o", (oct $from_mode) & 0777, (oct $to_mode) & 0777);
+ } elsif (S_ISREG($to_mode)) {
+ $mode_chnge .= sprintf(" mode: %04o", (oct $to_mode) & 0777);
+ }
+ }
+ $mode_chnge .= "]</span>\n";
+ }
+ print "<td>";
+ if ($to_id ne $from_id) {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$file"), -class => "list"}, esc_html($file));
+ } else {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file"), -class => "list"}, esc_html($file));
+ }
+ print "</td>\n" .
+ "<td>$mode_chnge</td>\n" .
+ "<td class=\"link\">";
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, "blob");
+ if ($to_id ne $from_id) {
+ print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$file")}, "diff");
+ }
+ print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;h=$hash;f=$file")}, "history") . "\n";
+ print "</td>\n";
+ } elsif ($status eq "R") {
+ my ($from_file, $to_file) = split "\t", $file;
+ my $mode_chng = "";
+ if ($from_mode != $to_mode) {
+ $mode_chng = sprintf(", mode: %04o", (oct $to_mode) & 0777);
+ }
+ print "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$to_file"), -class => "list"}, esc_html($to_file)) . "</td>\n" .
+ "<td><span style=\"color: #777777;\">[moved from " .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$from_file"), -class => "list"}, esc_html($from_file)) .
+ " with " . (int $similarity) . "% similarity$mode_chng]</span></td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$to_file")}, "blob");
+ if ($to_id ne $from_id) {
+ print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$to_file")}, "diff");
+ }
+ print "</td>\n";
+ }
+ print "</tr>\n";
+ }
+ print "</table>\n";
+ git_footer_html();
+}
+
+sub git_blobdiff {
+ mkdir($git_temp, 0700);
+ git_header_html();
+ if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash_base")}, "tree") .
+ "<br/>\n";
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff_plain;h=$hash;hp=$hash_parent")}, "plain") .
+ "</div>\n";
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash_base"), -class => "title"}, esc_html($co{'title'})) . "\n" .
+ "</div>\n";
+ } else {
+ print "<div class=\"page_nav\">\n" .
+ "<br/><br/></div>\n" .
+ "<div class=\"title\">$hash vs $hash_parent</div>\n";
+ }
+ if (defined $file_name) {
+ print "<div class=\"page_path\"><b>/" . esc_html($file_name) . "</b></div>\n";
+ }
+ print "<div class=\"page_body\">\n" .
+ "<div class=\"diff_info\">blob:" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash_parent;hb=$hash_base;f=$file_name")}, $hash_parent) .
+ " -> blob:" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name")}, $hash) .
+ "</div>\n";
+ git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash);
+ print "</div>";
+ git_footer_html();
+}
+
+sub git_blobdiff_plain {
+ mkdir($git_temp, 0700);
+ print $cgi->header(-type => "text/plain", -charset => 'utf-8');
+ git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash, "plain");
+}
+
+sub git_commitdiff {
+ mkdir($git_temp, 0700);
+ my %co = git_read_commit($hash);
+ if (!%co) {
+ die_error(undef, "Unknown commit object.");
+ }
+ if (!defined $hash_parent) {
+ $hash_parent = $co{'parent'};
+ }
+ open my $fd, "-|", "$gitbin/git-diff-tree -r $hash_parent $hash" or die_error(undef, "Open failed.");
+ my (@difftree) = map { chomp; $_ } <$fd>;
+ close $fd or die_error(undef, "Reading diff-tree failed.");
+
+ # non-textual hash id's can be cached
+ my $expires;
+ if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
+ $expires = "+1d";
+ }
+ my $refs = read_info_ref();
+ my $ref = "";
+ if (defined $refs->{$co{'id'}}) {
+ $ref = " <span class=\"tag\">" . esc_html($refs->{$co{'id'}}) . "</span>";
+ }
+ git_header_html(undef, $expires);
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$hash")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash")}, "commit") .
+ " | commitdiff" .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") . "<br/>\n";
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff_plain;h=$hash;hp=$hash_parent")}, "plain") . "\n" .
+ "</div>\n";
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash"), -class => "title"}, esc_html($co{'title'}) . $ref) . "\n" .
+ "</div>\n";
+ print "<div class=\"page_body\">\n";
+ my $comment = $co{'comment'};
+ my $empty = 0;
+ my $signed = 0;
+ my @log = @$comment;
+ # remove first and empty lines after that
+ shift @log;
+ while (defined $log[0] && $log[0] eq "") {
+ shift @log;
+ }
+ foreach my $line (@log) {
+ if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
+ next;
+ }
+ if ($line eq "") {
+ if ($empty) {
+ next;
+ }
+ $empty = 1;
+ } else {
+ $empty = 0;
+ }
+ print format_log_line_html($line) . "<br/>\n";
+ }
+ print "<br/>\n";
+ foreach my $line (@difftree) {
+ # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
+ # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
+ $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/;
+ my $from_mode = $1;
+ my $to_mode = $2;
+ my $from_id = $3;
+ my $to_id = $4;
+ my $status = $5;
+ my $file = validate_input(unquote($6));
+ if ($status eq "A") {
+ print "<div class=\"diff_info\">" . file_type($to_mode) . ":" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id) . "(new)" .
+ "</div>\n";
+ git_diff_print(undef, "/dev/null", $to_id, "b/$file");
+ } elsif ($status eq "D") {
+ print "<div class=\"diff_info\">" . file_type($from_mode) . ":" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, $from_id) . "(deleted)" .
+ "</div>\n";
+ git_diff_print($from_id, "a/$file", undef, "/dev/null");
+ } elsif ($status eq "M") {
+ if ($from_id ne $to_id) {
+ print "<div class=\"diff_info\">" .
+ file_type($from_mode) . ":" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, $from_id) .
+ " -> " .
+ file_type($to_mode) . ":" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id);
+ print "</div>\n";
+ git_diff_print($from_id, "a/$file", $to_id, "b/$file");
+ }
+ }
+ }
+ print "<br/>\n" .
+ "</div>";
+ git_footer_html();
+}
+
+sub git_commitdiff_plain {
+ mkdir($git_temp, 0700);
+ open my $fd, "-|", "$gitbin/git-diff-tree -r $hash_parent $hash" or die_error(undef, "Open failed.");
+ my (@difftree) = map { chomp; $_ } <$fd>;
+ close $fd or die_error(undef, "Reading diff-tree failed.");
+
+ # try to figure out the next tag after this commit
+ my $tagname;
+ my $refs = read_info_ref("tags");
+ open $fd, "-|", "$gitbin/git-rev-list HEAD";
+ chomp (my (@commits) = <$fd>);
+ close $fd;
+ foreach my $commit (@commits) {
+ if (defined $refs->{$commit}) {
+ $tagname = $refs->{$commit}
+ }
+ if ($commit eq $hash) {
+ last;
+ }
+ }
+
+ print $cgi->header(-type => "text/plain", -charset => 'utf-8', '-content-disposition' => "inline; filename=\"git-$hash.patch\"");
+ my %co = git_read_commit($hash);
+ my %ad = date_str($co{'author_epoch'}, $co{'author_tz'});
+ my $comment = $co{'comment'};
+ print "From: $co{'author'}\n" .
+ "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n".
+ "Subject: $co{'title'}\n";
+ if (defined $tagname) {
+ print "X-Git-Tag: $tagname\n";
+ }
+ print "X-Git-Url: $my_url?p=$project;a=commitdiff;h=$hash\n" .
+ "\n";
+
+ foreach my $line (@$comment) {;
+ print "$line\n";
+ }
+ print "---\n\n";
+
+ foreach my $line (@difftree) {
+ $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/;
+ my $from_id = $3;
+ my $to_id = $4;
+ my $status = $5;
+ my $file = $6;
+ if ($status eq "A") {
+ git_diff_print(undef, "/dev/null", $to_id, "b/$file", "plain");
+ } elsif ($status eq "D") {
+ git_diff_print($from_id, "a/$file", undef, "/dev/null", "plain");
+ } elsif ($status eq "M") {
+ git_diff_print($from_id, "a/$file", $to_id, "b/$file", "plain");
+ }
+ }
+}
+
+sub git_history {
+ if (!defined $hash) {
+ $hash = git_read_head($project);
+ }
+ my %co = git_read_commit($hash);
+ if (!%co) {
+ die_error(undef, "Unknown commit object.");
+ }
+ my $refs = read_info_ref();
+ git_header_html();
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") .
+ "<br/><br/>\n" .
+ "</div>\n";
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash"), -class => "title"}, esc_html($co{'title'})) . "\n" .
+ "</div>\n";
+ print "<div class=\"page_path\"><b>/" . esc_html($file_name) . "</b><br/></div>\n";
+
+ open my $fd, "-|", "$gitbin/git-rev-list $hash | $gitbin/git-diff-tree -r --stdin -- \'$file_name\'";
+ my $commit;
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ while (my $line = <$fd>) {
+ if ($line =~ m/^([0-9a-fA-F]{40})/){
+ $commit = $1;
+ next;
+ }
+ if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/ && (defined $commit)) {
+ my %co = git_read_commit($commit);
+ if (!%co) {
+ next;
+ }
+ my $ref = "";
+ if (defined $refs->{$commit}) {
+ $ref = " <span class=\"tag\">" . esc_html($refs->{$commit}) . "</span>";
+ }
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+ "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" .
+ "<td>" . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit"), -class => "list"}, "<b>" .
+ esc_html(chop_str($co{'title'}, 50)) . "$ref</b>") . "</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;hb=$commit;f=$file_name")}, "blob");
+ my $blob = git_get_hash_by_path($hash, $file_name);
+ my $blob_parent = git_get_hash_by_path($commit, $file_name);
+ if (defined $blob && defined $blob_parent && $blob ne $blob_parent) {
+ print " | " .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff;h=$blob;hp=$blob_parent;hb=$commit;f=$file_name")},
+ "diff to current");
+ }
+ print "</td>\n" .
+ "</tr>\n";
+ undef $commit;
+ }
+ }
+ print "</table>\n";
+ close $fd;
+ git_footer_html();
+}
+
+sub git_search {
+ if (!defined $searchtext) {
+ die_error("", "Text field empty.");
+ }
+ if (!defined $hash) {
+ $hash = git_read_head($project);
+ }
+ my %co = git_read_commit($hash);
+ if (!%co) {
+ die_error(undef, "Unknown commit object.");
+ }
+ # pickaxe may take all resources of your box and run for several minutes
+ # with every query - so decide by yourself how public you make this feature :)
+ my $commit_search = 1;
+ my $author_search = 0;
+ my $committer_search = 0;
+ my $pickaxe_search = 0;
+ if ($searchtext =~ s/^author\\://i) {
+ $author_search = 1;
+ } elsif ($searchtext =~ s/^committer\\://i) {
+ $committer_search = 1;
+ } elsif ($searchtext =~ s/^pickaxe\\://i) {
+ $commit_search = 0;
+ $pickaxe_search = 1;
+ }
+ git_header_html();
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary;h=$hash")}, "summary") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "shortlog") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$hash")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") .
+ "<br/><br/>\n" .
+ "</div>\n";
+
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash"), -class => "title"}, esc_html($co{'title'})) . "\n" .
+ "</div>\n";
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ if ($commit_search) {
+ $/ = "\0";
+ open my $fd, "-|", "$gitbin/git-rev-list --header --parents $hash" or next;
+ while (my $commit_text = <$fd>) {
+ if (!grep m/$searchtext/i, $commit_text) {
+ next;
+ }
+ if ($author_search && !grep m/\nauthor .*$searchtext/i, $commit_text) {
+ next;
+ }
+ if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
+ next;
+ }
+ my @commit_lines = split "\n", $commit_text;
+ my %co = git_read_commit(undef, \@commit_lines);
+ if (!%co) {
+ next;
+ }
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+ "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
+ "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" . esc_html(chop_str($co{'title'}, 50)) . "</b><br/>");
+ my $comment = $co{'comment'};
+ foreach my $line (@$comment) {
+ if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
+ my $lead = esc_html($1) || "";
+ $lead = chop_str($lead, 30, 10);
+ my $match = esc_html($2) || "";
+ my $trail = esc_html($3) || "";
+ $trail = chop_str($trail, 30, 10);
+ my $text = "$lead<span style=\"color:#e00000\">$match</span>$trail";
+ print chop_str($text, 80, 5) . "<br/>\n";
+ }
+ }
+ print "</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree");
+ print "</td>\n" .
+ "</tr>\n";
+ }
+ close $fd;
+ }
+
+ if ($pickaxe_search) {
+ $/ = "\n";
+ open my $fd, "-|", "$gitbin/git-rev-list $hash | $gitbin/git-diff-tree -r --stdin -S\'$searchtext\'";
+ undef %co;
+ my @files;
+ while (my $line = <$fd>) {
+ if (%co && $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
+ my %set;
+ $set{'file'} = $6;
+ $set{'from_id'} = $3;
+ $set{'to_id'} = $4;
+ $set{'id'} = $set{'to_id'};
+ if ($set{'id'} =~ m/0{40}/) {
+ $set{'id'} = $set{'from_id'};
+ }
+ if ($set{'id'} =~ m/0{40}/) {
+ next;
+ }
+ push @files, \%set;
+ } elsif ($line =~ m/^([0-9a-fA-F]{40})$/){
+ if (%co) {
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+ "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
+ "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" .
+ esc_html(chop_str($co{'title'}, 50)) . "</b><br/>");
+ while (my $setref = shift @files) {
+ my %set = %$setref;
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$set{'id'};hb=$co{'id'};f=$set{'file'}"), class => "list"},
+ "<span style=\"color:#e00000\">" . esc_html($set{'file'}) . "</span>") .
+ "<br/>\n";
+ }
+ print "</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree");
+ print "</td>\n" .
+ "</tr>\n";
+ }
+ %co = git_read_commit($1);
+ }
+ }
+ close $fd;
+ }
+ print "</table>\n";
+ git_footer_html();
+}
+
+sub git_shortlog {
+ my $head = git_read_head($project);
+ if (!defined $hash) {
+ $hash = $head;
+ }
+ if (!defined $page) {
+ $page = 0;
+ }
+ my $refs = read_info_ref();
+ git_header_html();
+ print "<div class=\"page_nav\">\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary")}, "summary") .
+ " | shortlog" .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=log;h=$hash")}, "log") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$hash")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$hash")}, "commitdiff") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$hash;hb=$hash")}, "tree") . "<br/>\n";
+
+ my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
+ open my $fd, "-|", "$gitbin/git-rev-list $limit $hash" or die_error(undef, "Open failed.");
+ my (@revlist) = map { chomp; $_ } <$fd>;
+ close $fd;
+
+ if ($hash ne $head || $page) {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog")}, "HEAD");
+ } else {
+ print "HEAD";
+ }
+ if ($page > 0) {
+ print " ⋅ " .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash;pg=" . ($page-1)), -accesskey => "p", -title => "Alt-p"}, "prev");
+ } else {
+ print " ⋅ prev";
+ }
+ if ($#revlist >= (100 * ($page+1)-1)) {
+ print " ⋅ " .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash;pg=" . ($page+1)), -accesskey => "n", -title => "Alt-n"}, "next");
+ } else {
+ print " ⋅ next";
+ }
+ print "<br/>\n" .
+ "</div>\n";
+ print "<div>\n" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=summary"), -class => "title"}, " ") .
+ "</div>\n";
+ print "<table cellspacing=\"0\">\n";
+ my $alternate = 0;
+ for (my $i = ($page * 100); $i <= $#revlist; $i++) {
+ my $commit = $revlist[$i];
+ my $ref = "";
+ if (defined $refs->{$commit}) {
+ $ref = " <span class=\"tag\">" . esc_html($refs->{$commit}) . "</span>";
+ }
+ my %co = git_read_commit($commit);
+ my %ad = date_str($co{'author_epoch'});
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
+ "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
+ "<td>";
+ if (length($co{'title_short'}) < length($co{'title'})) {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit"), -class => "list", -title => "$co{'title'}"},
+ "<b>" . esc_html($co{'title_short'}) . "$ref</b>");
+ } else {
+ print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit"), -class => "list"},
+ "<b>" . esc_html($co{'title_short'}) . "$ref</b>");
+ }
+ print "</td>\n" .
+ "<td class=\"link\">" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$commit")}, "commit") .
+ " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
+ "</td>\n" .
+ "</tr>";
+ }
+ if ($#revlist >= (100 * ($page+1)-1)) {
+ print "<tr>\n" .
+ "<td>" .
+ $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash;pg=" . ($page+1)), -title => "Alt-n"}, "next") .
+ "</td>\n" .
+ "</tr>\n";
+ }
+ print "</table\n>";
+ git_footer_html();
+}
--- /dev/null
+Märchen
+Märchen
--- /dev/null
+This
+filename
+contains
+spaces.
--- /dev/null
+This
+filename
+contains
++
+plus
+chars.
snprintf(newreq->filename, sizeof(newreq->filename), "%s", filename);
snprintf(newreq->tmpfile, sizeof(newreq->tmpfile),
"%s.temp", filename);
+ newreq->slot = NULL;
newreq->next = NULL;
if (object_queue_head == NULL) {
// skip 'objects' at end
if (okay) {
target = xmalloc(serverlen + posn - i - 6);
- strncpy(target, base, serverlen);
- strncpy(target + serverlen, data + i,
- posn - i - 7);
- target[serverlen + posn - i - 7] = '\0';
+ safe_strncpy(target, base, serverlen);
+ safe_strncpy(target + serverlen, data + i, posn - i - 6);
if (get_verbosely)
fprintf(stderr,
"Also look at %s\n", target);
struct xml_ctx *ctx = (struct xml_ctx *)userData;
if (ctx->cdata)
free(ctx->cdata);
- ctx->cdata = xcalloc(len+1, 1);
- strncpy(ctx->cdata, s, len);
+ ctx->cdata = xmalloc(len + 1);
+ safe_strncpy(ctx->cdata, s, len + 1);
}
static int remote_ls(struct alt_base *repo, const char *path, int flags,
int rc = 0;
setup_git_directory();
+ git_config(git_default_config);
while (arg < argc && argv[arg][0] == '-') {
if (argv[arg][1] == 't') {
}
commit_id = argv[arg];
url = argv[arg + 1];
+ write_ref_log_details = url;
http_init();
if (pull(commit_id))
rc = 1;
- curl_slist_free_all(no_pragma_header);
-
http_cleanup();
+ curl_slist_free_all(no_pragma_header);
+
if (corrupt_object_found) {
fprintf(stderr,
"Some loose object were found to be corrupt, but they might be just\n"
finish_request(request);
}
+#ifdef USE_CURL_MULTI
static size_t fwrite_sha1_file(void *ptr, size_t eltsize, size_t nmemb,
void *data)
{
}
}
+static void start_mkcol(struct transfer_request *request)
+{
+ char *hex = sha1_to_hex(request->obj->sha1);
+ struct active_request_slot *slot;
+ char *posn;
+
+ request->url = xmalloc(strlen(remote->url) + 13);
+ strcpy(request->url, remote->url);
+ posn = request->url + strlen(remote->url);
+ strcpy(posn, "objects/");
+ posn += 8;
+ memcpy(posn, hex, 2);
+ posn += 2;
+ strcpy(posn, "/");
+
+ slot = get_active_slot();
+ slot->callback_func = process_response;
+ slot->callback_data = request;
+ curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); /* undo PUT setup */
+ curl_easy_setopt(slot->curl, CURLOPT_URL, request->url);
+ curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, request->errorstr);
+ curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_MKCOL);
+ curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
+
+ if (start_active_slot(slot)) {
+ request->slot = slot;
+ request->state = RUN_MKCOL;
+ } else {
+ request->state = ABORTED;
+ free(request->url);
+ request->url = NULL;
+ }
+}
+#endif
+
static void start_fetch_packed(struct transfer_request *request)
{
char *url;
}
}
-static void start_mkcol(struct transfer_request *request)
-{
- char *hex = sha1_to_hex(request->obj->sha1);
- struct active_request_slot *slot;
- char *posn;
-
- request->url = xmalloc(strlen(remote->url) + 13);
- strcpy(request->url, remote->url);
- posn = request->url + strlen(remote->url);
- strcpy(posn, "objects/");
- posn += 8;
- memcpy(posn, hex, 2);
- posn += 2;
- strcpy(posn, "/");
-
- slot = get_active_slot();
- slot->callback_func = process_response;
- slot->callback_data = request;
- curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); /* undo PUT setup */
- curl_easy_setopt(slot->curl, CURLOPT_URL, request->url);
- curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, request->errorstr);
- curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_MKCOL);
- curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null);
-
- if (start_active_slot(slot)) {
- request->slot = slot;
- request->state = RUN_MKCOL;
- } else {
- request->state = ABORTED;
- free(request->url);
- request->url = NULL;
- }
-}
-
static void start_put(struct transfer_request *request)
{
char *hex = sha1_to_hex(request->obj->sha1);
}
}
+#ifdef USE_CURL_MULTI
void fill_active_slots(void)
{
struct transfer_request *request = request_queue_head;
slot = slot->next;
}
}
+#endif
static void get_remote_object_list(unsigned char parent);
request->next = request_queue_head;
request_queue_head = request;
+#ifdef USE_CURL_MULTI
fill_active_slots();
step_active_slots();
+#endif
}
static int add_send_request(struct object *obj, struct remote_lock *lock)
request->next = request_queue_head;
request_queue_head = request;
+#ifdef USE_CURL_MULTI
fill_active_slots();
step_active_slots();
+#endif
return 1;
}
struct xml_ctx *ctx = (struct xml_ctx *)userData;
if (ctx->cdata)
free(ctx->cdata);
- ctx->cdata = xcalloc(len+1, 1);
- strncpy(ctx->cdata, s, len);
+ ctx->cdata = xmalloc(len + 1);
+ safe_strncpy(ctx->cdata, s, len + 1);
}
static struct remote_lock *lock_remote(char *path, long timeout)
return;
path += 8;
obj_hex = xmalloc(strlen(path));
- strncpy(obj_hex, path, 2);
+ safe_strncpy(obj_hex, path, 3);
strcpy(obj_hex + 2, path + 3);
one_remote_object(obj_hex);
free(obj_hex);
return p;
obj->flags |= SEEN;
+ name = strdup(name);
return add_object(obj, p, path, name);
}
const char *name)
{
struct object *obj = &tree->object;
- struct tree_entry_list *entry;
+ struct tree_desc desc;
+ struct name_entry entry;
struct name_path me;
obj->flags |= LOCAL;
die("bad tree object %s", sha1_to_hex(obj->sha1));
obj->flags |= SEEN;
+ name = strdup(name);
p = add_object(obj, p, NULL, 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, &me, entry->name);
+
+ desc.buf = tree->buffer;
+ desc.size = tree->size;
+
+ while (tree_entry(&desc, &entry)) {
+ if (S_ISDIR(entry.mode))
+ p = process_tree(lookup_tree(entry.sha1), p, &me, name);
else
- p = process_blob(entry->item.blob, p, &me, entry->name);
- free(entry);
- entry = next;
+ p = process_blob(lookup_blob(entry.sha1), p, &me, name);
}
+ free(tree->buffer);
+ tree->buffer = NULL;
return p;
}
/* If it's a symref, set the refname; otherwise try for a sha1 */
if (!strncmp((char *)buffer.buffer, "ref: ", 5)) {
- *symref = xcalloc(buffer.posn - 5, 1);
- strncpy(*symref, (char *)buffer.buffer + 5, buffer.posn - 6);
+ *symref = xmalloc(buffer.posn - 5);
+ safe_strncpy(*symref, (char *)buffer.buffer + 5, buffer.posn - 5);
} else {
get_sha1_hex(buffer.buffer, sha1);
}
if (objects_to_send)
fprintf(stderr, " sending %d objects\n",
objects_to_send);
+#ifdef USE_CURL_MULTI
fill_active_slots();
+#endif
finish_all_active_slots();
/* Update the remote branch if all went well */
long curl_low_speed_time = -1;
struct curl_slist *pragma_header;
-struct curl_slist *no_range_header;
struct active_request_slot *active_queue_head = NULL;
curl_global_init(CURL_GLOBAL_ALL);
pragma_header = curl_slist_append(pragma_header, "Pragma: no-cache");
- no_range_header = curl_slist_append(no_range_header, "Range:");
#ifdef USE_CURL_MULTI
{
curl_multi_cleanup(curlm);
#endif
curl_global_cleanup();
-
+
+ curl_slist_free_all(pragma_header);
}
struct active_request_slot *get_active_slot(void)
slot->finished = NULL;
slot->callback_data = NULL;
slot->callback_func = NULL;
+ curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL);
curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, pragma_header);
- curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_range_header);
curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, curl_errorstr);
+ curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, NULL);
+ curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, NULL);
+ curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, NULL);
+ curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 0);
+ curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1);
return slot;
}
{
closedown_active_slot(slot);
if (slot->curl) {
+#ifdef USE_CURL_MULTI
curl_multi_remove_handle(curlm, slot->curl);
+#endif
curl_easy_cleanup(slot->curl);
slot->curl = NULL;
}
+#ifdef USE_CURL_MULTI
fill_active_slots();
+#endif
}
static void finish_active_slot(struct active_request_slot *slot)
len = strlen(git_default_email);
git_default_email[len++] = '.';
if (he && (domainname = strchr(he->h_name, '.')))
- strncpy(git_default_email + len, domainname + 1, sizeof(git_default_email) - len);
+ safe_strncpy(git_default_email + len, domainname + 1, sizeof(git_default_email) - len);
else
- strncpy(git_default_email + len, "(none)", sizeof(git_default_email) - len);
- git_default_email[sizeof(git_default_email) - 1] = 0;
+ safe_strncpy(git_default_email + len, "(none)", sizeof(git_default_email) - len);
}
/* And set the default date */
datestamp(git_default_date, sizeof(git_default_date));
struct hostent *he;
struct sockaddr_in addr;
int s, a[2], preauth;
+ pid_t pid;
ctx = xcalloc( sizeof(*ctx), 1 );
exit( 1 );
}
- if (fork() == 0) {
+ pid = fork();
+ if (pid < 0)
+ _exit( 127 );
+ if (!pid) {
if (dup2( a[0], 0 ) == -1 || dup2( a[0], 1 ) == -1)
_exit( 127 );
close( a[0] );
+++ /dev/null
-/*
- * Copyright (c) 2005, Junio C Hamano
- */
-#include <signal.h>
-#include "cache.h"
-
-static struct cache_file *cache_file_list;
-
-static void remove_lock_file(void)
-{
- while (cache_file_list) {
- if (cache_file_list->lockfile[0])
- unlink(cache_file_list->lockfile);
- cache_file_list = cache_file_list->next;
- }
-}
-
-static void remove_lock_file_on_signal(int signo)
-{
- remove_lock_file();
- signal(SIGINT, SIG_DFL);
- raise(signo);
-}
-
-int hold_index_file_for_update(struct cache_file *cf, const char *path)
-{
- int fd;
- sprintf(cf->lockfile, "%s.lock", path);
- fd = open(cf->lockfile, O_RDWR | O_CREAT | O_EXCL, 0666);
- if (fd >=0 && !cf->next) {
- cf->next = cache_file_list;
- cache_file_list = cf;
- signal(SIGINT, remove_lock_file_on_signal);
- atexit(remove_lock_file);
- }
- return fd;
-}
-
-int commit_index_file(struct cache_file *cf)
-{
- char indexfile[PATH_MAX];
- int i;
- strcpy(indexfile, cf->lockfile);
- i = strlen(indexfile) - 5; /* .lock */
- indexfile[i] = 0;
- i = rename(cf->lockfile, indexfile);
- cf->lockfile[0] = 0;
- return i;
-}
-
-void rollback_index_file(struct cache_file *cf)
-{
- if (cf->lockfile[0])
- unlink(cf->lockfile);
- cf->lockfile[0] = 0;
-}
-
int arg = 1;
setup_git_directory();
+ git_config(git_default_config);
while (arg < argc && argv[arg][0] == '-') {
if (argv[arg][1] == 't')
usage(local_pull_usage);
commit_id = argv[arg];
path = argv[arg + 1];
+ write_ref_log_details = path;
if (pull(commit_id))
return 1;
--- /dev/null
+/*
+ * Copyright (c) 2005, Junio C Hamano
+ */
+#include <signal.h>
+#include "cache.h"
+
+static struct lock_file *lock_file_list;
+
+static void remove_lock_file(void)
+{
+ while (lock_file_list) {
+ if (lock_file_list->filename[0])
+ unlink(lock_file_list->filename);
+ lock_file_list = lock_file_list->next;
+ }
+}
+
+static void remove_lock_file_on_signal(int signo)
+{
+ remove_lock_file();
+ signal(SIGINT, SIG_DFL);
+ raise(signo);
+}
+
+int hold_lock_file_for_update(struct lock_file *lk, const char *path)
+{
+ int fd;
+ sprintf(lk->filename, "%s.lock", path);
+ fd = open(lk->filename, O_RDWR | O_CREAT | O_EXCL, 0666);
+ if (0 <= fd) {
+ if (!lk->next) {
+ lk->next = lock_file_list;
+ lock_file_list = lk;
+ signal(SIGINT, remove_lock_file_on_signal);
+ atexit(remove_lock_file);
+ }
+ if (adjust_shared_perm(lk->filename))
+ return error("cannot fix permission bits on %s",
+ lk->filename);
+ }
+ return fd;
+}
+
+int commit_lock_file(struct lock_file *lk)
+{
+ char result_file[PATH_MAX];
+ int i;
+ strcpy(result_file, lk->filename);
+ i = strlen(result_file) - 5; /* .lock */
+ result_file[i] = 0;
+ i = rename(lk->filename, result_file);
+ lk->filename[0] = 0;
+ return i;
+}
+
+void rollback_lock_file(struct lock_file *lk)
+{
+ if (lk->filename[0])
+ unlink(lk->filename);
+ lk->filename[0] = 0;
+}
+
}
}
+static int append_signoff(char *buf, int buf_sz, int at, const char *signoff)
+{
+ int signoff_len = strlen(signoff);
+ static const char signed_off_by[] = "Signed-off-by: ";
+ char *cp = buf;
+
+ /* Do we have enough space to add it? */
+ if (buf_sz - at <= strlen(signed_off_by) + signoff_len + 2)
+ return at;
+
+ /* First see if we already have the sign-off by the signer */
+ while (1) {
+ cp = strstr(cp, signed_off_by);
+ if (!cp)
+ break;
+ cp += strlen(signed_off_by);
+ if ((cp + signoff_len < buf + at) &&
+ !strncmp(cp, signoff, signoff_len) &&
+ isspace(cp[signoff_len]))
+ return at; /* we already have him */
+ }
+
+ strcpy(buf + at, signed_off_by);
+ at += strlen(signed_off_by);
+ strcpy(buf + at, signoff);
+ at += signoff_len;
+ buf[at++] = '\n';
+ buf[at] = 0;
+ return at;
+}
+
void show_log(struct rev_info *opt, struct log_info *log, const char *sep)
{
static char this_header[16384];
int abbrev_commit = opt->abbrev_commit ? opt->abbrev : 40;
const char *extra;
int len;
- char *subject = NULL, *after_subject = NULL;
+ const char *subject = NULL, *extra_headers = opt->extra_headers;
opt->loginfo = NULL;
if (!opt->verbose_header) {
static char subject_buffer[1024];
static char buffer[1024];
snprintf(subject_buffer, sizeof(subject_buffer) - 1,
+ "%s"
"MIME-Version: 1.0\n"
"Content-Type: multipart/mixed;\n"
" boundary=\"%s%s\"\n"
"Content-Type: text/plain; "
"charset=UTF-8; format=fixed\n"
"Content-Transfer-Encoding: 8bit\n\n",
+ extra_headers ? extra_headers : "",
mime_boundary_leader, opt->mime_boundary,
mime_boundary_leader, opt->mime_boundary);
- after_subject = subject_buffer;
+ extra_headers = subject_buffer;
snprintf(buffer, sizeof(buffer) - 1,
"--%s%s\n"
/*
* And then the pretty-printed message itself
*/
- len = pretty_print_commit(opt->commit_format, commit, ~0u, this_header, sizeof(this_header), abbrev, subject, after_subject);
+ len = pretty_print_commit(opt->commit_format, commit, ~0u, this_header, sizeof(this_header), abbrev, subject, extra_headers);
+
+ if (opt->add_signoff)
+ len = append_signoff(this_header, sizeof(this_header), len,
+ opt->add_signoff);
printf("%s%s%s", this_header, extra, sep);
}
return 1;
}
-static int handle_from(char *line)
+static int handle_from(char *in_line)
{
- char *at = strchr(line, '@');
+ char line[1000];
+ char *at;
char *dst;
+ strcpy(line, in_line);
+ at = strchr(line, '@');
if (!at)
return bogus_from(line);
#define SEEN_FROM 01
#define SEEN_DATE 02
#define SEEN_SUBJECT 04
+#define SEEN_BOGUS_UNIX_FROM 010
+#define SEEN_PREFIX 020
-/* First lines of body can have From:, Date:, and Subject: */
-static int handle_inbody_header(int *seen, char *line)
+/* First lines of body can have From:, Date:, and Subject: or empty */
+static void handle_inbody_header(int *seen, char *line)
{
+ if (*seen & SEEN_PREFIX)
+ return;
+ if (isspace(*line)) {
+ char *cp;
+ for (cp = line + 1; *cp; cp++) {
+ if (!isspace(*cp))
+ break;
+ }
+ if (!*cp)
+ return;
+ }
+ if (!memcmp(">From", line, 5) && isspace(line[5])) {
+ if (!(*seen & SEEN_BOGUS_UNIX_FROM)) {
+ *seen |= SEEN_BOGUS_UNIX_FROM;
+ return;
+ }
+ }
if (!memcmp("From:", line, 5) && isspace(line[5])) {
if (!(*seen & SEEN_FROM) && handle_from(line+6)) {
*seen |= SEEN_FROM;
- return 1;
+ return;
}
}
if (!memcmp("Date:", line, 5) && isspace(line[5])) {
if (!(*seen & SEEN_DATE)) {
handle_date(line+6);
*seen |= SEEN_DATE;
- return 1;
+ return;
}
}
if (!memcmp("Subject:", line, 8) && isspace(line[8])) {
if (!(*seen & SEEN_SUBJECT)) {
handle_subject(line+9);
*seen |= SEEN_SUBJECT;
- return 1;
+ return;
}
}
if (!memcmp("[PATCH]", line, 7) && isspace(line[7])) {
if (!(*seen & SEEN_SUBJECT)) {
handle_subject(line);
*seen |= SEEN_SUBJECT;
- return 1;
+ return;
}
}
- return 0;
+ *seen |= SEEN_PREFIX;
}
static char *cleanup_subject(char *subject)
}
break;
}
+ eatspace(subject);
return subject;
}
}
}
}
+static void decode_header_bq(char *it);
typedef int (*header_fn_t)(char *);
struct header_def {
const char *name;
int namelen;
};
-static void check_header(char *line, int len, struct header_def *header)
+static void check_header(char *line, struct header_def *header)
{
int i;
int len = header[i].namelen;
if (!strncasecmp(line, header[i].name, len) &&
line[len] == ':' && isspace(line[len + 1])) {
+ /* Unwrap inline B and Q encoding, and optionally
+ * normalize the meta information to utf8.
+ */
+ decode_header_bq(line + len + 2);
header[i].func(line + len + 2);
break;
}
}
}
-static void check_subheader_line(char *line, int len)
+static void check_subheader_line(char *line)
{
static struct header_def header[] = {
{ "Content-Type", handle_subcontent_type },
handle_content_transfer_encoding },
{ NULL },
};
- check_header(line, len, header);
+ check_header(line, header);
}
-static void check_header_line(char *line, int len)
+static void check_header_line(char *line)
{
static struct header_def header[] = {
{ "From", handle_from },
handle_content_transfer_encoding },
{ NULL },
};
- check_header(line, len, header);
+ check_header(line, header);
+}
+
+static int is_rfc2822_header(char *line)
+{
+ /*
+ * The section that defines the loosest possible
+ * field name is "3.6.8 Optional fields".
+ *
+ * optional-field = field-name ":" unstructured CRLF
+ * field-name = 1*ftext
+ * ftext = %d33-57 / %59-126
+ */
+ int ch;
+ char *cp = line;
+ while ((ch = *cp++)) {
+ if (ch == ':')
+ return cp != line;
+ if ((33 <= ch && ch <= 57) ||
+ (59 <= ch && ch <= 126))
+ continue;
+ break;
+ }
+ return 0;
}
static int read_one_header_line(char *line, int sz, FILE *in)
while (ofs < sz) {
int peek, len;
if (fgets(line + ofs, sz - ofs, in) == NULL)
- return ofs;
+ break;
len = eatspace(line + ofs);
- if (len == 0)
- return ofs;
- peek = fgetc(in); ungetc(peek, in);
- if (peek == ' ' || peek == '\t') {
- /* Yuck, 2822 header "folding" */
- ofs += len;
- continue;
+ if ((len == 0) || !is_rfc2822_header(line)) {
+ /* Re-add the newline */
+ line[ofs + len] = '\n';
+ line[ofs + len + 1] = '\0';
+ break;
}
- return ofs + len;
+ ofs += len;
+ /* Yuck, 2822 header "folding" */
+ peek = fgetc(in); ungetc(peek, in);
+ if (peek != ' ' && peek != '\t')
+ break;
}
+ /* Count mbox From headers as headers */
+ if (!ofs && !memcmp(line, "From ", 5))
+ ofs = 1;
return ofs;
}
static void handle_info(void)
{
char *sub;
- static int done_info = 0;
-
- if (done_info)
- return;
- done_info = 1;
sub = cleanup_subject(subject);
cleanup_space(name);
cleanup_space(date);
cleanup_space(email);
cleanup_space(sub);
- /* Unwrap inline B and Q encoding, and optionally
- * normalize the meta information to utf8.
- */
- decode_header_bq(name);
- decode_header_bq(date);
- decode_header_bq(email);
- decode_header_bq(sub);
printf("Author: %s\nEmail: %s\nSubject: %s\nDate: %s\n\n",
name, email, sub, date);
}
/* We are inside message body and have read line[] already.
* Spit out the commit log.
*/
-static int handle_commit_msg(void)
+static int handle_commit_msg(int *seen)
{
if (!cmitmsg)
return 0;
decode_transfer_encoding(line);
if (metainfo_charset)
convert_to_utf8(line, charset);
+
+ handle_inbody_header(seen, line);
+ if (!(*seen & SEEN_PREFIX))
+ continue;
+
fputs(line, cmitmsg);
} while (fgets(line, sizeof(line), stdin) != NULL);
fclose(cmitmsg);
* that the first part to contain commit message and a patch, and
* handle other parts as pure patches.
*/
-static int handle_multipart_one_part(void)
+static int handle_multipart_one_part(int *seen)
{
- int seen = 0;
int n = 0;
- int len;
while (fgets(line, sizeof(line), stdin) != NULL) {
again:
- len = eatspace(line);
n++;
- if (!len)
- continue;
if (is_multipart_boundary(line))
break;
- if (0 <= seen && handle_inbody_header(&seen, line))
- continue;
- seen = -1; /* no more inbody headers */
- line[len] = '\n';
- handle_info();
- if (handle_commit_msg())
+ if (handle_commit_msg(seen))
goto again;
handle_patch();
break;
static void handle_multipart_body(void)
{
+ int seen = 0;
int part_num = 0;
/* Skip up to the first boundary */
return;
/* We are on boundary line. Start slurping the subhead. */
while (1) {
- int len = read_one_header_line(line, sizeof(line), stdin);
- if (!len) {
- if (handle_multipart_one_part() < 0)
+ int hdr = read_one_header_line(line, sizeof(line), stdin);
+ if (!hdr) {
+ if (handle_multipart_one_part(&seen) < 0)
return;
/* Reset per part headers */
transfer_encoding = TE_DONTCARE;
charset[0] = 0;
}
else
- check_subheader_line(line, len);
+ check_subheader_line(line);
}
fclose(patchfile);
if (!patch_lines) {
{
int seen = 0;
- while (fgets(line, sizeof(line), stdin) != NULL) {
- int len = eatspace(line);
- if (!len)
- continue;
- if (0 <= seen && handle_inbody_header(&seen, line))
- continue;
- seen = -1; /* no more inbody headers */
- line[len] = '\n';
- handle_info();
- handle_commit_msg();
- handle_patch();
- break;
- }
+ handle_commit_msg(&seen);
+ handle_patch();
fclose(patchfile);
if (!patch_lines) {
fprintf(stderr, "No patch found\n");
exit(1);
}
while (1) {
- int len = read_one_header_line(line, sizeof(line), stdin);
- if (!len) {
+ int hdr = read_one_header_line(line, sizeof(line), stdin);
+ if (!hdr) {
if (multipart_boundary[0])
handle_multipart_body();
else
handle_body();
+ handle_info();
break;
}
- check_header_line(line, len);
+ check_header_line(line);
}
return 0;
}
while (*argp) {
const char *file = *argp++;
- FILE *f = !strcmp(file, "-") ? stdin : fopen(file, "rt");
+ FILE *f = !strcmp(file, "-") ? stdin : fopen(file, "r");
int file_done = 0;
if ( !f )
static int nr_objs;
int obj_allocs;
-int track_object_refs = 1;
+int track_object_refs = 0;
static int hashtable_index(const unsigned char *sha1)
{
obj = &blob->object;
} else if (!strcmp(type, tree_type)) {
struct tree *tree = lookup_tree(sha1);
- parse_tree_buffer(tree, buffer, size);
obj = &tree->object;
+ if (!tree->object.parsed) {
+ parse_tree_buffer(tree, buffer, size);
+ buffer = NULL;
+ }
} else if (!strcmp(type, commit_type)) {
struct commit *commit = lookup_commit(sha1);
parse_commit_buffer(commit, buffer, size);
}
}
-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)
+static unsigned name_hash(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;
- }
- }
+ unsigned char c;
+ unsigned hash = 0;
+
/*
- * Make sure "Makefile" and "t/Makefile" are hashed separately
- * but close enough.
+ * This effectively just creates a sortable number from the
+ * last sixteen non-whitespace characters. Last characters
+ * count "most", so things that end in ".c" sort together.
*/
- hash = (name_hash<<DIRBITS) | (hash & ((1U<<DIRBITS )-1));
+ while ((c = *name++) != 0) {
+ if (isspace(c))
+ continue;
+ hash = (hash >> 2) + (c << 24);
+ }
return hash;
}
}
static void add_pbase_object(struct tree_desc *tree,
- struct name_path *up,
const char *name,
- int cmplen)
+ int cmplen,
+ const char *fullname)
{
- while (tree->size) {
- const unsigned char *sha1;
- const char *entry_name;
- int entry_len;
- unsigned mode;
+ struct name_entry entry;
+
+ while (tree_entry(tree,&entry)) {
unsigned long size;
char type[20];
- sha1 = tree_entry_extract(tree, &entry_name, &mode);
- update_tree_entry(tree);
- entry_len = strlen(entry_name);
- if (entry_len != cmplen ||
- memcmp(entry_name, name, cmplen) ||
- !has_sha1_file(sha1) ||
- sha1_object_info(sha1, type, &size))
+ if (entry.pathlen != cmplen ||
+ memcmp(entry.path, name, cmplen) ||
+ !has_sha1_file(entry.sha1) ||
+ sha1_object_info(entry.sha1, type, &size))
continue;
if (name[cmplen] != '/') {
- unsigned hash = name_hash(up, name);
- add_object_entry(sha1, hash, 1);
+ unsigned hash = name_hash(fullname);
+ add_object_entry(entry.sha1, hash, 1);
return;
}
if (!strcmp(type, tree_type)) {
struct tree_desc sub;
- struct name_path me;
struct pbase_tree_cache *tree;
const char *down = name+cmplen+1;
int downlen = name_cmp_len(down);
- tree = pbase_tree_get(sha1);
+ tree = pbase_tree_get(entry.sha1);
if (!tree)
return;
sub.buf = tree->tree_data;
sub.size = tree->tree_size;
- me.up = up;
- me.elem = entry_name;
- me.len = entry_len;
- add_pbase_object(&sub, &me, down, downlen);
+ add_pbase_object(&sub, down, downlen, fullname);
pbase_tree_put(tree);
}
}
for (it = pbase_tree; it; it = it->next) {
if (cmplen == 0) {
- hash = name_hash(NULL, "");
+ hash = name_hash("");
add_object_entry(it->pcache.sha1, hash, 1);
}
else {
struct tree_desc tree;
tree.buf = it->pcache.tree_data;
tree.size = it->pcache.tree_size;
- add_pbase_object(&tree, NULL, name, cmplen);
+ add_pbase_object(&tree, name, cmplen, name);
}
}
}
}
if (get_sha1_hex(line, sha1))
die("expected sha1, got garbage:\n %s", line);
- hash = name_hash(NULL, line+41);
+ hash = name_hash(line+41);
add_preferred_base_object(line+41, hash);
add_object_entry(sha1, hash, 0);
}
}
-char *safe_strncpy(char *dest, const char *src, size_t n)
+size_t safe_strncpy(char *dest, const char *src, size_t size)
{
- strncpy(dest, src, n);
- dest[n - 1] = '\0';
+ size_t ret = strlen(src);
- return dest;
+ if (size) {
+ size_t len = (ret >= size) ? size - 1 : ret;
+ memcpy(dest, src, len);
+ dest[len] = '\0';
+ }
+ return ret;
}
+
int validate_symref(const char *path)
{
struct stat st;
return NULL;
}
+
+int adjust_shared_perm(const char *path)
+{
+ struct stat st;
+ int mode;
+
+ if (!shared_repository)
+ return 0;
+ if (lstat(path, &st) < 0)
+ return -1;
+ mode = st.st_mode;
+ if (mode & S_IRUSR)
+ mode |= S_IRGRP;
+ if (mode & S_IWUSR)
+ mode |= S_IWGRP;
+ if (mode & S_IXUSR)
+ mode |= S_IXGRP;
+ if (S_ISDIR(mode))
+ mode |= S_ISGID;
+ if (chmod(path, mode) < 0)
+ return -2;
+ return 0;
+}
* Copyright (C) Linus Torvalds, 2005
*/
#include "cache.h"
+#include "cache-tree.h"
+
+/* Index extensions.
+ *
+ * The first letter should be 'A'..'Z' for extensions that are not
+ * necessary for a correct operation (i.e. optimization data).
+ * When new extensions are added that _needs_ to be understood in
+ * order to correctly interpret the index file, pick character that
+ * is outside the range, to cause the reader to abort.
+ */
+
+#define CACHE_EXT(s) ( (s[0]<<24)|(s[1]<<16)|(s[2]<<8)|(s[3]) )
+#define CACHE_EXT_TREE 0x54524545 /* "TREE" */
struct cache_entry **active_cache = NULL;
static time_t index_file_timestamp;
unsigned int active_nr = 0, active_alloc = 0, active_cache_changed = 0;
+struct cache_tree *active_cache_tree = NULL;
+
/*
* This only updates the "non-critical" parts of the directory
* cache, ie the parts that aren't tracked by GIT, and only used
return 0;
}
+static int read_index_extension(const char *ext, void *data, unsigned long sz)
+{
+ switch (CACHE_EXT(ext)) {
+ case CACHE_EXT_TREE:
+ active_cache_tree = cache_tree_read(data, sz);
+ break;
+ default:
+ if (*ext < 'A' || 'Z' < *ext)
+ return error("index uses %.4s extension, which we do not understand",
+ ext);
+ fprintf(stderr, "ignoring %.4s extension\n", ext);
+ break;
+ }
+ return 0;
+}
+
int read_cache(void)
{
int fd, i;
active_cache[i] = ce;
}
index_file_timestamp = st.st_mtime;
+ while (offset <= size - 20 - 8) {
+ /* After an array of active_nr index entries,
+ * there can be arbitrary number of extended
+ * sections, each of which is prefixed with
+ * extension name (4-byte) and section length
+ * in 4-byte network byte order.
+ */
+ unsigned long extsize;
+ memcpy(&extsize, map + offset + 4, 4);
+ extsize = ntohl(extsize);
+ if (read_index_extension(map + offset,
+ map + offset + 8, extsize) < 0)
+ goto unmap;
+ offset += 8;
+ offset += extsize;
+ }
return active_nr;
unmap:
return 0;
}
+static int write_index_ext_header(SHA_CTX *context, int fd,
+ unsigned int ext, unsigned int sz)
+{
+ ext = htonl(ext);
+ sz = htonl(sz);
+ if ((ce_write(context, fd, &ext, 4) < 0) ||
+ (ce_write(context, fd, &sz, 4) < 0))
+ return -1;
+ return 0;
+}
+
static int ce_flush(SHA_CTX *context, int fd)
{
unsigned int left = write_buffer_len;
if (ce_write(&c, newfd, ce, ce_size(ce)) < 0)
return -1;
}
+
+ /* Write extension data here */
+ if (active_cache_tree) {
+ unsigned long sz;
+ void *data = cache_tree_write(active_cache_tree, &sz);
+ if (data &&
+ !write_index_ext_header(&c, newfd, CACHE_EXT_TREE, sz) &&
+ !ce_write(&c, newfd, data, sz))
+ ;
+ else {
+ free(data);
+ return -1;
+ }
+ }
return ce_flush(&c, newfd);
}
error("Unable to create %s", git_HEAD);
return -3;
}
+ if (adjust_shared_perm(git_HEAD)) {
+ unlink(lockpath);
+ error("Unable to fix permissions on %s", lockpath);
+ return -4;
+ }
return 0;
}
namelen = strlen(de->d_name);
if (namelen > 255)
continue;
+ if (namelen>5 && !strcmp(de->d_name+namelen-5,".lock"))
+ continue;
memcpy(path + baselen, de->d_name, namelen+1);
if (stat(git_path("%s", path), &st) < 0)
continue;
return do_for_each_ref("refs/remotes", fn, 13);
}
-static char *ref_file_name(const char *ref)
-{
- char *base = get_refs_directory();
- int baselen = strlen(base);
- int reflen = strlen(ref);
- char *ret = xmalloc(baselen + 2 + reflen);
- sprintf(ret, "%s/%s", base, ref);
- return ret;
-}
-
-static char *ref_lock_file_name(const char *ref)
-{
- char *base = get_refs_directory();
- int baselen = strlen(base);
- int reflen = strlen(ref);
- char *ret = xmalloc(baselen + 7 + reflen);
- sprintf(ret, "%s/%s.lock", base, ref);
- return ret;
-}
-
int get_ref_sha1(const char *ref, unsigned char *sha1)
{
if (check_ref_format(ref))
return read_ref(git_path("refs/%s", ref), sha1);
}
-static int lock_ref_file(const char *filename, const char *lock_filename,
- const unsigned char *old_sha1)
-{
- int fd = open(lock_filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
- unsigned char current_sha1[20];
- int retval;
- if (fd < 0) {
- return error("Couldn't open lock file for %s: %s",
- filename, strerror(errno));
- }
- retval = read_ref(filename, current_sha1);
- if (old_sha1) {
- if (retval) {
- close(fd);
- unlink(lock_filename);
- return error("Could not read the current value of %s",
- filename);
- }
- if (memcmp(current_sha1, old_sha1, 20)) {
- close(fd);
- unlink(lock_filename);
- error("The current value of %s is %s",
- filename, sha1_to_hex(current_sha1));
- return error("Expected %s",
- sha1_to_hex(old_sha1));
- }
- } else {
- if (!retval) {
- close(fd);
- unlink(lock_filename);
- return error("Unexpectedly found a value of %s for %s",
- sha1_to_hex(current_sha1), filename);
- }
- }
- return fd;
-}
-
-int lock_ref_sha1(const char *ref, const unsigned char *old_sha1)
-{
- char *filename;
- char *lock_filename;
- int retval;
- if (check_ref_format(ref))
- return -1;
- filename = ref_file_name(ref);
- lock_filename = ref_lock_file_name(ref);
- retval = lock_ref_file(filename, lock_filename, old_sha1);
- free(filename);
- free(lock_filename);
- return retval;
-}
-
-static int write_ref_file(const char *filename,
- const char *lock_filename, int fd,
- const unsigned char *sha1)
-{
- char *hex = sha1_to_hex(sha1);
- char term = '\n';
- if (write(fd, hex, 40) < 40 ||
- write(fd, &term, 1) < 1) {
- error("Couldn't write %s", filename);
- close(fd);
- return -1;
- }
- close(fd);
- rename(lock_filename, filename);
- return 0;
-}
-
-int write_ref_sha1(const char *ref, int fd, const unsigned char *sha1)
-{
- char *filename;
- char *lock_filename;
- int retval;
- if (fd < 0)
- return -1;
- if (check_ref_format(ref))
- return -1;
- filename = ref_file_name(ref);
- lock_filename = ref_lock_file_name(ref);
- if (safe_create_leading_directories(filename))
- die("unable to create leading directory for %s", filename);
- retval = write_ref_file(filename, lock_filename, fd, sha1);
- free(filename);
- free(lock_filename);
- return retval;
-}
-
/*
* Make sure "ref" is something reasonable to have under ".git/refs/";
* We do not like it if:
}
}
-int write_ref_sha1_unlocked(const char *ref, const unsigned char *sha1)
+static struct ref_lock *verify_lock(struct ref_lock *lock,
+ const unsigned char *old_sha1, int mustexist)
+{
+ char buf[40];
+ int nr, fd = open(lock->ref_file, O_RDONLY);
+ if (fd < 0 && (mustexist || errno != ENOENT)) {
+ error("Can't verify ref %s", lock->ref_file);
+ unlock_ref(lock);
+ return NULL;
+ }
+ nr = read(fd, buf, 40);
+ close(fd);
+ if (nr != 40 || get_sha1_hex(buf, lock->old_sha1) < 0) {
+ error("Can't verify ref %s", lock->ref_file);
+ unlock_ref(lock);
+ return NULL;
+ }
+ if (memcmp(lock->old_sha1, old_sha1, 20)) {
+ error("Ref %s is at %s but expected %s", lock->ref_file,
+ sha1_to_hex(lock->old_sha1), sha1_to_hex(old_sha1));
+ unlock_ref(lock);
+ return NULL;
+ }
+ return lock;
+}
+
+static struct ref_lock *lock_ref_sha1_basic(const char *path,
+ int plen,
+ const unsigned char *old_sha1, int mustexist)
+{
+ struct ref_lock *lock;
+ struct stat st;
+
+ lock = xcalloc(1, sizeof(struct ref_lock));
+ lock->lock_fd = -1;
+
+ plen = strlen(path) - plen;
+ path = resolve_ref(path, lock->old_sha1, mustexist);
+ if (!path) {
+ unlock_ref(lock);
+ return NULL;
+ }
+ lock->lk = xcalloc(1, sizeof(struct lock_file));
+
+ lock->ref_file = strdup(path);
+ lock->log_file = strdup(git_path("logs/%s", lock->ref_file + plen));
+ lock->force_write = lstat(lock->ref_file, &st) && errno == ENOENT;
+
+ if (safe_create_leading_directories(lock->ref_file))
+ die("unable to create directory for %s", lock->ref_file);
+ lock->lock_fd = hold_lock_file_for_update(lock->lk, lock->ref_file);
+ if (lock->lock_fd < 0) {
+ error("Couldn't open lock file %s: %s",
+ lock->lk->filename, strerror(errno));
+ unlock_ref(lock);
+ return NULL;
+ }
+
+ return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock;
+}
+
+struct ref_lock *lock_ref_sha1(const char *ref,
+ const unsigned char *old_sha1, int mustexist)
{
- char *filename;
- char *lock_filename;
- int fd;
- int retval;
if (check_ref_format(ref))
+ return NULL;
+ return lock_ref_sha1_basic(git_path("refs/%s", ref),
+ 5 + strlen(ref), old_sha1, mustexist);
+}
+
+struct ref_lock *lock_any_ref_for_update(const char *ref,
+ const unsigned char *old_sha1, int mustexist)
+{
+ return lock_ref_sha1_basic(git_path("%s", ref),
+ strlen(ref), old_sha1, mustexist);
+}
+
+void unlock_ref(struct ref_lock *lock)
+{
+ if (lock->lock_fd >= 0) {
+ close(lock->lock_fd);
+ /* Do not free lock->lk -- atexit() still looks at them */
+ if (lock->lk)
+ rollback_lock_file(lock->lk);
+ }
+ if (lock->ref_file)
+ free(lock->ref_file);
+ if (lock->log_file)
+ free(lock->log_file);
+ free(lock);
+}
+
+static int log_ref_write(struct ref_lock *lock,
+ const unsigned char *sha1, const char *msg)
+{
+ int logfd, written, oflags = O_APPEND | O_WRONLY;
+ unsigned maxlen, len;
+ char *logrec;
+ const char *comitter;
+
+ if (log_all_ref_updates) {
+ if (safe_create_leading_directories(lock->log_file) < 0)
+ return error("unable to create directory for %s",
+ lock->log_file);
+ oflags |= O_CREAT;
+ }
+
+ logfd = open(lock->log_file, oflags, 0666);
+ if (logfd < 0) {
+ if (!log_all_ref_updates && errno == ENOENT)
+ return 0;
+ return error("Unable to append to %s: %s",
+ lock->log_file, strerror(errno));
+ }
+
+ setup_ident();
+ comitter = git_committer_info(1);
+ if (msg) {
+ maxlen = strlen(comitter) + strlen(msg) + 2*40 + 5;
+ logrec = xmalloc(maxlen);
+ len = snprintf(logrec, maxlen, "%s %s %s\t%s\n",
+ sha1_to_hex(lock->old_sha1),
+ sha1_to_hex(sha1),
+ comitter,
+ msg);
+ }
+ else {
+ maxlen = strlen(comitter) + 2*40 + 4;
+ logrec = xmalloc(maxlen);
+ len = snprintf(logrec, maxlen, "%s %s %s\n",
+ sha1_to_hex(lock->old_sha1),
+ sha1_to_hex(sha1),
+ comitter);
+ }
+ written = len <= maxlen ? write(logfd, logrec, len) : -1;
+ free(logrec);
+ close(logfd);
+ if (written != len)
+ return error("Unable to append to %s", lock->log_file);
+ return 0;
+}
+
+int write_ref_sha1(struct ref_lock *lock,
+ const unsigned char *sha1, const char *logmsg)
+{
+ static char term = '\n';
+
+ if (!lock)
return -1;
- filename = ref_file_name(ref);
- lock_filename = ref_lock_file_name(ref);
- if (safe_create_leading_directories(filename))
- die("unable to create leading directory for %s", filename);
- fd = open(lock_filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
- if (fd < 0) {
- error("Writing %s", lock_filename);
- perror("Open");
+ if (!lock->force_write && !memcmp(lock->old_sha1, sha1, 20)) {
+ unlock_ref(lock);
+ return 0;
}
- retval = write_ref_file(filename, lock_filename, fd, sha1);
- free(filename);
- free(lock_filename);
- return retval;
+ if (write(lock->lock_fd, sha1_to_hex(sha1), 40) != 40 ||
+ write(lock->lock_fd, &term, 1) != 1
+ || close(lock->lock_fd) < 0) {
+ error("Couldn't write %s", lock->lk->filename);
+ unlock_ref(lock);
+ return -1;
+ }
+ if (log_ref_write(lock, sha1, logmsg) < 0) {
+ unlock_ref(lock);
+ return -1;
+ }
+ if (commit_lock_file(lock->lk)) {
+ error("Couldn't set %s", lock->ref_file);
+ unlock_ref(lock);
+ return -1;
+ }
+ lock->lock_fd = -1;
+ unlock_ref(lock);
+ return 0;
+}
+
+int read_ref_at(const char *ref, unsigned long at_time, unsigned char *sha1)
+{
+ const char *logfile, *logdata, *logend, *rec, *lastgt, *lastrec;
+ char *tz_c;
+ int logfd, tz;
+ struct stat st;
+ unsigned long date;
+ unsigned char logged_sha1[20];
+
+ logfile = git_path("logs/%s", ref);
+ logfd = open(logfile, O_RDONLY, 0);
+ if (logfd < 0)
+ die("Unable to read log %s: %s", logfile, strerror(errno));
+ fstat(logfd, &st);
+ if (!st.st_size)
+ die("Log %s is empty.", logfile);
+ logdata = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, logfd, 0);
+ close(logfd);
+
+ lastrec = NULL;
+ rec = logend = logdata + st.st_size;
+ while (logdata < rec) {
+ if (logdata < rec && *(rec-1) == '\n')
+ rec--;
+ lastgt = NULL;
+ while (logdata < rec && *(rec-1) != '\n') {
+ rec--;
+ if (*rec == '>')
+ lastgt = rec;
+ }
+ if (!lastgt)
+ die("Log %s is corrupt.", logfile);
+ date = strtoul(lastgt + 1, &tz_c, 10);
+ if (date <= at_time) {
+ if (lastrec) {
+ if (get_sha1_hex(lastrec, logged_sha1))
+ die("Log %s is corrupt.", logfile);
+ if (get_sha1_hex(rec + 41, sha1))
+ die("Log %s is corrupt.", logfile);
+ if (memcmp(logged_sha1, sha1, 20)) {
+ tz = strtoul(tz_c, NULL, 10);
+ fprintf(stderr,
+ "warning: Log %s has gap after %s.\n",
+ logfile, show_rfc2822_date(date, tz));
+ }
+ }
+ else if (date == at_time) {
+ if (get_sha1_hex(rec + 41, sha1))
+ die("Log %s is corrupt.", logfile);
+ }
+ else {
+ if (get_sha1_hex(rec + 41, logged_sha1))
+ die("Log %s is corrupt.", logfile);
+ if (memcmp(logged_sha1, sha1, 20)) {
+ tz = strtoul(tz_c, NULL, 10);
+ fprintf(stderr,
+ "warning: Log %s unexpectedly ended on %s.\n",
+ logfile, show_rfc2822_date(date, tz));
+ }
+ }
+ munmap((void*)logdata, st.st_size);
+ return 0;
+ }
+ lastrec = rec;
+ }
+
+ rec = logdata;
+ while (rec < logend && *rec != '>' && *rec != '\n')
+ rec++;
+ if (rec == logend || *rec == '\n')
+ die("Log %s is corrupt.", logfile);
+ date = strtoul(rec + 1, &tz_c, 10);
+ tz = strtoul(tz_c, NULL, 10);
+ if (get_sha1_hex(logdata, sha1))
+ die("Log %s is corrupt.", logfile);
+ munmap((void*)logdata, st.st_size);
+ fprintf(stderr, "warning: Log %s only goes back to %s.\n",
+ logfile, show_rfc2822_date(date, tz));
+ return 0;
}
#ifndef REFS_H
#define REFS_H
+struct ref_lock {
+ char *ref_file;
+ char *log_file;
+ struct lock_file *lk;
+ unsigned char old_sha1[20];
+ int lock_fd;
+ int force_write;
+};
+
/*
* Calls the specified function for each ref file until it returns nonzero,
* and returns the value
/** Reads the refs file specified into sha1 **/
extern int get_ref_sha1(const char *ref, unsigned char *sha1);
-/** Locks ref and returns the fd to give to write_ref_sha1() if the ref
- * has the given value currently; otherwise, returns -1.
- **/
-extern int lock_ref_sha1(const char *ref, const unsigned char *old_sha1);
+/** Locks a "refs/" ref returning the lock on success and NULL on failure. **/
+extern struct ref_lock *lock_ref_sha1(const char *ref, const unsigned char *old_sha1, int mustexist);
+
+/** Locks any ref (for 'HEAD' type refs). */
+extern struct ref_lock *lock_any_ref_for_update(const char *ref, const unsigned char *old_sha1, int mustexist);
+
+/** Release any lock taken but not written. **/
+extern void unlock_ref(struct ref_lock *lock);
-/** Writes sha1 into the refs file specified, locked with the given fd. **/
-extern int write_ref_sha1(const char *ref, int fd, const unsigned char *sha1);
+/** Writes sha1 into the ref specified by the lock. **/
+extern int write_ref_sha1(struct ref_lock *lock, const unsigned char *sha1, const char *msg);
-/** Writes sha1 into the refs file specified. **/
-extern int write_ref_sha1_unlocked(const char *ref, const unsigned char *sha1);
+/** Reads log for the value of ref during at_time. **/
+extern int read_ref_at(const char *ref, unsigned long at_time, unsigned char *sha1);
/** Returns 0 if target has the right format for a ref. **/
extern int check_ref_format(const char *target);
int main(int argc, const char **argv)
{
- setup_git_directory();
+ int nongit = 0;
+ setup_git_directory_gently(&nongit);
while (1 < argc) {
if (!strcmp(argv[1], "--int"))
+++ /dev/null
-/*
- * rev-parse.c
- *
- * Copyright (C) Linus Torvalds, 2005
- */
-#include "cache.h"
-#include "commit.h"
-#include "refs.h"
-#include "quote.h"
-
-#define DO_REVS 1
-#define DO_NOREV 2
-#define DO_FLAGS 4
-#define DO_NONFLAGS 8
-static int filter = ~0;
-
-static char *def = NULL;
-
-#define NORMAL 0
-#define REVERSED 1
-static int show_type = NORMAL;
-static int symbolic = 0;
-static int abbrev = 0;
-static int output_sq = 0;
-
-static int revs_count = 0;
-
-/*
- * Some arguments are relevant "revision" arguments,
- * others are about output format or other details.
- * This sorts it all out.
- */
-static int is_rev_argument(const char *arg)
-{
- static const char *rev_args[] = {
- "--all",
- "--bisect",
- "--dense",
- "--branches",
- "--header",
- "--max-age=",
- "--max-count=",
- "--min-age=",
- "--no-merges",
- "--objects",
- "--objects-edge",
- "--parents",
- "--pretty",
- "--remotes",
- "--sparse",
- "--tags",
- "--topo-order",
- "--date-order",
- "--unpacked",
- NULL
- };
- const char **p = rev_args;
-
- /* accept -<digit>, like traditional "head" */
- if ((*arg == '-') && isdigit(arg[1]))
- return 1;
-
- for (;;) {
- const char *str = *p++;
- int len;
- if (!str)
- return 0;
- len = strlen(str);
- if (!strcmp(arg, str) ||
- (str[len-1] == '=' && !strncmp(arg, str, len)))
- return 1;
- }
-}
-
-/* Output argument as a string, either SQ or normal */
-static void show(const char *arg)
-{
- if (output_sq) {
- int sq = '\'', ch;
-
- putchar(sq);
- while ((ch = *arg++)) {
- if (ch == sq)
- fputs("'\\'", stdout);
- putchar(ch);
- }
- putchar(sq);
- putchar(' ');
- }
- else
- puts(arg);
-}
-
-/* Output a revision, only if filter allows it */
-static void show_rev(int type, const unsigned char *sha1, const char *name)
-{
- if (!(filter & DO_REVS))
- return;
- def = NULL;
- revs_count++;
-
- if (type != show_type)
- putchar('^');
- if (symbolic && name)
- show(name);
- else if (abbrev)
- show(find_unique_abbrev(sha1, abbrev));
- else
- show(sha1_to_hex(sha1));
-}
-
-/* Output a flag, only if filter allows it. */
-static int show_flag(char *arg)
-{
- if (!(filter & DO_FLAGS))
- return 0;
- if (filter & (is_rev_argument(arg) ? DO_REVS : DO_NOREV)) {
- show(arg);
- return 1;
- }
- return 0;
-}
-
-static void show_default(void)
-{
- char *s = def;
-
- if (s) {
- unsigned char sha1[20];
-
- def = NULL;
- if (!get_sha1(s, sha1)) {
- show_rev(NORMAL, sha1, s);
- return;
- }
- }
-}
-
-static int show_reference(const char *refname, const unsigned char *sha1)
-{
- show_rev(NORMAL, sha1, refname);
- return 0;
-}
-
-static void show_datestring(const char *flag, const char *datestr)
-{
- static char buffer[100];
-
- /* date handling requires both flags and revs */
- if ((filter & (DO_FLAGS | DO_REVS)) != (DO_FLAGS | DO_REVS))
- return;
- snprintf(buffer, sizeof(buffer), "%s%lu", flag, approxidate(datestr));
- show(buffer);
-}
-
-static int show_file(const char *arg)
-{
- show_default();
- if ((filter & (DO_NONFLAGS|DO_NOREV)) == (DO_NONFLAGS|DO_NOREV)) {
- show(arg);
- return 1;
- }
- return 0;
-}
-
-int main(int argc, char **argv)
-{
- int i, as_is = 0, verify = 0;
- unsigned char sha1[20];
- const char *prefix = setup_git_directory();
-
- git_config(git_default_config);
-
- for (i = 1; i < argc; i++) {
- char *arg = argv[i];
- char *dotdot;
-
- if (as_is) {
- if (show_file(arg) && as_is < 2)
- verify_filename(prefix, arg);
- continue;
- }
- if (!strcmp(arg,"-n")) {
- if (++i >= argc)
- die("-n requires an argument");
- if ((filter & DO_FLAGS) && (filter & DO_REVS)) {
- show(arg);
- show(argv[i]);
- }
- continue;
- }
- if (!strncmp(arg,"-n",2)) {
- if ((filter & DO_FLAGS) && (filter & DO_REVS))
- show(arg);
- continue;
- }
-
- if (*arg == '-') {
- if (!strcmp(arg, "--")) {
- as_is = 2;
- /* Pass on the "--" if we show anything but files.. */
- if (filter & (DO_FLAGS | DO_REVS))
- show_file(arg);
- continue;
- }
- if (!strcmp(arg, "--default")) {
- def = argv[i+1];
- i++;
- continue;
- }
- if (!strcmp(arg, "--revs-only")) {
- filter &= ~DO_NOREV;
- continue;
- }
- if (!strcmp(arg, "--no-revs")) {
- filter &= ~DO_REVS;
- continue;
- }
- if (!strcmp(arg, "--flags")) {
- filter &= ~DO_NONFLAGS;
- continue;
- }
- if (!strcmp(arg, "--no-flags")) {
- filter &= ~DO_FLAGS;
- continue;
- }
- if (!strcmp(arg, "--verify")) {
- filter &= ~(DO_FLAGS|DO_NOREV);
- verify = 1;
- continue;
- }
- if (!strcmp(arg, "--short") ||
- !strncmp(arg, "--short=", 8)) {
- filter &= ~(DO_FLAGS|DO_NOREV);
- verify = 1;
- abbrev = DEFAULT_ABBREV;
- if (arg[7] == '=')
- abbrev = strtoul(arg + 8, NULL, 10);
- if (abbrev < MINIMUM_ABBREV)
- abbrev = MINIMUM_ABBREV;
- else if (40 <= abbrev)
- abbrev = 40;
- continue;
- }
- if (!strcmp(arg, "--sq")) {
- output_sq = 1;
- continue;
- }
- if (!strcmp(arg, "--not")) {
- show_type ^= REVERSED;
- continue;
- }
- if (!strcmp(arg, "--symbolic")) {
- symbolic = 1;
- continue;
- }
- if (!strcmp(arg, "--all")) {
- for_each_ref(show_reference);
- continue;
- }
- if (!strcmp(arg, "--branches")) {
- for_each_branch_ref(show_reference);
- continue;
- }
- if (!strcmp(arg, "--tags")) {
- for_each_tag_ref(show_reference);
- continue;
- }
- if (!strcmp(arg, "--remotes")) {
- for_each_remote_ref(show_reference);
- continue;
- }
- if (!strcmp(arg, "--show-prefix")) {
- if (prefix)
- puts(prefix);
- continue;
- }
- if (!strcmp(arg, "--show-cdup")) {
- const char *pfx = prefix;
- while (pfx) {
- pfx = strchr(pfx, '/');
- if (pfx) {
- pfx++;
- printf("../");
- }
- }
- putchar('\n');
- continue;
- }
- if (!strcmp(arg, "--git-dir")) {
- const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
- static char cwd[PATH_MAX];
- if (gitdir) {
- puts(gitdir);
- continue;
- }
- if (!prefix) {
- puts(".git");
- continue;
- }
- if (!getcwd(cwd, PATH_MAX))
- die("unable to get current working directory");
- printf("%s/.git\n", cwd);
- continue;
- }
- if (!strncmp(arg, "--since=", 8)) {
- show_datestring("--max-age=", arg+8);
- continue;
- }
- if (!strncmp(arg, "--after=", 8)) {
- show_datestring("--max-age=", arg+8);
- continue;
- }
- if (!strncmp(arg, "--before=", 9)) {
- show_datestring("--min-age=", arg+9);
- continue;
- }
- if (!strncmp(arg, "--until=", 8)) {
- show_datestring("--min-age=", arg+8);
- continue;
- }
- if (show_flag(arg) && verify)
- die("Needed a single revision");
- continue;
- }
-
- /* Not a flag argument */
- dotdot = strstr(arg, "..");
- if (dotdot) {
- unsigned char end[20];
- char *next = dotdot + 2;
- char *this = arg;
- *dotdot = 0;
- if (!*next)
- next = "HEAD";
- if (dotdot == arg)
- this = "HEAD";
- if (!get_sha1(this, sha1) && !get_sha1(next, end)) {
- show_rev(NORMAL, end, next);
- show_rev(REVERSED, sha1, this);
- continue;
- }
- *dotdot = '.';
- }
- if (!get_sha1(arg, sha1)) {
- show_rev(NORMAL, sha1, arg);
- continue;
- }
- if (*arg == '^' && !get_sha1(arg+1, sha1)) {
- show_rev(REVERSED, sha1, arg+1);
- continue;
- }
- as_is = 1;
- if (!show_file(arg))
- continue;
- if (verify)
- die("Needed a single revision");
- verify_filename(prefix, arg);
- }
- show_default();
- if (verify && revs_count != 1)
- die("Needed a single revision");
- return 0;
-}
void mark_tree_uninteresting(struct tree *tree)
{
+ struct tree_desc desc;
+ struct name_entry entry;
struct object *obj = &tree->object;
- struct tree_entry_list *entry;
if (obj->flags & UNINTERESTING)
return;
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);
+
+ desc.buf = tree->buffer;
+ desc.size = tree->size;
+ while (tree_entry(&desc, &entry)) {
+ if (S_ISDIR(entry.mode))
+ mark_tree_uninteresting(lookup_tree(entry.sha1));
else
- mark_blob_uninteresting(entry->item.blob);
- free(entry);
- entry = next;
+ mark_blob_uninteresting(lookup_blob(entry.sha1));
}
+
+ /*
+ * We don't care about the tree any more
+ * after it has been marked uninteresting.
+ */
+ free(tree->buffer);
+ tree->buffer = NULL;
}
void mark_parents_uninteresting(struct commit *commit)
parse_commit(p);
switch (rev_compare_tree(revs, p->tree, commit->tree)) {
case REV_TREE_SAME:
- if (p->object.flags & UNINTERESTING) {
+ if (!revs->simplify_history || (p->object.flags & UNINTERESTING)) {
/* Even if a merge with an uninteresting
* side branch brought the entire change
* we are interested in, we do not want
revs->abbrev = DEFAULT_ABBREV;
revs->ignore_merges = 1;
+ revs->simplify_history = 1;
revs->pruning.recursive = 1;
revs->pruning.add_remove = file_add_remove;
revs->pruning.change = file_change;
revs->abbrev = DEFAULT_ABBREV;
continue;
}
+ if (!strncmp(arg, "--abbrev=", 9)) {
+ revs->abbrev = strtoul(arg + 9, NULL, 10);
+ if (revs->abbrev < MINIMUM_ABBREV)
+ revs->abbrev = MINIMUM_ABBREV;
+ else if (revs->abbrev > 40)
+ revs->abbrev = 40;
+ continue;
+ }
if (!strcmp(arg, "--abbrev-commit")) {
revs->abbrev_commit = 1;
continue;
revs->full_diff = 1;
continue;
}
+ if (!strcmp(arg, "--full-history")) {
+ revs->simplify_history = 0;
+ continue;
+ }
opts = diff_opt_parse(&revs->diffopt, argv+i, argc-i);
if (opts > 0) {
revs->diff = 1;
no_merges:1,
no_walk:1,
remove_empty_trees:1,
+ simplify_history:1,
lifo:1,
topo_order:1,
tag_objects:1,
struct log_info *loginfo;
int nr, total;
const char *mime_boundary;
+ const char *add_signoff;
+ const char *extra_headers;
/* special limits */
int max_count;
int sizen;
int of;
int i;
+ pid_t pid;
if (!strcmp(url, "-")) {
*fd_in = 0;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv))
return error("Couldn't create socket");
- if (!fork()) {
+ pid = fork();
+ if (pid < 0)
+ return error("Couldn't fork");
+ if (!pid) {
const char *ssh, *ssh_basename;
ssh = getenv("GIT_SSH");
if (!ssh) ssh = "ssh";
return 0;
}
-int adjust_shared_perm(const char *path)
-{
- struct stat st;
- int mode;
-
- if (!shared_repository)
- return 0;
- if (lstat(path, &st) < 0)
- return -1;
- mode = st.st_mode;
- if (mode & S_IRUSR)
- mode |= S_IRGRP;
- if (mode & S_IWUSR)
- mode |= S_IWGRP;
- if (mode & S_IXUSR)
- mode |= S_IXGRP;
- if (S_ISDIR(mode))
- mode |= S_ISGID;
- if (chmod(path, mode) < 0)
- return -2;
- return 0;
-}
-
int safe_create_leading_directories(char *path)
{
char *pos = path;
/* we have .idx. Is it a file we can map? */
strcpy(path + len, de->d_name);
+ for (p = packed_git; p; p = p->next) {
+ if (!memcmp(path, p->pack_name, len + namelen - 4))
+ break;
+ }
+ if (p)
+ continue;
p = add_packed_git(path, len + namelen, local);
if (!p)
continue;
closedir(dir);
}
+static int prepare_packed_git_run_once = 0;
void prepare_packed_git(void)
{
- static int run_once = 0;
struct alternate_object_database *alt;
- if (run_once)
+ if (prepare_packed_git_run_once)
return;
prepare_packed_git_one(get_object_directory(), 1);
prepare_alt_odb();
prepare_packed_git_one(alt->base, 0);
alt->name[-1] = '/';
}
- run_once = 1;
+ prepare_packed_git_run_once = 1;
+}
+
+static void reprepare_packed_git(void)
+{
+ prepare_packed_git_run_once = 0;
+ prepare_packed_git();
}
int check_sha1_signature(const unsigned char *sha1, void *map, unsigned long size, const char *type)
if (!map) {
struct pack_entry e;
- if (!find_pack_entry(sha1, &e))
- return error("unable to find %s", sha1_to_hex(sha1));
- return packed_object_info(&e, type, sizep);
+ if (find_pack_entry(sha1, &e))
+ return packed_object_info(&e, type, sizep);
+ reprepare_packed_git();
+ if (find_pack_entry(sha1, &e))
+ return packed_object_info(&e, type, sizep);
+ return error("unable to find %s", sha1_to_hex(sha1));
}
if (unpack_sha1_header(&stream, map, mapsize, hdr, sizeof(hdr)) < 0)
status = error("unable to unpack %s header",
munmap(map, mapsize);
return buf;
}
+ reprepare_packed_git();
+ if (find_pack_entry(sha1, &e))
+ return read_packed_sha1(sha1, type, size);
return NULL;
}
#include "tree.h"
#include "blob.h"
#include "tree-walk.h"
+#include "refs.h"
static int find_short_object_filename(int len, const char *name, unsigned char *sha1)
{
"refs/remotes/%.*s/HEAD",
NULL
};
- const char **p;
- const char *warning = "warning: refname '%.*s' is ambiguous.\n";
- char *pathname;
- int already_found = 0;
+ static const char *warning = "warning: refname '%.*s' is ambiguous.\n";
+ const char **p, *pathname;
+ char *real_path = NULL;
+ int refs_found = 0, am;
+ unsigned long at_time = (unsigned long)-1;
unsigned char *this_result;
unsigned char sha1_from_ref[20];
if (len == 40 && !get_sha1_hex(str, sha1))
return 0;
+ /* At a given period of time? "@{2 hours ago}" */
+ for (am = 1; am < len - 1; am++) {
+ if (str[am] == '@' && str[am+1] == '{' && str[len-1] == '}') {
+ int date_len = len - am - 3;
+ char *date_spec = xmalloc(date_len + 1);
+ safe_strncpy(date_spec, str + am + 2, date_len + 1);
+ at_time = approxidate(date_spec);
+ free(date_spec);
+ len = am;
+ break;
+ }
+ }
+
/* Accept only unambiguous ref paths. */
if (ambiguous_path(str, len))
return -1;
for (p = fmt; *p; p++) {
- this_result = already_found ? sha1_from_ref : sha1;
- pathname = git_path(*p, len, str);
- if (!read_ref(pathname, this_result)) {
- if (warn_ambiguous_refs) {
- if (already_found)
- fprintf(stderr, warning, len, str);
- already_found++;
- }
- else
- return 0;
+ this_result = refs_found ? sha1_from_ref : sha1;
+ pathname = resolve_ref(git_path(*p, len, str), this_result, 1);
+ if (pathname) {
+ if (!refs_found++)
+ real_path = strdup(pathname);
+ if (!warn_ambiguous_refs)
+ break;
}
}
- if (already_found)
- return 0;
- return -1;
+
+ if (!refs_found)
+ return -1;
+
+ if (warn_ambiguous_refs && refs_found > 1)
+ fprintf(stderr, warning, len, str);
+
+ if (at_time != (unsigned long)-1) {
+ read_ref_at(
+ real_path + strlen(git_path(".")) - 1,
+ at_time,
+ sha1);
+ }
+
+ free(real_path);
+ return 0;
}
static int get_sha1_1(const char *name, int len, unsigned char *sha1);
*/
int get_sha1(const char *name, unsigned char *sha1)
{
- int ret;
+ int ret, bracket_depth;
unsigned unused;
int namelen = strlen(name);
const char *cp;
}
return -1;
}
- cp = strchr(name, ':');
- if (cp) {
+ for (cp = name, bracket_depth = 0; *cp; cp++) {
+ if (*cp == '{')
+ bracket_depth++;
+ else if (bracket_depth && *cp == '}')
+ bracket_depth--;
+ else if (!bracket_depth && *cp == ':')
+ break;
+ }
+ if (*cp == ':') {
unsigned char tree_sha1[20];
if (!get_sha1_1(name, cp-name, tree_sha1))
return get_tree_entry(tree_sha1, cp+1, sha1,
if (!prog) prog = "git-ssh-upload";
setup_git_directory();
+ git_config(git_default_config);
while (arg < argc && argv[arg][0] == '-') {
if (argv[arg][1] == 't') {
}
commit_id = argv[arg];
url = argv[arg + 1];
+ write_ref_log_details = url;
if (setup_connection(&fd_in, &fd_out, prog, url, arg, argv + 1))
return 1;
'git-ls-tree -r output for a known tree.' \
'diff current expected'
+test_expect_success \
+ 'writing partial tree out with git-write-tree --prefix.' \
+ 'ptree=$(git-write-tree --prefix=path3)'
+test_expect_success \
+ 'validate object ID for a known tree.' \
+ 'test "$ptree" = 21ae8269cacbe57ae09138dcc3a2887f904d02b3'
+
+test_expect_success \
+ 'writing partial tree out with git-write-tree --prefix.' \
+ 'ptree=$(git-write-tree --prefix=path3/subp3)'
+test_expect_success \
+ 'validate object ID for a known tree.' \
+ 'test "$ptree" = 3c5e5399f3a333eddecce7a9b9465b63f65f51e2'
+
################################################################
rm .git/index
test_expect_success \
git-update-index --add yomin &&
git-read-tree -m -u $treeH $treeM &&
git-ls-files --stage >4.out || return 1
- diff --unified=0 M.out 4.out >4diff.out
+ diff -U0 M.out 4.out >4diff.out
compare_change 4diff.out expected &&
check_cache_at yomin clean &&
sum bozbar frotz nitfol >actual4.sum &&
echo yomin yomin >yomin &&
git-read-tree -m -u $treeH $treeM &&
git-ls-files --stage >5.out || return 1
- diff --unified=0 M.out 5.out >5diff.out
+ diff -U0 M.out 5.out >5diff.out
compare_change 5diff.out expected &&
check_cache_at yomin dirty &&
sum bozbar frotz nitfol >actual5.sum &&
git-update-index --add frotz &&
git-read-tree -m -u $treeH $treeM &&
git-ls-files --stage >6.out &&
- diff --unified=0 M.out 6.out &&
+ diff -U0 M.out 6.out &&
check_cache_at frotz clean &&
sum bozbar frotz nitfol >actual3.sum &&
cmp M.sum actual3.sum &&
echo frotz frotz >frotz &&
git-read-tree -m -u $treeH $treeM &&
git-ls-files --stage >7.out &&
- diff --unified=0 M.out 7.out &&
+ diff -U0 M.out 7.out &&
check_cache_at frotz dirty &&
sum bozbar frotz nitfol >actual7.sum &&
if cmp M.sum actual7.sum; then false; else :; fi &&
git-update-index --add nitfol &&
git-read-tree -m -u $treeH $treeM &&
git-ls-files --stage >14.out || return 1
- diff --unified=0 M.out 14.out >14diff.out
+ diff -U0 M.out 14.out >14diff.out
compare_change 14diff.out expected &&
sum bozbar frotz >actual14.sum &&
grep -v nitfol M.sum > expected14.sum &&
echo nitfol nitfol nitfol >nitfol &&
git-read-tree -m -u $treeH $treeM &&
git-ls-files --stage >15.out || return 1
- diff --unified=0 M.out 15.out >15diff.out
+ diff -U0 M.out 15.out >15diff.out
compare_change 15diff.out expected &&
check_cache_at nitfol dirty &&
sum bozbar frotz >actual15.sum &&
git-update-index --add bozbar &&
git-read-tree -m -u $treeH $treeM &&
git-ls-files --stage >18.out &&
- diff --unified=0 M.out 18.out &&
+ diff -U0 M.out 18.out &&
check_cache_at bozbar clean &&
sum bozbar frotz nitfol >actual18.sum &&
cmp M.sum actual18.sum'
echo gnusto gnusto >bozbar &&
git-read-tree -m -u $treeH $treeM &&
git-ls-files --stage >19.out &&
- diff --unified=0 M.out 19.out &&
+ diff -U0 M.out 19.out &&
check_cache_at bozbar dirty &&
sum frotz nitfol >actual19.sum &&
grep -v bozbar M.sum > expected19.sum &&
git-update-index --add bozbar &&
git-read-tree -m -u $treeH $treeM &&
git-ls-files --stage >20.out &&
- diff --unified=0 M.out 20.out &&
+ diff -U0 M.out 20.out &&
check_cache_at bozbar clean &&
sum bozbar frotz nitfol >actual20.sum &&
cmp M.sum actual20.sum'
git-update-index --add DF &&
git-read-tree -m -u $treeDF $treeDFDF &&
git-ls-files --stage >DFDFcheck.out &&
- diff --unified=0 DFDF.out DFDFcheck.out &&
+ diff -U0 DFDF.out DFDFcheck.out &&
check_cache_at DF/DF clean'
test_done
--- /dev/null
+#!/bin/sh
+#
+# Copyright (c) 2006 Shawn Pearce
+#
+
+test_description='Test git-update-ref and basic ref logging'
+. ./test-lib.sh
+
+Z=0000000000000000000000000000000000000000
+A=1111111111111111111111111111111111111111
+B=2222222222222222222222222222222222222222
+C=3333333333333333333333333333333333333333
+D=4444444444444444444444444444444444444444
+E=5555555555555555555555555555555555555555
+F=6666666666666666666666666666666666666666
+m=refs/heads/master
+
+test_expect_success \
+ "create $m" \
+ 'git-update-ref $m $A &&
+ test $A = $(cat .git/$m)'
+test_expect_success \
+ "create $m" \
+ 'git-update-ref $m $B $A &&
+ test $B = $(cat .git/$m)'
+rm -f .git/$m
+
+test_expect_success \
+ "create $m (by HEAD)" \
+ 'git-update-ref HEAD $A &&
+ test $A = $(cat .git/$m)'
+test_expect_success \
+ "create $m (by HEAD)" \
+ 'git-update-ref HEAD $B $A &&
+ test $B = $(cat .git/$m)'
+rm -f .git/$m
+
+test_expect_failure \
+ '(not) create HEAD with old sha1' \
+ 'git-update-ref HEAD $A $B'
+test_expect_failure \
+ "(not) prior created .git/$m" \
+ 'test -f .git/$m'
+rm -f .git/$m
+
+test_expect_success \
+ "create HEAD" \
+ 'git-update-ref HEAD $A'
+test_expect_failure \
+ '(not) change HEAD with wrong SHA1' \
+ 'git-update-ref HEAD $B $Z'
+test_expect_failure \
+ "(not) changed .git/$m" \
+ 'test $B = $(cat .git/$m)'
+rm -f .git/$m
+
+mkdir -p .git/logs/refs/heads
+touch .git/logs/refs/heads/master
+test_expect_success \
+ "create $m (logged by touch)" \
+ 'GIT_COMMITTER_DATE="2005-05-26 23:30" \
+ git-update-ref HEAD $A -m "Initial Creation" &&
+ test $A = $(cat .git/$m)'
+test_expect_success \
+ "update $m (logged by touch)" \
+ 'GIT_COMMITTER_DATE="2005-05-26 23:31" \
+ git-update-ref HEAD $B $A -m "Switch" &&
+ test $B = $(cat .git/$m)'
+test_expect_success \
+ "set $m (logged by touch)" \
+ 'GIT_COMMITTER_DATE="2005-05-26 23:41" \
+ git-update-ref HEAD $A &&
+ test $A = $(cat .git/$m)'
+
+cat >expect <<EOF
+$Z $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 Initial Creation
+$A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150260 +0000 Switch
+$B $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150860 +0000
+EOF
+test_expect_success \
+ "verifying $m's log" \
+ 'diff expect .git/logs/$m'
+rm -rf .git/$m .git/logs expect
+
+test_expect_success \
+ 'enable core.logAllRefUpdates' \
+ 'git-repo-config core.logAllRefUpdates true &&
+ test true = $(git-repo-config --bool --get core.logAllRefUpdates)'
+
+test_expect_success \
+ "create $m (logged by config)" \
+ 'GIT_COMMITTER_DATE="2005-05-26 23:32" \
+ git-update-ref HEAD $A -m "Initial Creation" &&
+ test $A = $(cat .git/$m)'
+test_expect_success \
+ "update $m (logged by config)" \
+ 'GIT_COMMITTER_DATE="2005-05-26 23:33" \
+ git-update-ref HEAD $B $A -m "Switch" &&
+ test $B = $(cat .git/$m)'
+test_expect_success \
+ "set $m (logged by config)" \
+ 'GIT_COMMITTER_DATE="2005-05-26 23:43" \
+ git-update-ref HEAD $A &&
+ test $A = $(cat .git/$m)'
+
+cat >expect <<EOF
+$Z $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150320 +0000 Initial Creation
+$A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150380 +0000 Switch
+$B $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150980 +0000
+EOF
+test_expect_success \
+ "verifying $m's log" \
+ 'diff expect .git/logs/$m'
+rm -f .git/$m .git/logs/$m expect
+
+git-update-ref $m $D
+cat >.git/logs/$m <<EOF
+$C $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150320 -0500
+$A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150380 -0500
+$F $Z $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150680 -0500
+$Z $E $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150980 -0500
+EOF
+
+ed="Thu, 26 May 2005 18:32:00 -0500"
+gd="Thu, 26 May 2005 18:33:00 -0500"
+ld="Thu, 26 May 2005 18:43:00 -0500"
+test_expect_success \
+ 'Query "master@{May 25 2005}" (before history)' \
+ 'rm -f o e
+ git-rev-parse --verify "master@{May 25 2005}" >o 2>e &&
+ test $C = $(cat o) &&
+ test "warning: Log .git/logs/$m only goes back to $ed." = "$(cat e)"'
+test_expect_success \
+ "Query master@{2005-05-25} (before history)" \
+ 'rm -f o e
+ git-rev-parse --verify master@{2005-05-25} >o 2>e &&
+ test $C = $(cat o) &&
+ echo test "warning: Log .git/logs/$m only goes back to $ed." = "$(cat e)"'
+test_expect_success \
+ 'Query "master@{May 26 2005 23:31:59}" (1 second before history)' \
+ 'rm -f o e
+ git-rev-parse --verify "master@{May 26 2005 23:31:59}" >o 2>e &&
+ test $C = $(cat o) &&
+ test "warning: Log .git/logs/$m only goes back to $ed." = "$(cat e)"'
+test_expect_success \
+ 'Query "master@{May 26 2005 23:32:00}" (exactly history start)' \
+ 'rm -f o e
+ git-rev-parse --verify "master@{May 26 2005 23:32:00}" >o 2>e &&
+ test $A = $(cat o) &&
+ test "" = "$(cat e)"'
+test_expect_success \
+ 'Query "master@{2005-05-26 23:33:01}" (middle of history with gap)' \
+ 'rm -f o e
+ git-rev-parse --verify "master@{2005-05-26 23:33:01}" >o 2>e &&
+ test $B = $(cat o) &&
+ test "warning: Log .git/logs/$m has gap after $gd." = "$(cat e)"'
+test_expect_success \
+ 'Query "master@{2005-05-26 23:38:00}" (middle of history)' \
+ 'rm -f o e
+ git-rev-parse --verify "master@{2005-05-26 23:38:00}" >o 2>e &&
+ test $Z = $(cat o) &&
+ test "" = "$(cat e)"'
+test_expect_success \
+ 'Query "master@{2005-05-26 23:43:00}" (exact end of history)' \
+ 'rm -f o e
+ git-rev-parse --verify "master@{2005-05-26 23:43:00}" >o 2>e &&
+ test $E = $(cat o) &&
+ test "" = "$(cat e)"'
+test_expect_success \
+ 'Query "master@{2005-05-28}" (past end of history)' \
+ 'rm -f o e
+ git-rev-parse --verify "master@{2005-05-28}" >o 2>e &&
+ test $D = $(cat o) &&
+ test "warning: Log .git/logs/$m unexpectedly ended on $ld." = "$(cat e)"'
+
+
+rm -f .git/$m .git/logs/$m expect
+
+test_expect_success \
+ 'creating initial files' \
+ 'echo TEST >F &&
+ git-add F &&
+ GIT_AUTHOR_DATE="2005-05-26 23:30" \
+ GIT_COMMITTER_DATE="2005-05-26 23:30" git-commit -m add -a &&
+ h_TEST=$(git-rev-parse --verify HEAD)
+ echo The other day this did not work. >M &&
+ echo And then Bob told me how to fix it. >>M &&
+ echo OTHER >F &&
+ GIT_AUTHOR_DATE="2005-05-26 23:41" \
+ GIT_COMMITTER_DATE="2005-05-26 23:41" git-commit -F M -a &&
+ h_OTHER=$(git-rev-parse --verify HEAD)
+ rm -f M'
+
+cat >expect <<EOF
+$Z $h_TEST $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 commit: add
+$h_TEST $h_OTHER $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150860 +0000 commit: The other day this did not work.
+EOF
+test_expect_success \
+ 'git-commit logged updates' \
+ 'diff expect .git/logs/$m'
+unset h_TEST h_OTHER
+
+test_expect_success \
+ 'git-cat-file blob master:F (expect OTHER)' \
+ 'test OTHER = $(git-cat-file blob master:F)'
+test_expect_success \
+ 'git-cat-file blob master@{2005-05-26 23:30}:F (expect TEST)' \
+ 'test TEST = $(git-cat-file blob "master@{2005-05-26 23:30}:F")'
+test_expect_success \
+ 'git-cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)' \
+ 'test OTHER = $(git-cat-file blob "master@{2005-05-26 23:42}:F")'
+
+test_done
. ./test-lib.sh
+cat > expected <<\EOF
+100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0 file1
+100644 9db8893856a8a02eaa73470054b7c1c5a7c82e47 0 file2
+EOF
test_expect_success 'update-index --add' \
'echo hello world >file1 &&
echo goodbye people >file2 &&
git-update-index --add file1 file2 &&
git-ls-files -s >current &&
- cmp current - <<\EOF
-100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0 file1
-100644 9db8893856a8a02eaa73470054b7c1c5a7c82e47 0 file2
-EOF'
+ cmp current expected'
test_expect_success 'update-index --again' \
'rm -f file1 &&
echo happy - failed as expected
fi &&
git-ls-files -s >current &&
- cmp current - <<\EOF
-100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0 file1
-100644 9db8893856a8a02eaa73470054b7c1c5a7c82e47 0 file2
-EOF'
+ cmp current expected'
+cat > expected <<\EOF
+100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0 file2
+EOF
test_expect_success 'update-index --remove --again' \
'git-update-index --remove --again &&
git-ls-files -s >current &&
- cmp current - <<\EOF
-100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0 file2
-EOF'
+ cmp current expected'
test_expect_success 'first commit' 'git-commit -m initial'
+cat > expected <<\EOF
+100644 53ab446c3f4e42ce9bb728a0ccb283a101be4979 0 dir1/file3
+100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0 file2
+EOF
test_expect_success 'update-index again' \
'mkdir -p dir1 &&
echo hello world >dir1/file3 &&
echo happy >dir1/file3 &&
git-update-index --again &&
git-ls-files -s >current &&
- cmp current - <<\EOF
-100644 53ab446c3f4e42ce9bb728a0ccb283a101be4979 0 dir1/file3
-100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0 file2
-EOF'
+ cmp current expected'
+cat > expected <<\EOF
+100644 d7fb3f695f06c759dbf3ab00046e7cc2da22d10f 0 dir1/file3
+100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0 file2
+EOF
test_expect_success 'update-index --update from subdir' \
'echo not so happy >file2 &&
cd dir1 &&
git-update-index --again &&
cd .. &&
git-ls-files -s >current &&
- cmp current - <<\EOF
-100644 d7fb3f695f06c759dbf3ab00046e7cc2da22d10f 0 dir1/file3
-100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0 file2
-EOF'
+ cmp current expected'
+cat > expected <<\EOF
+100644 594fb5bb1759d90998e2bf2a38261ae8e243c760 0 dir1/file3
+100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0 file2
+EOF
test_expect_success 'update-index --update with pathspec' \
'echo very happy >file2 &&
cat file2 >dir1/file3 &&
git-update-index --again dir1/ &&
git-ls-files -s >current &&
- cmp current - <<\EOF
-100644 594fb5bb1759d90998e2bf2a38261ae8e243c760 0 dir1/file3
-100644 0f1ae1422c2bf43f117d3dbd715c988a9ed2103f 0 file2
-EOF'
+ cmp current expected'
test_done
'prepare an trivial repository' \
'echo Hello > A &&
git-update-index --add A &&
- git-commit -m "Initial commit."'
+ git-commit -m "Initial commit." &&
+ HEAD=$(git-rev-parse --verify HEAD)'
test_expect_success \
'git branch --help should return success now.' \
'git branch a/b/c should create a branch' \
'git-branch a/b/c && test -f .git/refs/heads/a/b/c'
+cat >expect <<EOF
+0000000000000000000000000000000000000000 $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 branch: Created from HEAD
+EOF
+test_expect_success \
+ 'git branch -l d/e/f should create a branch and a log' \
+ 'GIT_COMMITTER_DATE="2005-05-26 23:30" \
+ git-branch -l d/e/f &&
+ test -f .git/refs/heads/d/e/f &&
+ test -f .git/logs/refs/heads/d/e/f &&
+ diff expect .git/logs/refs/heads/d/e/f'
+
+test_expect_success \
+ 'git branch -d d/e/f should delete a branch and a log' \
+ 'git-branch -d d/e/f &&
+ test ! -f .git/refs/heads/d/e/f &&
+ test ! -f .git/logs/refs/heads/d/e/f'
+
+cat >expect <<EOF
+0000000000000000000000000000000000000000 $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 checkout: Created from master^0
+EOF
+test_expect_success \
+ 'git checkout -b g/h/i -l should create a branch and a log' \
+ 'GIT_COMMITTER_DATE="2005-05-26 23:30" \
+ git-checkout -b g/h/i -l master &&
+ test -f .git/refs/heads/g/h/i &&
+ test -f .git/logs/refs/heads/g/h/i &&
+ diff expect .git/logs/refs/heads/g/h/i'
+
test_done
t0=`git-write-tree`
echo "$t0" >t0
-echo 'just space
+cat > expected <<\EOF
+just space
no-funny
-"tabs\t,\" (dq) and spaces"' >expected
+"tabs\t,\" (dq) and spaces"
+EOF
test_expect_success 'git-ls-files with-funny' \
'git-update-index --add "$p1" &&
git-ls-files >current &&
t1=`git-write-tree`
echo "$t1" >t1
-echo 'just space
+cat > expected <<\EOF
+just space
no-funny
-"tabs\t,\" (dq) and spaces"' >expected
+"tabs\t,\" (dq) and spaces"
+EOF
test_expect_success 'git-ls-tree with funny' \
'git-ls-tree -r $t1 | sed -e "s/^[^ ]* //" >current &&
diff -u expected current'
-echo 'A "tabs\t,\" (dq) and spaces"' >expected
+cat > expected <<\EOF
+A "tabs\t,\" (dq) and spaces"
+EOF
test_expect_success 'git-diff-index with-funny' \
'git-diff-index --name-status $t0 >current &&
diff -u expected current'
'git-diff-tree -z --name-status $t0 $t1 | tr \\0 \\012 >current &&
diff -u expected current'
-echo 'CNUM no-funny "tabs\t,\" (dq) and spaces"' >expected
+cat > expected <<\EOF
+CNUM no-funny "tabs\t,\" (dq) and spaces"
+EOF
test_expect_success 'git-diff-tree -C with-funny' \
'git-diff-tree -C --find-copies-harder --name-status \
$t0 $t1 | sed -e 's/^C[0-9]*/CNUM/' >current &&
diff -u expected current'
-echo 'RNUM no-funny "tabs\t,\" (dq) and spaces"' >expected
+cat > expected <<\EOF
+RNUM no-funny "tabs\t,\" (dq) and spaces"
+EOF
test_expect_success 'git-diff-tree delete with-funny' \
'git-update-index --force-remove "$p0" &&
git-diff-index -M --name-status \
$t0 | sed -e 's/^R[0-9]*/RNUM/' >current &&
diff -u expected current'
-echo 'diff --git a/no-funny "b/tabs\t,\" (dq) and spaces"
+cat > expected <<\EOF
+diff --git a/no-funny "b/tabs\t,\" (dq) and spaces"
similarity index NUM%
rename from no-funny
-rename to "tabs\t,\" (dq) and spaces"' >expected
-
+rename to "tabs\t,\" (dq) and spaces"
+EOF
test_expect_success 'git-diff-tree delete with-funny' \
'git-diff-index -M -p $t0 |
sed -e "s/index [0-9]*%/index NUM%/" >current &&
diff -u expected current'
chmod +x "$p1"
-echo 'diff --git a/no-funny "b/tabs\t,\" (dq) and spaces"
+cat > expected <<\EOF
+diff --git a/no-funny "b/tabs\t,\" (dq) and spaces"
old mode 100644
new mode 100755
similarity index NUM%
rename from no-funny
-rename to "tabs\t,\" (dq) and spaces"' >expected
-
+rename to "tabs\t,\" (dq) and spaces"
+EOF
test_expect_success 'git-diff-tree delete with-funny' \
'git-diff-index -M -p $t0 |
sed -e "s/index [0-9]*%/index NUM%/" >current &&
diff -u expected current'
-echo >expected ' "tabs\t,\" (dq) and spaces"
- 1 files changed, 0 insertions(+), 0 deletions(-)'
+cat >expected <<\EOF
+ "tabs\t,\" (dq) and spaces"
+ 1 files changed, 0 insertions(+), 0 deletions(-)
+EOF
test_expect_success 'git-diff-tree rename with-funny applied' \
'git-diff-index -M -p $t0 |
git-apply --stat | sed -e "s/|.*//" -e "s/ *\$//" >current &&
diff -u expected current'
-echo >expected ' no-funny
+cat > expected <<\EOF
+ no-funny
"tabs\t,\" (dq) and spaces"
- 2 files changed, 3 insertions(+), 3 deletions(-)'
-
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+EOF
test_expect_success 'git-diff-tree delete with-funny applied' \
'git-diff-index -p $t0 |
git-apply --stat | sed -e "s/|.*//" -e "s/ *\$//" >current &&
. ./test-lib.sh
# Setup some files to be removed, some with funny characters
-touch -- foo bar baz 'space embedded' -q
-git-add -- foo bar baz 'space embedded' -q
-git-commit -m "add normal files"
-test_tabs=y
-if touch -- 'tab embedded' 'newline
-embedded'
-then
-git-add -- 'tab embedded' 'newline
+test_expect_success \
+ 'Initialize test directory' \
+ "touch -- foo bar baz 'space embedded' -q &&
+ git-add -- foo bar baz 'space embedded' -q &&
+ git-commit -m 'add normal files' &&
+ test_tabs=y &&
+ if touch -- 'tab embedded' 'newline
embedded'
-git-commit -m "add files with tabs and newlines"
-else
- say 'Your filesystem does not allow tabs in filenames.'
- test_tabs=n
-fi
+ then
+ git-add -- 'tab embedded' 'newline
+embedded' &&
+ git-commit -m 'add files with tabs and newlines'
+ else
+ say 'Your filesystem does not allow tabs in filenames.'
+ test_tabs=n
+ fi"
# Later we will try removing an unremovable path to make sure
# git-rm barfs, but if the test is run as root that cannot be
# arranged.
-: >test-file
-chmod a-w .
-rm -f test-file
-test -f test-file && test_failed_remove=y
-chmod 775 .
-rm -f test-file
+test_expect_success \
+ 'Determine rm behavior' \
+ ': >test-file
+ chmod a-w .
+ rm -f test-file
+ test -f test-file && test_failed_remove=y
+ chmod 775 .
+ rm -f test-file'
test_expect_success \
'Pre-check that foo exists and is in index before git-rm foo' \
echo git >c &&
cat b b >d'
-test_expect_success 'diff without --binary' \
- 'git-diff | git-apply --stat --summary >current &&
- cmp current - <<\EOF
+cat > expected <<\EOF
a | 2 +-
b | Bin
c | 2 +-
d | Bin
4 files changed, 2 insertions(+), 2 deletions(-)
-EOF'
+EOF
+test_expect_success 'diff without --binary' \
+ 'git-diff | git-apply --stat --summary >current &&
+ cmp current expected'
test_expect_success 'diff with --binary' \
'git-diff --binary | git-apply --stat --summary >current &&
- cmp current - <<\EOF
- a | 2 +-
- b | Bin
- c | 2 +-
- d | Bin
- 4 files changed, 2 insertions(+), 2 deletions(-)
-EOF'
+ cmp current expected'
# apply needs to be able to skip the binary material correctly
# in order to report the line number of a corrupt patch.
for j in 0 1 2 3
do
test $i -eq $j && continue
- diff -u frotz.$i frotz.$j |
- sed -e '
- /^---/s|.*|--- a/frotz|
- /^+++/s|.*|+++ b/frotz|' >diff.$i-$j
cat frotz.$i >frotz
test_expect_success \
"apply diff between $i and $j" \
- "git-apply <diff.$i-$j && diff frotz.$j frotz"
+ "git-apply <../t4101/diff.$i-$j && diff frotz.$j frotz"
done
done
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,3 @@
+ a
+ b
++c
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,2 @@
+ a
+-b
++b
+\ No newline at end of file
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,3 @@
+ a
+-b
++c
++b
+\ No newline at end of file
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,2 @@
+ a
+ b
+-c
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,2 @@
+ a
+-b
+-c
++b
+\ No newline at end of file
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,3 @@
+ a
+-b
+ c
++b
+\ No newline at end of file
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,2 @@
+ a
+-b
+\ No newline at end of file
++b
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,3 @@
+ a
+-b
+\ No newline at end of file
++b
++c
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,2 +1,3 @@
+ a
++c
+ b
+\ No newline at end of file
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,2 @@
+ a
+-c
+-b
+\ No newline at end of file
++b
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,3 @@
+ a
++b
+ c
+-b
+\ No newline at end of file
--- /dev/null
+--- a/frotz
++++ b/frotz
+@@ -1,3 +1,2 @@
+ a
+-c
+ b
+\ No newline at end of file
--- /dev/null
+#!/bin/sh
+#
+# Copyright (c) 2006 Catalin Marinas
+#
+
+test_description='git-apply trying to add an ending line.
+
+'
+. ./test-lib.sh
+
+# setup
+
+cat >test-patch <<\EOF
+diff --git a/file b/file
+--- a/file
++++ b/file
+@@ -1,2 +1,3 @@
+ a
+ b
++c
+EOF
+
+echo 'a' >file
+echo 'b' >>file
+echo 'c' >>file
+
+test_expect_success setup \
+ 'git-update-index --add file'
+
+# test
+
+test_expect_failure 'apply at the end' \
+ 'git-apply --index test-patch'
+
+cat >test-patch <<\EOF
+diff a/file b/file
+--- a/file
++++ b/file
+@@ -1,2 +1,3 @@
++a
+ b
+ c
+EOF
+
+echo >file 'a
+b
+c'
+git-update-index file
+
+test_expect_failure 'apply at the beginning' \
+ 'git-apply --index test-patch'
+
+test_done
--- /dev/null
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+test_description='git-mailinfo and git-mailsplit test'
+
+. ./test-lib.sh
+
+test_expect_success 'split sample box' \
+ 'git-mailsplit -o. ../t5100/sample.mbox >last &&
+ last=`cat last` &&
+ echo total is $last &&
+ test `cat last` = 5'
+
+for mail in `echo 00*`
+do
+ test_expect_success "mailinfo $mail" \
+ "git-mailinfo -u msg$mail patch$mail <$mail >info$mail &&
+ echo msg &&
+ diff ../t5100/msg$mail msg$mail &&
+ echo patch &&
+ diff ../t5100/patch$mail patch$mail &&
+ echo info &&
+ diff ../t5100/info$mail info$mail"
+done
+
+test_done
--- /dev/null
+Author: A U Thor
+Email: a.u.thor@example.com
+Subject: a commit.
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+
--- /dev/null
+Author: A U Thor
+Email: a.u.thor@example.com
+Subject: another patch
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+
--- /dev/null
+Author: A U Thor
+Email: a.u.thor@example.com
+Subject: third patch
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+
--- /dev/null
+Author: YOSHIFUJI Hideaki / 吉藤英明
+Email: yoshfuji@linux-ipv6.org
+Subject: GIT: Try all addresses for given remote name
+Date: Thu, 21 Jul 2005 09:10:36 -0400 (EDT)
+
--- /dev/null
+Author: David Kågedal
+Email: davidk@lysator.liu.se
+Subject: Fixed two bugs in git-cvsimport-script.
+Date: Mon, 15 Aug 2005 20:18:25 +0200
+
--- /dev/null
+Here is a patch from A U Thor.
+
--- /dev/null
+Here is a patch from A U Thor. This addresses the issue raised in the
+message:
+
+From: Nit Picker <nit.picker@example.net>
+Subject: foo is too old
+Message-Id: <nitpicker.12121212@example.net>
+
+Hopefully this would fix the problem stated there.
+
+
+I have included an extra blank line above, but it does not have to be
+stripped away here, along with the
+whitespaces at the end of the above line. They are expected to be squashed
+when the message is made into a commit log by stripspace,
+Also, there are three blank lines after this paragraph,
+two truly blank and another full of spaces in between.
+
+
+
+Hope this helps.
+
--- /dev/null
+Here is a patch from A U Thor. This addresses the issue raised in the
+message:
+
+From: Nit Picker <nit.picker@example.net>
+Subject: foo is too old
+Message-Id: <nitpicker.12121212@example.net>
+
+Hopefully this would fix the problem stated there.
+
--- /dev/null
+Hello.
+
+Try all addresses for given remote name until it succeeds.
+Also supports IPv6.
+
+Signed-of-by: Hideaki YOSHIFUJI <yoshfuji@linux-ipv6.org>
+
--- /dev/null
+The git-cvsimport-script had a copule of small bugs that prevented me
+from importing a big CVS repository.
+
+The first was that it didn't handle removed files with a multi-digit
+primary revision number.
+
+The second was that it was asking the CVS server for "F" messages,
+although they were not handled.
+
+I also updated the documentation for that script to correspond to
+actual flags.
+
+Signed-off-by: David Kågedal <davidk@lysator.liu.se>
--- /dev/null
+---
+ foo | 2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun 9 00:44:04 PDT 2006
++Fri Jun 9 00:44:13 PDT 2006
+--
+1.4.0.g6f2b
+
--- /dev/null
+---
+ foo | 2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun 9 00:44:04 PDT 2006
++Fri Jun 9 00:44:13 PDT 2006
+--
+1.4.0.g6f2b
+
--- /dev/null
+---
+ foo | 2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun 9 00:44:04 PDT 2006
++Fri Jun 9 00:44:13 PDT 2006
+--
+1.4.0.g6f2b
+
--- /dev/null
+diff --git a/connect.c b/connect.c
+--- a/connect.c
++++ b/connect.c
+@@ -96,42 +96,57 @@ static enum protocol get_protocol(const
+ die("I don't handle protocol '%s'", name);
+ }
+
+-static void lookup_host(const char *host, struct sockaddr *in)
+-{
+- struct addrinfo *res;
+- int ret;
+-
+- ret = getaddrinfo(host, NULL, NULL, &res);
+- if (ret)
+- die("Unable to look up %s (%s)", host, gai_strerror(ret));
+- *in = *res->ai_addr;
+- freeaddrinfo(res);
+-}
++#define STR_(s) # s
++#define STR(s) STR_(s)
+
+ static int git_tcp_connect(int fd[2], const char *prog, char *host, char *path)
+ {
+- struct sockaddr addr;
+- int port = DEFAULT_GIT_PORT, sockfd;
+- char *colon;
+-
+- colon = strchr(host, ':');
+- if (colon) {
+- char *end;
+- unsigned long n = strtoul(colon+1, &end, 0);
+- if (colon[1] && !*end) {
+- *colon = 0;
+- port = n;
++ int sockfd = -1;
++ char *colon, *end;
++ char *port = STR(DEFAULT_GIT_PORT);
++ struct addrinfo hints, *ai0, *ai;
++ int gai;
++
++ if (host[0] == '[') {
++ end = strchr(host + 1, ']');
++ if (end) {
++ *end = 0;
++ end++;
++ host++;
++ } else
++ end = host;
++ } else
++ end = host;
++ colon = strchr(end, ':');
++
++ if (colon)
++ port = colon + 1;
++
++ memset(&hints, 0, sizeof(hints));
++ hints.ai_socktype = SOCK_STREAM;
++ hints.ai_protocol = IPPROTO_TCP;
++
++ gai = getaddrinfo(host, port, &hints, &ai);
++ if (gai)
++ die("Unable to look up %s (%s)", host, gai_strerror(gai));
++
++ for (ai0 = ai; ai; ai = ai->ai_next) {
++ sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
++ if (sockfd < 0)
++ continue;
++ if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
++ close(sockfd);
++ sockfd = -1;
++ continue;
+ }
++ break;
+ }
+
+- lookup_host(host, &addr);
+- ((struct sockaddr_in *)&addr)->sin_port = htons(port);
++ freeaddrinfo(ai0);
+
+- sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_IP);
+ if (sockfd < 0)
+ die("unable to create socket (%s)", strerror(errno));
+- if (connect(sockfd, (void *)&addr, sizeof(addr)) < 0)
+- die("unable to connect (%s)", strerror(errno));
++
+ fd[0] = sockfd;
+ fd[1] = sockfd;
+ packet_write(sockfd, "%s %s\n", prog, path);
+
+--
+YOSHIFUJI Hideaki @ USAGI Project <yoshfuji@linux-ipv6.org>
+GPG-FP : 9022 65EB 1ECF 3AD1 0BDF 80D8 4807 F894 E062 0EEA
+
--- /dev/null
+---
+
+ Documentation/git-cvsimport-script.txt | 9 ++++++++-
+ git-cvsimport-script | 4 ++--
+ 2 files changed, 10 insertions(+), 3 deletions(-)
+
+50452f9c0c2df1f04d83a26266ba704b13861632
+diff --git a/Documentation/git-cvsimport-script.txt b/Documentation/git-cvsimport-script.txt
+--- a/Documentation/git-cvsimport-script.txt
++++ b/Documentation/git-cvsimport-script.txt
+@@ -29,6 +29,10 @@ OPTIONS
+ currently, only the :local:, :ext: and :pserver: access methods
+ are supported.
+
++-C <target-dir>::
++ The GIT repository to import to. If the directory doesn't
++ exist, it will be created. Default is the current directory.
++
+ -i::
+ Import-only: don't perform a checkout after importing. This option
+ ensures the working directory and cache remain untouched and will
+@@ -44,7 +48,7 @@ OPTIONS
+
+ -p <options-for-cvsps>::
+ Additional options for cvsps.
+- The options '-x' and '-A' are implicit and should not be used here.
++ The options '-u' and '-A' are implicit and should not be used here.
+
+ If you need to pass multiple options, separate them with a comma.
+
+@@ -57,6 +61,9 @@ OPTIONS
+ -h::
+ Print a short usage message and exit.
+
++-z <fuzz>::
++ Pass the timestamp fuzz factor to cvsps.
++
+ OUTPUT
+ ------
+ If '-v' is specified, the script reports what it is doing.
+diff --git a/git-cvsimport-script b/git-cvsimport-script
+--- a/git-cvsimport-script
++++ b/git-cvsimport-script
+@@ -190,7 +190,7 @@ sub conn {
+ $self->{'socketo'}->write("Root $repo\n");
+
+ # Trial and error says that this probably is the minimum set
+- $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mode M Mbinary E F Checked-in Created Updated Merged Removed\n");
++ $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mode M Mbinary E Checked-in Created Updated Merged Removed\n");
+
+ $self->{'socketo'}->write("valid-requests\n");
+ $self->{'socketo'}->flush();
+@@ -691,7 +691,7 @@ while(<CVS>) {
+ unlink($tmpname);
+ my $mode = pmode($cvs->{'mode'});
+ push(@new,[$mode, $sha, $fn]); # may be resurrected!
+- } elsif($state == 9 and /^\s+(\S+):\d(?:\.\d+)+->(\d(?:\.\d+)+)\(DEAD\)\s*$/) {
++ } elsif($state == 9 and /^\s+(\S+):\d+(?:\.\d+)+->(\d+(?:\.\d+)+)\(DEAD\)\s*$/) {
+ my $fn = $1;
+ $fn =~ s#^/+##;
+ push(@old,$fn);
+
+--
+David Kågedal
+-
+To unsubscribe from this list: send the line "unsubscribe git" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at http://vger.kernel.org/majordomo-info.html
+
--- /dev/null
+From nobody Mon Sep 17 00:00:00 2001
+From: A U Thor <a.u.thor@example.com>
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+Subject: [PATCH] a commit.
+
+Here is a patch from A U Thor.
+
+---
+ foo | 2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun 9 00:44:04 PDT 2006
++Fri Jun 9 00:44:13 PDT 2006
+--
+1.4.0.g6f2b
+
+From nobody Mon Sep 17 00:00:00 2001
+From: A U Thor <a.u.thor@example.com>
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+Subject: [PATCH] another patch
+
+Here is a patch from A U Thor. This addresses the issue raised in the
+message:
+
+From: Nit Picker <nit.picker@example.net>
+Subject: foo is too old
+Message-Id: <nitpicker.12121212@example.net>
+
+Hopefully this would fix the problem stated there.
+
+
+I have included an extra blank line above, but it does not have to be
+stripped away here, along with the
+whitespaces at the end of the above line. They are expected to be squashed
+when the message is made into a commit log by stripspace,
+Also, there are three blank lines after this paragraph,
+two truly blank and another full of spaces in between.
+
+
+
+Hope this helps.
+
+---
+ foo | 2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun 9 00:44:04 PDT 2006
++Fri Jun 9 00:44:13 PDT 2006
+--
+1.4.0.g6f2b
+
+From nobody Mon Sep 17 00:00:00 2001
+From: Junio C Hamano <junio@kernel.org>
+Date: Fri, 9 Jun 2006 00:44:16 -0700
+Subject: re: [PATCH] another patch
+
+From: A U Thor <a.u.thor@example.com>
+Subject: [PATCH] third patch
+
+Here is a patch from A U Thor. This addresses the issue raised in the
+message:
+
+From: Nit Picker <nit.picker@example.net>
+Subject: foo is too old
+Message-Id: <nitpicker.12121212@example.net>
+
+Hopefully this would fix the problem stated there.
+
+---
+ foo | 2 +-
+ 1 files changed, 1 insertions(+), 1 deletions(-)
+
+diff --git a/foo b/foo
+index 9123cdc..918dcf8 100644
+--- a/foo
++++ b/foo
+@@ -1 +1 @@
+-Fri Jun 9 00:44:04 PDT 2006
++Fri Jun 9 00:44:13 PDT 2006
+--
+1.4.0.g6f2b
+
+From nobody Sat Aug 27 23:07:49 2005
+Path: news.gmane.org!not-for-mail
+Message-ID: <20050721.091036.01119516.yoshfuji@linux-ipv6.org>
+From: YOSHIFUJI Hideaki / =?iso-2022-jp?B?GyRCNUhGIzFRTEAbKEI=?=
+ <yoshfuji@linux-ipv6.org>
+Newsgroups: gmane.comp.version-control.git
+Subject: [PATCH 1/2] GIT: Try all addresses for given remote name
+Date: Thu, 21 Jul 2005 09:10:36 -0400 (EDT)
+Lines: 99
+Organization: USAGI/WIDE Project
+Approved: news@gmane.org
+NNTP-Posting-Host: main.gmane.org
+Mime-Version: 1.0
+Content-Type: Text/Plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+X-Trace: sea.gmane.org 1121951434 29350 80.91.229.2 (21 Jul 2005 13:10:34 GMT)
+X-Complaints-To: usenet@sea.gmane.org
+NNTP-Posting-Date: Thu, 21 Jul 2005 13:10:34 +0000 (UTC)
+
+Hello.
+
+Try all addresses for given remote name until it succeeds.
+Also supports IPv6.
+
+Signed-of-by: Hideaki YOSHIFUJI <yoshfuji@linux-ipv6.org>
+
+diff --git a/connect.c b/connect.c
+--- a/connect.c
++++ b/connect.c
+@@ -96,42 +96,57 @@ static enum protocol get_protocol(const
+ die("I don't handle protocol '%s'", name);
+ }
+
+-static void lookup_host(const char *host, struct sockaddr *in)
+-{
+- struct addrinfo *res;
+- int ret;
+-
+- ret = getaddrinfo(host, NULL, NULL, &res);
+- if (ret)
+- die("Unable to look up %s (%s)", host, gai_strerror(ret));
+- *in = *res->ai_addr;
+- freeaddrinfo(res);
+-}
++#define STR_(s) # s
++#define STR(s) STR_(s)
+
+ static int git_tcp_connect(int fd[2], const char *prog, char *host, char *path)
+ {
+- struct sockaddr addr;
+- int port = DEFAULT_GIT_PORT, sockfd;
+- char *colon;
+-
+- colon = strchr(host, ':');
+- if (colon) {
+- char *end;
+- unsigned long n = strtoul(colon+1, &end, 0);
+- if (colon[1] && !*end) {
+- *colon = 0;
+- port = n;
++ int sockfd = -1;
++ char *colon, *end;
++ char *port = STR(DEFAULT_GIT_PORT);
++ struct addrinfo hints, *ai0, *ai;
++ int gai;
++
++ if (host[0] == '[') {
++ end = strchr(host + 1, ']');
++ if (end) {
++ *end = 0;
++ end++;
++ host++;
++ } else
++ end = host;
++ } else
++ end = host;
++ colon = strchr(end, ':');
++
++ if (colon)
++ port = colon + 1;
++
++ memset(&hints, 0, sizeof(hints));
++ hints.ai_socktype = SOCK_STREAM;
++ hints.ai_protocol = IPPROTO_TCP;
++
++ gai = getaddrinfo(host, port, &hints, &ai);
++ if (gai)
++ die("Unable to look up %s (%s)", host, gai_strerror(gai));
++
++ for (ai0 = ai; ai; ai = ai->ai_next) {
++ sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
++ if (sockfd < 0)
++ continue;
++ if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
++ close(sockfd);
++ sockfd = -1;
++ continue;
+ }
++ break;
+ }
+
+- lookup_host(host, &addr);
+- ((struct sockaddr_in *)&addr)->sin_port = htons(port);
++ freeaddrinfo(ai0);
+
+- sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_IP);
+ if (sockfd < 0)
+ die("unable to create socket (%s)", strerror(errno));
+- if (connect(sockfd, (void *)&addr, sizeof(addr)) < 0)
+- die("unable to connect (%s)", strerror(errno));
++
+ fd[0] = sockfd;
+ fd[1] = sockfd;
+ packet_write(sockfd, "%s %s\n", prog, path);
+
+--
+YOSHIFUJI Hideaki @ USAGI Project <yoshfuji@linux-ipv6.org>
+GPG-FP : 9022 65EB 1ECF 3AD1 0BDF 80D8 4807 F894 E062 0EEA
+
+From nobody Sat Aug 27 23:07:49 2005
+Path: news.gmane.org!not-for-mail
+Message-ID: <u5tacjjdpxq.fsf@lysator.liu.se>
+From: =?iso-8859-1?Q?David_K=E5gedal?= <davidk@lysator.liu.se>
+Newsgroups: gmane.comp.version-control.git
+Subject: [PATCH] Fixed two bugs in git-cvsimport-script.
+Date: Mon, 15 Aug 2005 20:18:25 +0200
+Lines: 83
+Approved: news@gmane.org
+NNTP-Posting-Host: main.gmane.org
+Mime-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+X-Trace: sea.gmane.org 1124130247 31839 80.91.229.2 (15 Aug 2005 18:24:07 GMT)
+X-Complaints-To: usenet@sea.gmane.org
+NNTP-Posting-Date: Mon, 15 Aug 2005 18:24:07 +0000 (UTC)
+Cc: "Junio C. Hamano" <junkio@cox.net>
+Original-X-From: git-owner@vger.kernel.org Mon Aug 15 20:24:05 2005
+
+The git-cvsimport-script had a copule of small bugs that prevented me
+from importing a big CVS repository.
+
+The first was that it didn't handle removed files with a multi-digit
+primary revision number.
+
+The second was that it was asking the CVS server for "F" messages,
+although they were not handled.
+
+I also updated the documentation for that script to correspond to
+actual flags.
+
+Signed-off-by: David K=E5gedal <davidk@lysator.liu.se>
+---
+
+ Documentation/git-cvsimport-script.txt | 9 ++++++++-
+ git-cvsimport-script | 4 ++--
+ 2 files changed, 10 insertions(+), 3 deletions(-)
+
+50452f9c0c2df1f04d83a26266ba704b13861632
+diff --git a/Documentation/git-cvsimport-script.txt b/Documentation/git=
+-cvsimport-script.txt
+--- a/Documentation/git-cvsimport-script.txt
++++ b/Documentation/git-cvsimport-script.txt
+@@ -29,6 +29,10 @@ OPTIONS
+ currently, only the :local:, :ext: and :pserver: access methods=20
+ are supported.
+=20
++-C <target-dir>::
++ The GIT repository to import to. If the directory doesn't
++ exist, it will be created. Default is the current directory.
++
+ -i::
+ Import-only: don't perform a checkout after importing. This option
+ ensures the working directory and cache remain untouched and will
+@@ -44,7 +48,7 @@ OPTIONS
+=20
+ -p <options-for-cvsps>::
+ Additional options for cvsps.
+- The options '-x' and '-A' are implicit and should not be used here.
++ The options '-u' and '-A' are implicit and should not be used here.
+=20
+ If you need to pass multiple options, separate them with a comma.
+=20
+@@ -57,6 +61,9 @@ OPTIONS
+ -h::
+ Print a short usage message and exit.
+=20
++-z <fuzz>::
++ Pass the timestamp fuzz factor to cvsps.
++
+ OUTPUT
+ ------
+ If '-v' is specified, the script reports what it is doing.
+diff --git a/git-cvsimport-script b/git-cvsimport-script
+--- a/git-cvsimport-script
++++ b/git-cvsimport-script
+@@ -190,7 +190,7 @@ sub conn {
+ $self->{'socketo'}->write("Root $repo\n");
+=20
+ # Trial and error says that this probably is the minimum set
+- $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mo=
+de M Mbinary E F Checked-in Created Updated Merged Removed\n");
++ $self->{'socketo'}->write("Valid-responses ok error Valid-requests Mo=
+de M Mbinary E Checked-in Created Updated Merged Removed\n");
+=20
+ $self->{'socketo'}->write("valid-requests\n");
+ $self->{'socketo'}->flush();
+@@ -691,7 +691,7 @@ while(<CVS>) {
+ unlink($tmpname);
+ my $mode =3D pmode($cvs->{'mode'});
+ push(@new,[$mode, $sha, $fn]); # may be resurrected!
+- } elsif($state =3D=3D 9 and /^\s+(\S+):\d(?:\.\d+)+->(\d(?:\.\d+)+)\(=
+DEAD\)\s*$/) {
++ } elsif($state =3D=3D 9 and /^\s+(\S+):\d+(?:\.\d+)+->(\d+(?:\.\d+)+)=
+\(DEAD\)\s*$/) {
+ my $fn =3D $1;
+ $fn =3D~ s#^/+##;
+ push(@old,$fn);
+
+--=20
+David K=E5gedal
+-
+To unsubscribe from this list: send the line "unsubscribe git" in
+the body of a message to majordomo@vger.kernel.org
+More majordomo info at http://vger.kernel.org/majordomo-info.html
+
# Some convenience functions
-function add () {
- local name=$1
- local text="$@"
- local branch=${name:0:1}
- local parents=""
+add () {
+ name=$1
+ text="$@"
+ branch=`echo $name | sed -e 's/^\(.\).*$/\1/'`
+ parents=""
shift
while test $1; do
eval ${branch}TIP=$commit
}
-function count_objects () {
+count_objects () {
ls .git/objects/??/* 2>>log2.txt | wc -l | tr -d " "
}
-function test_expect_object_count () {
- local message=$1
- local count=$2
+test_expect_object_count () {
+ message=$1
+ count=$2
output="$(count_objects)"
test_expect_success \
"test $count = $output"
}
-function pull_to_client () {
- local number=$1
- local heads=$2
- local count=$3
- local no_strict_count_check=$4
+pull_to_client () {
+ number=$1
+ heads=$2
+ count=$3
+ no_strict_count_check=$4
cd client
test_expect_success "$number pull" \
"git-fetch-pack -k -v .. $heads"
case "$heads" in *A*) echo $ATIP > .git/refs/heads/A;; esac
case "$heads" in *B*) echo $BTIP > .git/refs/heads/B;; esac
- git-symbolic-ref HEAD refs/heads/${heads:0:1}
+ git-symbolic-ref HEAD refs/heads/`echo $heads | sed -e 's/^\(.\).*$/\1/'`
test_expect_success "fsck" 'git-fsck-objects --full > fsck.txt 2>&1'
{
_date=$1
shift 1
- GIT_COMMITTER_DATE=$_date "$@"
+ export GIT_COMMITTER_DATE="$_date"
+ "$@"
+ unset GIT_COMMITTER_DATE
}
# Execute a command and suppress any error output.
--- /dev/null
+#!/bin/sh
+
+test_description='git-send-email'
+. ./test-lib.sh
+
+PROG='git send-email'
+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 \
+ 'Setup helper tool' \
+ '(echo "#!/bin/sh"
+ echo shift
+ echo for a
+ echo do
+ echo " echo \"!\$a!\""
+ echo "done >commandline"
+ echo "cat > msgtxt"
+ ) >fake.sendmail
+ chmod +x ./fake.sendmail
+ git add fake.sendmail
+ GIT_AUTHOR_NAME="A" git commit -a -m "Second."'
+
+test_expect_success \
+ 'Extract patches and send' \
+ 'git format-patch -n HEAD^1
+ git send-email -from="Example <nobody@example.com>" --to=nobody@example.com --smtp-server="$(pwd)/fake.sendmail" ./0001*txt'
+
+cat >expected <<\EOF
+!nobody@example.com!
+!author@example.com!
+EOF
+test_expect_success \
+ 'Verify commandline' \
+ 'diff commandline expected'
+
+test_done
void update_tree_entry(struct tree_desc *desc)
{
- void *buf = desc->buf;
+ const void *buf = desc->buf;
unsigned long size = desc->size;
int len = strlen(buf) + 1 + 20;
desc->size = size - len;
}
+static const char *get_mode(const char *str, unsigned int *modep)
+{
+ unsigned char c;
+ unsigned int mode = 0;
+
+ while ((c = *str++) != ' ') {
+ if (c < '0' || c > '7')
+ return NULL;
+ mode = (mode << 3) + (c - '0');
+ }
+ *modep = mode;
+ return str;
+}
+
const unsigned char *tree_entry_extract(struct tree_desc *desc, const char **pathp, unsigned int *modep)
{
- void *tree = desc->buf;
+ const void *tree = desc->buf;
unsigned long size = desc->size;
int len = strlen(tree)+1;
const unsigned char *sha1 = tree + len;
- const char *path = strchr(tree, ' ');
+ const char *path;
unsigned int mode;
- if (!path || size < len + 20 || sscanf(tree, "%o", &mode) != 1)
+ path = get_mode(tree, &mode);
+ if (!path || size < len + 20)
die("corrupt tree file");
- *pathp = path+1;
+ *pathp = path;
*modep = canon_mode(mode);
return sha1;
}
+int tree_entry(struct tree_desc *desc, struct name_entry *entry)
+{
+ const void *tree = desc->buf, *path;
+ unsigned long len, size = desc->size;
+
+ if (!size)
+ return 0;
+
+ path = get_mode(tree, &entry->mode);
+ if (!path)
+ die("corrupt tree file");
+
+ entry->path = path;
+ len = strlen(path);
+ entry->pathlen = len;
+
+ path += len + 1;
+ entry->sha1 = path;
+
+ path += 20;
+ len = path - tree;
+ if (len > size)
+ die("corrupt tree file");
+
+ desc->buf = path;
+ desc->size = size - len;
+ return 1;
+}
+
void traverse_trees(int n, struct tree_desc *t, const char *base, traverse_callback_t callback)
{
struct name_entry *entry = xmalloc(n*sizeof(*entry));
#define TREE_WALK_H
struct tree_desc {
- void *buf;
+ const void *buf;
unsigned long size;
};
void update_tree_entry(struct tree_desc *);
const unsigned char *tree_entry_extract(struct tree_desc *, const char **, unsigned int *);
+/* Helper function that does both of the above and returns true for success */
+int tree_entry(struct tree_desc *, struct name_entry *);
+
void *fill_tree_descriptor(struct tree_desc *desc, const unsigned char *sha1);
typedef void (*traverse_callback_t)(int n, unsigned long mask, struct name_entry *entry, const char *base);
#include "blob.h"
#include "commit.h"
#include "tag.h"
+#include "tree-walk.h"
#include <stdlib.h>
const char *tree_type = "tree";
-static int read_one_entry(unsigned char *sha1, const char *base, int baselen, const char *pathname, unsigned mode, int stage)
+static int read_one_entry(const unsigned char *sha1, const char *base, int baselen, const char *pathname, unsigned mode, int stage)
{
int len;
unsigned int size;
int stage, const char **match,
read_tree_fn_t fn)
{
- struct tree_entry_list *list;
+ struct tree_desc desc;
+ struct name_entry entry;
+
if (parse_tree(tree))
return -1;
- list = tree->entries;
- while (list) {
- struct tree_entry_list *current = list;
- list = list->next;
- if (!match_tree_entry(base, baselen, current->name,
- current->mode, match))
+
+ desc.buf = tree->buffer;
+ desc.size = tree->size;
+
+ while (tree_entry(&desc, &entry)) {
+ if (!match_tree_entry(base, baselen, entry.path, entry.mode, match))
continue;
- switch (fn(current->item.any->sha1, base, baselen,
- current->name, current->mode, stage)) {
+ switch (fn(entry.sha1, base, baselen, entry.path, entry.mode, stage)) {
case 0:
continue;
case READ_TREE_RECURSIVE:
default:
return -1;
}
- if (current->directory) {
+ if (S_ISDIR(entry.mode)) {
int retval;
- int pathlen = strlen(current->name);
char *newbase;
- newbase = xmalloc(baselen + 1 + pathlen);
+ newbase = xmalloc(baselen + 1 + entry.pathlen);
memcpy(newbase, base, baselen);
- memcpy(newbase + baselen, current->name, pathlen);
- newbase[baselen + pathlen] = '/';
- retval = read_tree_recursive(current->item.tree,
+ memcpy(newbase + baselen, entry.path, entry.pathlen);
+ newbase[baselen + entry.pathlen] = '/';
+ retval = read_tree_recursive(lookup_tree(entry.sha1),
newbase,
- baselen + pathlen + 1,
+ baselen + entry.pathlen + 1,
stage, match, fn);
free(newbase);
if (retval)
return (struct tree *) obj;
}
-int parse_tree_buffer(struct tree *item, void *buffer, unsigned long size)
+static int track_tree_refs(struct tree *item)
{
- void *bufptr = buffer;
- struct tree_entry_list **list_p;
- int n_refs = 0;
+ int n_refs = 0, i;
+ struct object_refs *refs;
+ struct tree_desc desc;
+ struct name_entry entry;
+
+ /* Count how many entries there are.. */
+ desc.buf = item->buffer;
+ desc.size = item->size;
+ while (desc.size) {
+ n_refs++;
+ update_tree_entry(&desc);
+ }
- if (item->object.parsed)
- return 0;
- item->object.parsed = 1;
- list_p = &item->entries;
- while (size) {
+ /* Allocate object refs and walk it again.. */
+ i = 0;
+ refs = alloc_object_refs(n_refs);
+ desc.buf = item->buffer;
+ desc.size = item->size;
+ while (tree_entry(&desc, &entry)) {
struct object *obj;
- struct tree_entry_list *entry;
- int len = 1+strlen(bufptr);
- unsigned char *file_sha1 = bufptr + len;
- char *path = strchr(bufptr, ' ');
- unsigned int mode;
- if (size < len + 20 || !path ||
- sscanf(bufptr, "%o", &mode) != 1)
- return -1;
- entry = xmalloc(sizeof(struct tree_entry_list));
- entry->name = strdup(path + 1);
- entry->directory = S_ISDIR(mode) != 0;
- entry->executable = (mode & S_IXUSR) != 0;
- entry->symlink = S_ISLNK(mode) != 0;
- entry->zeropad = *(char *)bufptr == '0';
- entry->mode = mode;
- entry->next = NULL;
-
- bufptr += len + 20;
- size -= len + 20;
-
- if (entry->directory) {
- entry->item.tree = lookup_tree(file_sha1);
- obj = &entry->item.tree->object;
- } else {
- entry->item.blob = lookup_blob(file_sha1);
- obj = &entry->item.blob->object;
- }
- if (obj)
- n_refs++;
- *list_p = entry;
- list_p = &entry->next;
+ if (S_ISDIR(entry.mode))
+ obj = &lookup_tree(entry.sha1)->object;
+ else
+ obj = &lookup_blob(entry.sha1)->object;
+ refs->ref[i++] = obj;
}
+ set_object_refs(&item->object, refs);
+ return 0;
+}
- if (track_object_refs) {
- struct tree_entry_list *entry;
- unsigned i = 0;
- struct object_refs *refs = alloc_object_refs(n_refs);
- for (entry = item->entries; entry; entry = entry->next)
- refs->ref[i++] = entry->item.any;
- set_object_refs(&item->object, refs);
- }
+int parse_tree_buffer(struct tree *item, void *buffer, unsigned long size)
+{
+ if (item->object.parsed)
+ return 0;
+ item->object.parsed = 1;
+ item->buffer = buffer;
+ item->size = size;
+ if (track_object_refs)
+ track_tree_refs(item);
return 0;
}
char type[20];
void *buffer;
unsigned long size;
- int ret;
if (item->object.parsed)
return 0;
return error("Object %s not a tree",
sha1_to_hex(item->object.sha1));
}
- ret = parse_tree_buffer(item, buffer, size);
- free(buffer);
- return ret;
+ return parse_tree_buffer(item, buffer, size);
}
struct tree *parse_tree_indirect(const unsigned char *sha1)
extern const char *tree_type;
-struct tree_entry_list {
- struct tree_entry_list *next;
- unsigned directory : 1;
- unsigned executable : 1;
- unsigned symlink : 1;
- unsigned zeropad : 1;
- unsigned int mode;
- char *name;
- union {
- struct object *any;
- struct tree *tree;
- struct blob *blob;
- } item;
-};
-
struct tree {
struct object object;
- struct tree_entry_list *entries;
+ void *buffer;
+ unsigned long size;
};
struct tree *lookup_tree(const unsigned char *sha1);
struct tree *parse_tree_indirect(const unsigned char *sha1);
#define READ_TREE_RECURSIVE 1
-typedef int (*read_tree_fn_t)(unsigned char *, const char *, int, const char *, unsigned int, int);
+typedef int (*read_tree_fn_t)(const unsigned char *, const char *, int, const char *, unsigned int, int);
extern int read_tree_recursive(struct tree *tree,
const char *base, int baselen,
#include "cache.h"
#include "strbuf.h"
#include "quote.h"
+#include "cache-tree.h"
#include "tree-walk.h"
/*
active_cache[pos]->ce_flags &= ~htons(CE_VALID);
break;
}
+ cache_tree_invalidate_path(active_cache_tree, path);
active_cache_changed = 1;
return 0;
}
struct stat st;
status = lstat(path, &st);
+
+ /* We probably want to do this in remove_file_from_cache() and
+ * add_cache_entry() instead...
+ */
+ cache_tree_invalidate_path(active_cache_tree, path);
+
if (status < 0 || S_ISDIR(st.st_mode)) {
/* When we used to have "path" and now we want to add
* "path/file", we need a way to remove "path" before
return error("%s: cannot add to the index - missing --add option?",
path);
report("add '%s'", path);
+ cache_tree_invalidate_path(active_cache_tree, path);
return 0;
}
default:
goto fail;
}
+ cache_tree_invalidate_path(active_cache_tree, path);
active_cache_changed = 1;
report("chmod %cx '%s'", flip, path);
return;
die("git-update-index: cannot chmod %cx '%s'", flip, path);
}
-static struct cache_file cache_file;
+static struct lock_file lock_file;
static void update_one(const char *path, const char *prefix, int prefix_length)
{
die("Unable to mark file %s", path);
goto free_return;
}
+ cache_tree_invalidate_path(active_cache_tree, path);
if (force_remove) {
if (remove_file_from_cache(p))
free(path_name);
continue;
}
+ cache_tree_invalidate_path(active_cache_tree, path_name);
if (!mode) {
/* mode == 0 means there is no such path -- remove */
goto free_return;
}
+ cache_tree_invalidate_path(active_cache_tree, path);
remove_file_from_cache(path);
if (add_cache_entry(ce_2, ADD_CACHE_OK_TO_ADD)) {
error("%s: cannot add our version to the index.", path);
git_config(git_default_config);
- newfd = hold_index_file_for_update(&cache_file, get_index_file());
+ newfd = hold_lock_file_for_update(&lock_file, get_index_file());
if (newfd < 0)
- die("unable to create new cachefile");
+ die("unable to create new index file");
entries = read_cache();
if (entries < 0)
finish:
if (active_cache_changed) {
if (write_cache(newfd, active_cache, active_nr) ||
- commit_index_file(&cache_file))
- die("Unable to write new cachefile");
+ commit_lock_file(&lock_file))
+ die("Unable to write new index file");
}
return has_errors ? 1 : 0;
#include "cache.h"
#include "refs.h"
-static const char git_update_ref_usage[] = "git-update-ref <refname> <value> [<oldval>]";
-
-static int re_verify(const char *path, unsigned char *oldsha1, unsigned char *currsha1)
-{
- char buf[40];
- int fd = open(path, O_RDONLY), nr;
- if (fd < 0)
- return -1;
- nr = read(fd, buf, 40);
- close(fd);
- if (nr != 40 || get_sha1_hex(buf, currsha1) < 0)
- return -1;
- return memcmp(oldsha1, currsha1, 20) ? -1 : 0;
-}
+static const char git_update_ref_usage[] =
+"git-update-ref <refname> <value> [<oldval>] [-m <reason>]";
int main(int argc, char **argv)
{
- char *hex;
- const char *refname, *value, *oldval, *path;
- char *lockpath;
- unsigned char sha1[20], oldsha1[20], currsha1[20];
- int fd, written;
+ const char *refname=NULL, *value=NULL, *oldval=NULL, *msg=NULL;
+ struct ref_lock *lock;
+ unsigned char sha1[20], oldsha1[20];
+ int i;
setup_git_directory();
git_config(git_default_config);
- if (argc < 3 || argc > 4)
+
+ for (i = 1; i < argc; i++) {
+ if (!strcmp("-m", argv[i])) {
+ if (i+1 >= argc)
+ usage(git_update_ref_usage);
+ msg = argv[++i];
+ if (!*msg)
+ die("Refusing to perform update with empty message.");
+ if (strchr(msg, '\n'))
+ die("Refusing to perform update with \\n in message.");
+ continue;
+ }
+ if (!refname) {
+ refname = argv[i];
+ continue;
+ }
+ if (!value) {
+ value = argv[i];
+ continue;
+ }
+ if (!oldval) {
+ oldval = argv[i];
+ continue;
+ }
+ }
+ if (!refname || !value)
usage(git_update_ref_usage);
- refname = argv[1];
- value = argv[2];
- oldval = argv[3];
if (get_sha1(value, sha1))
die("%s: not a valid SHA1", value);
memset(oldsha1, 0, 20);
if (oldval && get_sha1(oldval, oldsha1))
die("%s: not a valid old SHA1", oldval);
- path = resolve_ref(git_path("%s", refname), currsha1, !!oldval);
- if (!path)
- die("No such ref: %s", refname);
-
- if (oldval) {
- if (memcmp(currsha1, oldsha1, 20))
- die("Ref %s is at %s but expected %s", refname, sha1_to_hex(currsha1), sha1_to_hex(oldsha1));
- /* Nothing to do? */
- if (!memcmp(oldsha1, sha1, 20))
- exit(0);
- }
- path = strdup(path);
- lockpath = mkpath("%s.lock", path);
- if (safe_create_leading_directories(lockpath) < 0)
- die("Unable to create all of %s", lockpath);
-
- fd = open(lockpath, O_CREAT | O_EXCL | O_WRONLY, 0666);
- if (fd < 0)
- die("Unable to create %s", lockpath);
- hex = sha1_to_hex(sha1);
- hex[40] = '\n';
- written = write(fd, hex, 41);
- close(fd);
- if (written != 41) {
- unlink(lockpath);
- die("Unable to write to %s", lockpath);
- }
-
- /*
- * Re-read the ref after getting the lock to verify
- */
- if (oldval && re_verify(path, oldsha1, currsha1) < 0) {
- unlink(lockpath);
- die("Ref lock failed");
- }
-
- /*
- * Finally, replace the old ref with the new one
- */
- if (rename(lockpath, path) < 0) {
- unlink(lockpath);
- die("Unable to create %s", path);
- }
+ lock = lock_any_ref_for_update(refname, oldval ? oldsha1 : NULL, 0);
+ if (!lock)
+ return 1;
+ if (write_ref_sha1(lock, sha1, msg) < 0)
+ return 1;
return 0;
}
*/
#include "cache.h"
#include "tree.h"
+#include "cache-tree.h"
static int missing_ok = 0;
+static char *prefix = NULL;
-static int check_valid_sha1(unsigned char *sha1)
-{
- int ret;
-
- /* If we were anal, we'd check that the sha1 of the contents actually matches */
- ret = has_sha1_file(sha1);
- if (ret == 0)
- perror(sha1_file_name(sha1));
- return ret ? 0 : -1;
-}
-
-static int write_tree(struct cache_entry **cachep, int maxentries, const char *base, int baselen, unsigned char *returnsha1)
-{
- unsigned char subdir_sha1[20];
- unsigned long size, offset;
- char *buffer;
- int nr;
-
- /* Guess at some random initial size */
- size = 8192;
- buffer = xmalloc(size);
- offset = 0;
-
- nr = 0;
- while (nr < maxentries) {
- struct cache_entry *ce = cachep[nr];
- const char *pathname = ce->name, *filename, *dirname;
- int pathlen = ce_namelen(ce), entrylen;
- unsigned char *sha1;
- unsigned int mode;
-
- /* Did we hit the end of the directory? Return how many we wrote */
- if (baselen >= pathlen || memcmp(base, pathname, baselen))
- break;
-
- sha1 = ce->sha1;
- mode = ntohl(ce->ce_mode);
-
- /* Do we have _further_ subdirectories? */
- filename = pathname + baselen;
- dirname = strchr(filename, '/');
- if (dirname) {
- int subdir_written;
-
- subdir_written = write_tree(cachep + nr, maxentries - nr, pathname, dirname-pathname+1, subdir_sha1);
- nr += subdir_written;
-
- /* Now we need to write out the directory entry into this tree.. */
- mode = S_IFDIR;
- pathlen = dirname - pathname;
-
- /* ..but the directory entry doesn't count towards the total count */
- nr--;
- sha1 = subdir_sha1;
- }
+static const char write_tree_usage[] =
+"git-write-tree [--missing-ok] [--prefix=<prefix>/]";
- if (!missing_ok && check_valid_sha1(sha1) < 0)
- exit(1);
-
- entrylen = pathlen - baselen;
- if (offset + entrylen + 100 > size) {
- size = alloc_nr(offset + entrylen + 100);
- buffer = xrealloc(buffer, size);
- }
- offset += sprintf(buffer + offset, "%o %.*s", mode, entrylen, filename);
- buffer[offset++] = 0;
- memcpy(buffer + offset, sha1, 20);
- offset += 20;
- nr++;
- }
-
- write_sha1_file(buffer, offset, tree_type, returnsha1);
- free(buffer);
- return nr;
-}
-
-static const char write_tree_usage[] = "git-write-tree [--missing-ok]";
+static struct lock_file lock_file;
int main(int argc, char **argv)
{
- int i, funny;
- int entries;
- unsigned char sha1[20];
-
+ int entries, was_valid, newfd;
+
setup_git_directory();
+ newfd = hold_lock_file_for_update(&lock_file, get_index_file());
entries = read_cache();
- if (argc == 2) {
- if (!strcmp(argv[1], "--missing-ok"))
+
+ while (1 < argc) {
+ char *arg = argv[1];
+ if (!strcmp(arg, "--missing-ok"))
missing_ok = 1;
+ else if (!strncmp(arg, "--prefix=", 9))
+ prefix = arg + 9;
else
die(write_tree_usage);
+ argc--; argv++;
}
-
+
if (argc > 2)
die("too many options");
if (entries < 0)
die("git-write-tree: error reading cache");
- /* Verify that the tree is merged */
- funny = 0;
- for (i = 0; i < entries; i++) {
- struct cache_entry *ce = active_cache[i];
- if (ce_stage(ce)) {
- if (10 < ++funny) {
- fprintf(stderr, "...\n");
- break;
- }
- fprintf(stderr, "%s: unmerged (%s)\n", ce->name, sha1_to_hex(ce->sha1));
+ if (!active_cache_tree)
+ active_cache_tree = cache_tree();
+
+ was_valid = cache_tree_fully_valid(active_cache_tree);
+ if (!was_valid) {
+ if (cache_tree_update(active_cache_tree,
+ active_cache, active_nr,
+ missing_ok, 0) < 0)
+ die("git-write-tree: error building trees");
+ if (0 <= newfd) {
+ if (!write_cache(newfd, active_cache, active_nr))
+ commit_lock_file(&lock_file);
}
- }
- if (funny)
- die("git-write-tree: not able to write tree");
-
- /* Also verify that the cache does not have path and path/file
- * at the same time. At this point we know the cache has only
- * stage 0 entries.
- */
- funny = 0;
- for (i = 0; i < entries - 1; i++) {
- /* path/file always comes after path because of the way
- * the cache is sorted. Also path can appear only once,
- * which means conflicting one would immediately follow.
+ /* Not being able to write is fine -- we are only interested
+ * in updating the cache-tree part, and if the next caller
+ * ends up using the old index with unupdated cache-tree part
+ * it misses the work we did here, but that is just a
+ * performance penalty and not a big deal.
*/
- const char *this_name = active_cache[i]->name;
- const char *next_name = active_cache[i+1]->name;
- int this_len = strlen(this_name);
- if (this_len < strlen(next_name) &&
- strncmp(this_name, next_name, this_len) == 0 &&
- next_name[this_len] == '/') {
- if (10 < ++funny) {
- fprintf(stderr, "...\n");
- break;
- }
- fprintf(stderr, "You have both %s and %s\n",
- this_name, next_name);
- }
}
- if (funny)
- die("git-write-tree: not able to write tree");
-
- /* Ok, write it out */
- if (write_tree(active_cache, entries, "", 0, sha1) != entries)
- die("git-write-tree: internal error");
- printf("%s\n", sha1_to_hex(sha1));
+ if (prefix) {
+ struct cache_tree *subtree =
+ cache_tree_find(active_cache_tree, prefix);
+ printf("%s\n", sha1_to_hex(subtree->sha1));
+ }
+ else
+ printf("%s\n", sha1_to_hex(active_cache_tree->sha1));
return 0;
}