diff options
804 files changed, 160693 insertions, 20 deletions
diff --git a/.gitignore b/.gitignore index c714d382e8..d99372afc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,162 @@ +GIT-CFLAGS GIT-VERSION-FILE -git-citool -git-gui +git +git-add +git-add--interactive +git-am +git-annotate +git-apply +git-applymbox +git-applypatch +git-archimport +git-archive +git-bisect +git-blame +git-branch +git-cat-file +git-check-ref-format +git-checkout +git-checkout-index +git-cherry +git-cherry-pick +git-clean +git-clone +git-commit +git-commit-tree +git-config +git-convert-objects +git-count-objects +git-cvsexportcommit +git-cvsimport +git-cvsserver +git-daemon +git-diff +git-diff-files +git-diff-index +git-diff-stages +git-diff-tree +git-describe +git-fast-import +git-fetch +git-fetch-pack +git-findtags +git-fmt-merge-msg +git-for-each-ref +git-format-patch +git-fsck +git-fsck-objects +git-gc +git-get-tar-commit-id +git-grep +git-hash-object +git-http-fetch +git-http-push +git-imap-send +git-index-pack +git-init +git-init-db +git-instaweb +git-local-fetch +git-log +git-lost-found +git-ls-files +git-ls-remote +git-ls-tree +git-mailinfo +git-mailsplit +git-merge +git-merge-base +git-merge-index +git-merge-file +git-merge-tree +git-merge-octopus +git-merge-one-file +git-merge-ours +git-merge-recursive +git-merge-resolve +git-merge-stupid +git-mktag +git-mktree +git-name-rev +git-mv +git-pack-redundant +git-pack-objects +git-pack-refs +git-parse-remote +git-patch-id +git-peek-remote +git-prune +git-prune-packed +git-pull +git-push +git-quiltimport +git-read-tree +git-rebase +git-receive-pack +git-reflog +git-relink +git-remote +git-repack +git-repo-config +git-request-pull +git-rerere +git-reset +git-resolve +git-rev-list +git-rev-parse +git-revert +git-rm +git-runstatus +git-send-email +git-send-pack +git-sh-setup +git-shell +git-shortlog +git-show +git-show-branch +git-show-index +git-show-ref +git-ssh-fetch +git-ssh-pull +git-ssh-push +git-ssh-upload +git-status +git-stripspace +git-svn +git-svnimport +git-symbolic-ref +git-tag +git-tar-tree +git-unpack-file +git-unpack-objects +git-update-index +git-update-ref +git-update-server-info +git-upload-archive +git-upload-pack +git-var +git-verify-pack +git-verify-tag +git-whatchanged +git-write-tree +git-core-*/?* +gitweb/gitweb.cgi +test-date +test-delta +test-dump-cache-tree +common-cmds.h +*.tar.gz +*.dsc +*.deb +git-core.spec +*.exe +*.[ao] +*.py[co] +config.mak +autom4te.cache +config.cache +config.log +config.status +config.mak.autogen +config.mak.append +configure diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000..c7a3a75f33 --- /dev/null +++ b/.mailmap @@ -0,0 +1,39 @@ +# +# This list is used by git-shortlog to fix a few botched name translations +# in the git archive, either because the author's full name was messed up +# and/or not always written the same way, making contributions from the +# same person appearing not to be so. +# + +Aneesh Kumar K.V <aneesh.kumar@gmail.com> +Chris Shoemaker <c.shoemaker@cox.net> +Daniel Barkalow <barkalow@iabervon.org> +David KÃ¥gedal <davidk@lysator.liu.se> +Fredrik Kuivinen <freku045@student.liu.se> +H. Peter Anvin <hpa@bonde.sc.orionmulti.com> +H. Peter Anvin <hpa@tazenda.sc.orionmulti.com> +H. Peter Anvin <hpa@trantor.hos.anvin.org> +Horst H. von Brand <vonbrand@inf.utfsm.cl> +Joachim Berdal Haga <cjhaga@fys.uio.no> +Jon Loeliger <jdl@freescale.com> +Jon Seymour <jon@blackcubes.dyndns.org> +Karl Hasselström <kha@treskal.com> +Kent Engstrom <kent@lysator.liu.se> +Lars Doelle <lars.doelle@on-line.de> +Lars Doelle <lars.doelle@on-line ! de> +Lukas Sandström <lukass@etek.chalmers.se> +Martin Langhoff <martin@catalyst.net.nz> +Nguyá»…n Thái Ngá»c Duy <pclouds@gmail.com> +Ramsay Allan Jones <ramsay@ramsay1.demon.co.uk> +René Scharfe <rene.scharfe@lsrfire.ath.cx> +Robert Fitzsimons <robfitz@273k.net> +Santi Béjar <sbejar@gmail.com> +Sean Estabrooks <seanlkml@sympatico.ca> +Shawn O. Pearce <spearce@spearce.org> +Theodore Ts'o <tytso@mit.edu> +Tony Luck <tony.luck@intel.com> +Uwe Kleine-König <zeisberg@informatik.uni-freiburg.de> +Ville Skyttä <scop@xemacs.org> +YOSHIFUJI Hideaki <yoshfuji@linux-ipv6.org> +anonymous <linux@horizon.com> +anonymous <linux@horizon.net> diff --git a/COPYING b/COPYING new file mode 100644 index 0000000000..6ff87c4664 --- /dev/null +++ b/COPYING @@ -0,0 +1,361 @@ + + Note that the only valid version of the GPL as far as this project + is concerned is _this_ particular version of the license (ie v2, not + v2.2 or v3.x or whatever), unless explicitly otherwise stated. + + HOWEVER, in order to allow a migration to GPLv3 if that seems like + a good idea, I also ask that people involved with the project make + their preferences known. In particular, if you trust me to make that + decision, you might note so in your copyright message, ie something + like + + This file is licensed under the GPL v2, or a later version + at the discretion of Linus. + + might avoid issues. But we can also just decide to synchronize and + contact all copyright holders on record if/when the occasion arises. + + Linus Torvalds + +---------------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/Documentation/.gitignore b/Documentation/.gitignore new file mode 100644 index 0000000000..6a51331b2f --- /dev/null +++ b/Documentation/.gitignore @@ -0,0 +1,7 @@ +*.xml +*.html +*.1 +*.7 +howto-index.txt +doc.dep +cmds-*.txt diff --git a/Documentation/Makefile b/Documentation/Makefile new file mode 100644 index 0000000000..aaf95918c3 --- /dev/null +++ b/Documentation/Makefile @@ -0,0 +1,133 @@ +MAN1_TXT= \ + $(filter-out $(addsuffix .txt, $(ARTICLES) $(SP_ARTICLES)), \ + $(wildcard git-*.txt)) \ + gitk.txt +MAN7_TXT=git.txt + +DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT) $(MAN7_TXT)) + +ARTICLES = tutorial +ARTICLES += tutorial-2 +ARTICLES += core-tutorial +ARTICLES += cvs-migration +ARTICLES += diffcore +ARTICLES += howto-index +ARTICLES += repository-layout +ARTICLES += hooks +ARTICLES += everyday +ARTICLES += git-tools +# with their own formatting rules. +SP_ARTICLES = glossary howto/revert-branch-rebase user-manual + +DOC_HTML += $(patsubst %,%.html,$(ARTICLES) $(SP_ARTICLES)) + +DOC_MAN1=$(patsubst %.txt,%.1,$(MAN1_TXT)) +DOC_MAN7=$(patsubst %.txt,%.7,$(MAN7_TXT)) + +prefix?=$(HOME) +bindir?=$(prefix)/bin +mandir?=$(prefix)/man +man1dir=$(mandir)/man1 +man7dir=$(mandir)/man7 +# DESTDIR= + +ASCIIDOC=asciidoc +INSTALL?=install +DOC_REF = origin/man + +-include ../config.mak.autogen + +# +# Please note that there is a minor bug in asciidoc. +# The version after 6.0.3 _will_ include the patch found here: +# http://marc.theaimsgroup.com/?l=git&m=111558757202243&w=2 +# +# Until that version is released you may have to apply the patch +# yourself - yes, all 6 characters of it! +# + +all: html man + +html: $(DOC_HTML) + +$(DOC_HTML) $(DOC_MAN1) $(DOC_MAN7): asciidoc.conf + +man: man1 man7 +man1: $(DOC_MAN1) +man7: $(DOC_MAN7) + +install: man + $(INSTALL) -d -m755 $(DESTDIR)$(man1dir) $(DESTDIR)$(man7dir) + $(INSTALL) -m644 $(DOC_MAN1) $(DESTDIR)$(man1dir) + $(INSTALL) -m644 $(DOC_MAN7) $(DESTDIR)$(man7dir) + + +# +# Determine "include::" file references in asciidoc files. +# +doc.dep : $(wildcard *.txt) build-docdep.perl + rm -f $@+ $@ + perl ./build-docdep.perl >$@+ + mv $@+ $@ + +-include doc.dep + +cmds_txt = cmds-ancillaryinterrogators.txt \ + cmds-ancillarymanipulators.txt \ + cmds-mainporcelain.txt \ + cmds-plumbinginterrogators.txt \ + cmds-plumbingmanipulators.txt \ + cmds-synchingrepositories.txt \ + cmds-synchelpers.txt \ + cmds-purehelpers.txt \ + cmds-foreignscminterface.txt + +$(cmds_txt): cmd-list.perl $(MAN1_TXT) + perl ./cmd-list.perl + +git.7 git.html: git.txt core-intro.txt + +clean: + rm -f *.xml *.html *.1 *.7 howto-index.txt howto/*.html doc.dep + rm -f $(cmds_txt) + +%.html : %.txt + $(ASCIIDOC) -b xhtml11 -d manpage -f asciidoc.conf $< + +%.1 %.7 : %.xml + xmlto -m callouts.xsl man $< + +%.xml : %.txt + $(ASCIIDOC) -b docbook -d manpage -f asciidoc.conf $< + +user-manual.xml: user-manual.txt user-manual.conf + $(ASCIIDOC) -b docbook -d book $< + +user-manual.html: user-manual.xml + xmlto html-nochunks $< + +glossary.html : glossary.txt sort_glossary.pl + cat $< | \ + perl sort_glossary.pl | \ + $(ASCIIDOC) -b xhtml11 - > glossary.html + +howto-index.txt: howto-index.sh $(wildcard howto/*.txt) + rm -f $@+ $@ + sh ./howto-index.sh $(wildcard howto/*.txt) >$@+ + mv $@+ $@ + +$(patsubst %,%.html,$(ARTICLES)) : %.html : %.txt + $(ASCIIDOC) -b xhtml11 $*.txt + +WEBDOC_DEST = /pub/software/scm/git/docs + +$(patsubst %.txt,%.html,$(wildcard howto/*.txt)): %.html : %.txt + rm -f $@+ $@ + sed -e '1,/^$$/d' $< | $(ASCIIDOC) -b xhtml11 - >$@+ + mv $@+ $@ + +install-webdoc : html + sh ./install-webdoc.sh $(WEBDOC_DEST) + +quick-install: + sh ./install-doc-quick.sh $(DOC_REF) $(mandir) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches new file mode 100644 index 0000000000..285781d9db --- /dev/null +++ b/Documentation/SubmittingPatches @@ -0,0 +1,349 @@ +I started reading over the SubmittingPatches document for Linux +kernel, primarily because I wanted to have a document similar to +it for the core GIT to make sure people understand what they are +doing when they write "Signed-off-by" line. + +But the patch submission requirements are a lot more relaxed +here on the technical/contents front, because the core GIT is +thousand times smaller ;-). So here is only the relevant bits. + + +(1) Make separate commits for logically separate changes. + +Unless your patch is really trivial, you should not be sending +out a patch that was generated between your working tree and +your commit head. Instead, always make a commit with complete +commit message and generate a series of patches from your +repository. It is a good discipline. + +Describe the technical detail of the change(s). + +If your description starts to get too long, that's a sign that you +probably need to split up your commit to finer grained pieces. + +Oh, another thing. I am picky about whitespaces. Make sure your +changes do not trigger errors with the sample pre-commit hook shipped +in templates/hooks--pre-commit. To help ensure this does not happen, +run git diff --check on your changes before you commit. + + +(2) Generate your patch using git tools out of your commits. + +git based diff tools (git, Cogito, and StGIT included) generate +unidiff which is the preferred format. + +You do not have to be afraid to use -M option to "git diff" or +"git format-patch", if your patch involves file renames. The +receiving end can handle them just fine. + +Please make sure your patch does not include any extra files +which do not belong in a patch submission. Make sure to review +your patch after generating it, to ensure accuracy. Before +sending out, please make sure it cleanly applies to the "master" +branch head. If you are preparing a work based on "next" branch, +that is fine, but please mark it as such. + + +(3) Sending your patches. + +People on the git mailing list need to be able to read and +comment on the changes you are submitting. It is important for +a developer to be able to "quote" your changes, using standard +e-mail tools, so that they may comment on specific portions of +your code. For this reason, all patches should be submitted +"inline". WARNING: Be wary of your MUAs word-wrap +corrupting your patch. Do not cut-n-paste your patch; you can +lose tabs that way if you are not careful. + +It is a common convention to prefix your subject line with +[PATCH]. This lets people easily distinguish patches from other +e-mail discussions. + +"git format-patch" command follows the best current practice to +format the body of an e-mail message. At the beginning of the +patch should come your commit message, ending with the +Signed-off-by: lines, and a line that consists of three dashes, +followed by the diffstat information and the patch itself. If +you are forwarding a patch from somebody else, optionally, at +the beginning of the e-mail message just before the commit +message starts, you can put a "From: " line to name that person. + +You often want to add additional explanation about the patch, +other than the commit message itself. Place such "cover letter" +material between the three dash lines and the diffstat. + +Do not attach the patch as a MIME attachment, compressed or not. +Do not let your e-mail client send quoted-printable. Do not let +your e-mail client send format=flowed which would destroy +whitespaces in your patches. Many +popular e-mail applications will not always transmit a MIME +attachment as plain text, making it impossible to comment on +your code. A MIME attachment also takes a bit more time to +process. This does not decrease the likelihood of your +MIME-attached change being accepted, but it makes it more likely +that it will be postponed. + +Exception: If your mailer is mangling patches then someone may ask +you to re-send them using MIME, that is OK. + +Do not PGP sign your patch, at least for now. Most likely, your +maintainer or other people on the list would not have your PGP +key and would not bother obtaining it anyway. Your patch is not +judged by who you are; a good patch from an unknown origin has a +far better chance of being accepted than a patch from a known, +respected origin that is done poorly or does incorrect things. + +If you really really really really want to do a PGP signed +patch, format it as "multipart/signed", not a text/plain message +that starts with '-----BEGIN PGP SIGNED MESSAGE-----'. That is +not a text/plain, it's something else. + +Note that your maintainer does not necessarily read everything +on the git mailing list. If your patch is for discussion first, +send it "To:" the mailing list, and optionally "cc:" him. If it +is trivially correct or after the list reached a consensus, send +it "To:" the maintainer and optionally "cc:" the list. + +Also note that your maintainer does not actively involve himself in +maintaining what are in contrib/ hierarchy. When you send fixes and +enhancements to them, do not forget to "cc: " the person who primarily +worked on that hierarchy in contrib/. + + +(4) Sign your work + +To improve tracking of who did what, we've borrowed the +"sign-off" procedure from the Linux kernel project on patches +that are being emailed around. Although core GIT is a lot +smaller project it is a good discipline to follow it. + +The sign-off is a simple line at the end of the explanation for +the patch, which certifies that you wrote it or otherwise have +the right to pass it on as a open-source patch. The rules are +pretty simple: if you can certify the below: + + Developer's Certificate of Origin 1.1 + + By making a contribution to this project, I certify that: + + (a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + + (b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + + (c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + + (d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. + +then you just add a line saying + + Signed-off-by: Random J Developer <random@developer.example.org> + +This line can be automatically added by git if you run the git-commit +command with the -s option. + +Some people also put extra tags at the end. They'll just be ignored for +now, but you can do this to mark internal company procedures or just +point out some special detail about the sign-off. + + +------------------------------------------------ +MUA specific hints + +Some of patches I receive or pick up from the list share common +patterns of breakage. Please make sure your MUA is set up +properly not to corrupt whitespaces. Here are two common ones +I have seen: + +* Empty context lines that do not have _any_ whitespace. + +* Non empty context lines that have one extra whitespace at the + beginning. + +One test you could do yourself if your MUA is set up correctly is: + +* Send the patch to yourself, exactly the way you would, except + To: and Cc: lines, which would not contain the list and + maintainer address. + +* Save that patch to a file in UNIX mailbox format. Call it say + a.patch. + +* Try to apply to the tip of the "master" branch from the + git.git public repository: + + $ git fetch http://kernel.org/pub/scm/git/git.git master:test-apply + $ git checkout test-apply + $ git reset --hard + $ git applymbox a.patch + +If it does not apply correctly, there can be various reasons. + +* Your patch itself does not apply cleanly. That is _bad_ but + does not have much to do with your MUA. Please rebase the + patch appropriately. + +* Your MUA corrupted your patch; applymbox would complain that + the patch does not apply. Look at .dotest/ subdirectory and + see what 'patch' file contains and check for the common + corruption patterns mentioned above. + +* While you are at it, check what are in 'info' and + 'final-commit' files as well. If what is in 'final-commit' is + not exactly what you would want to see in the commit log + message, it is very likely that your maintainer would end up + hand editing the log message when he applies your patch. + Things like "Hi, this is my first patch.\n", if you really + want to put in the patch e-mail, should come after the + three-dash line that signals the end of the commit message. + + +Pine +---- + +(Johannes Schindelin) + +I don't know how many people still use pine, but for those poor +souls it may be good to mention that the quell-flowed-text is +needed for recent versions. + +... the "no-strip-whitespace-before-send" option, too. AFAIK it +was introduced in 4.60. + +(Linus Torvalds) + +And 4.58 needs at least this. + +--- +diff-tree 8326dd8350be64ac7fc805f6563a1d61ad10d32c (from e886a61f76edf5410573e92e38ce22974f9c40f1) +Author: Linus Torvalds <torvalds@g5.osdl.org> +Date: Mon Aug 15 17:23:51 2005 -0700 + + Fix pine whitespace-corruption bug + + There's no excuse for unconditionally removing whitespace from + the pico buffers on close. + +diff --git a/pico/pico.c b/pico/pico.c +--- a/pico/pico.c ++++ b/pico/pico.c +@@ -219,7 +219,9 @@ PICO *pm; + switch(pico_all_done){ /* prepare for/handle final events */ + case COMP_EXIT : /* already confirmed */ + packheader(); ++#if 0 + stripwhitespace(); ++#endif + c |= COMP_EXIT; + break; + + +(Daniel Barkalow) + +> A patch to SubmittingPatches, MUA specific help section for +> users of Pine 4.63 would be very much appreciated. + +Ah, it looks like a recent version changed the default behavior to do the +right thing, and inverted the sense of the configuration option. (Either +that or Gentoo did it.) So you need to set the +"no-strip-whitespace-before-send" option, unless the option you have is +"strip-whitespace-before-send", in which case you should avoid checking +it. + + +Thunderbird +----------- + +(A Large Angry SCM) + +Here are some hints on how to successfully submit patches inline using +Thunderbird. + +This recipe appears to work with the current [*1*] Thunderbird from Suse. + +The following Thunderbird extensions are needed: + AboutConfig 0.5 + http://aboutconfig.mozdev.org/ + External Editor 0.7.2 + http://globs.org/articles.php?lng=en&pg=8 + +1) Prepare the patch as a text file using your method of choice. + +2) Before opening a compose window, use Edit->Account Settings to +uncheck the "Compose messages in HTML format" setting in the +"Composition & Addressing" panel of the account to be used to send the +patch. [*2*] + +3) In the main Thunderbird window, _before_ you open the compose window +for the patch, use Tools->about:config to set the following to the +indicated values: + mailnews.send_plaintext_flowed => false + mailnews.wraplength => 0 + +4) Open a compose window and click the external editor icon. + +5) In the external editor window, read in the patch file and exit the +editor normally. + +6) Back in the compose window: Add whatever other text you wish to the +message, complete the addressing and subject fields, and press send. + +7) Optionally, undo the about:config/account settings changes made in +steps 2 & 3. + + +[Footnotes] +*1* Version 1.0 (20041207) from the MozillaThunderbird-1.0-5 rpm of Suse +9.3 professional updates. + +*2* It may be possible to do this with about:config and the following +settings but I haven't tried, yet. + mail.html_compose => false + mail.identity.default.compose_html => false + mail.identity.id?.compose_html => false + + +Gnus +---- + +'|' in the *Summary* buffer can be used to pipe the current +message to an external program, and this is a handy way to drive +"git am". However, if the message is MIME encoded, what is +piped into the program is the representation you see in your +*Article* buffer after unwrapping MIME. This is often not what +you would want for two reasons. It tends to screw up non ASCII +characters (most notably in people's names), and also +whitespaces (fatal in patches). Running 'C-u g' to display the +message in raw form before using '|' to run the pipe can work +this problem around. + + +KMail +----- + +This should help you to submit patches inline using KMail. + +1) Prepare the patch as a text file. + +2) Click on New Mail. + +3) Go under "Options" in the Composer window and be sure that +"Word wrap" is not set. + +4) Use Message -> Insert file... and insert the patch. + +5) Back in the compose window: add whatever other text you wish to the +message, complete the addressing and subject fields, and press send. diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf new file mode 100644 index 0000000000..44b1ce4c6b --- /dev/null +++ b/Documentation/asciidoc.conf @@ -0,0 +1,39 @@ +## gitlink: macro +# +# Usage: gitlink:command[manpage-section] +# +# Note, {0} is the manpage section, while {target} is the command. +# +# Show GIT link as: <command>(<section>); if section is defined, else just show +# the command. + +[attributes] +caret=^ +startsb=[ +endsb=] +tilde=~ + +ifdef::backend-docbook[] +[gitlink-inlinemacro] +{0%{target}} +{0#<citerefentry>} +{0#<refentrytitle>{target}</refentrytitle><manvolnum>{0}</manvolnum>} +{0#</citerefentry>} +endif::backend-docbook[] + +ifdef::backend-docbook[] +# "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this. +[listingblock] +<example><title>{title}</title> +<literallayout> +| +</literallayout> +{title#}</example> +endif::backend-docbook[] + +ifdef::backend-xhtml11[] +[gitlink-inlinemacro] +<a href="{target}.html">{target}{0?({0})}</a> +endif::backend-xhtml11[] + + diff --git a/Documentation/build-docdep.perl b/Documentation/build-docdep.perl new file mode 100755 index 0000000000..489389c32a --- /dev/null +++ b/Documentation/build-docdep.perl @@ -0,0 +1,50 @@ +#!/usr/bin/perl + +my %include = (); +my %included = (); + +for my $text (<*.txt>) { + open I, '<', $text || die "cannot read: $text"; + while (<I>) { + if (/^include::/) { + chomp; + s/^include::\s*//; + s/\[\]//; + $include{$text}{$_} = 1; + $included{$_} = 1; + } + } + close I; +} + +# Do we care about chained includes??? +my $changed = 1; +while ($changed) { + $changed = 0; + while (my ($text, $included) = each %include) { + for my $i (keys %$included) { + # $text has include::$i; if $i includes $j + # $text indirectly includes $j. + if (exists $include{$i}) { + for my $j (keys %{$include{$i}}) { + if (!exists $include{$text}{$j}) { + $include{$text}{$j} = 1; + $included{$j} = 1; + $changed = 1; + } + } + } + } + } +} + +while (my ($text, $included) = each %include) { + if (! exists $included{$text} && + (my $base = $text) =~ s/\.txt$//) { + my ($suffix) = '1'; + if ($base eq 'git') { + $suffix = '7'; # yuck... + } + print "$base.html $base.$suffix : ", join(" ", keys %$included), "\n"; + } +} diff --git a/Documentation/callouts.xsl b/Documentation/callouts.xsl new file mode 100644 index 0000000000..6a361a2136 --- /dev/null +++ b/Documentation/callouts.xsl @@ -0,0 +1,30 @@ +<!-- callout.xsl: converts asciidoc callouts to man page format --> +<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> +<xsl:template match="co"> + <xsl:value-of select="concat('\fB(',substring-after(@id,'-'),')\fR')"/> +</xsl:template> +<xsl:template match="calloutlist"> + <xsl:text>.sp </xsl:text> + <xsl:apply-templates/> + <xsl:text> </xsl:text> +</xsl:template> +<xsl:template match="callout"> + <xsl:value-of select="concat('\fB',substring-after(@arearefs,'-'),'. \fR')"/> + <xsl:apply-templates/> + <xsl:text>.br </xsl:text> +</xsl:template> + +<!-- sorry, this is not about callouts, but attempts to work around + spurious .sp at the tail of the line docbook stylesheets seem to add --> +<xsl:template match="simpara"> + <xsl:variable name="content"> + <xsl:apply-templates/> + </xsl:variable> + <xsl:value-of select="normalize-space($content)"/> + <xsl:if test="not(ancestor::authorblurb) and + not(ancestor::personblurb)"> + <xsl:text> </xsl:text> + </xsl:if> +</xsl:template> + +</xsl:stylesheet> diff --git a/Documentation/cmd-list.perl b/Documentation/cmd-list.perl new file mode 100755 index 0000000000..69003e90af --- /dev/null +++ b/Documentation/cmd-list.perl @@ -0,0 +1,187 @@ +# + +sub format_one { + my ($out, $name) = @_; + my ($state, $description); + open I, '<', "$name.txt" or die "No such file $name.txt"; + while (<I>) { + if (/^NAME$/) { + $state = 1; + next; + } + if ($state == 1 && /^----$/) { + $state = 2; + next; + } + next if ($state != 2); + chomp; + $description = $_; + last; + } + close I; + if (!defined $description) { + die "No description found in $name.txt"; + } + if (my ($verify_name, $text) = ($description =~ /^($name) - (.*)/)) { + print $out "gitlink:$name\[1\]::\n"; + print $out "\t$text.\n\n"; + } + else { + die "Description does not match $name: $description"; + } +} + +my %cmds = (); +while (<DATA>) { + next if /^#/; + + chomp; + my ($name, $cat) = /^(\S+)\s+(.*)$/; + push @{$cmds{$cat}}, $name; +} + +for my $cat (qw(ancillaryinterrogators + ancillarymanipulators + mainporcelain + plumbinginterrogators + plumbingmanipulators + synchingrepositories + foreignscminterface + purehelpers + synchelpers)) { + my $out = "cmds-$cat.txt"; + open O, '>', "$out+" or die "Cannot open output file $out+"; + for (@{$cmds{$cat}}) { + format_one(\*O, $_); + } + close O; + rename "$out+", "$out"; +} + +__DATA__ +git-add mainporcelain +git-am mainporcelain +git-annotate ancillaryinterrogators +git-applymbox ancillaryinterrogators +git-applypatch purehelpers +git-apply plumbingmanipulators +git-archimport foreignscminterface +git-archive mainporcelain +git-bisect mainporcelain +git-blame ancillaryinterrogators +git-branch mainporcelain +git-cat-file plumbinginterrogators +git-checkout-index plumbingmanipulators +git-checkout mainporcelain +git-check-ref-format purehelpers +git-cherry ancillaryinterrogators +git-cherry-pick mainporcelain +git-clean mainporcelain +git-clone mainporcelain +git-commit mainporcelain +git-commit-tree plumbingmanipulators +git-convert-objects ancillarymanipulators +git-count-objects ancillaryinterrogators +git-cvsexportcommit foreignscminterface +git-cvsimport foreignscminterface +git-cvsserver foreignscminterface +git-daemon synchingrepositories +git-describe mainporcelain +git-diff-files plumbinginterrogators +git-diff-index plumbinginterrogators +git-diff mainporcelain +git-diff-stages plumbinginterrogators +git-diff-tree plumbinginterrogators +git-fast-import ancillarymanipulators +git-fetch mainporcelain +git-fetch-pack synchingrepositories +git-fmt-merge-msg purehelpers +git-for-each-ref plumbinginterrogators +git-format-patch mainporcelain +git-fsck ancillaryinterrogators +git-gc mainporcelain +git-get-tar-commit-id ancillaryinterrogators +git-grep mainporcelain +git-hash-object plumbingmanipulators +git-http-fetch synchelpers +git-http-push synchelpers +git-imap-send foreignscminterface +git-index-pack plumbingmanipulators +git-init mainporcelain +git-instaweb ancillaryinterrogators +gitk mainporcelain +git-local-fetch synchingrepositories +git-log mainporcelain +git-lost-found ancillarymanipulators +git-ls-files plumbinginterrogators +git-ls-remote plumbinginterrogators +git-ls-tree plumbinginterrogators +git-mailinfo purehelpers +git-mailsplit purehelpers +git-merge-base plumbinginterrogators +git-merge-file plumbingmanipulators +git-merge-index plumbingmanipulators +git-merge mainporcelain +git-merge-one-file purehelpers +git-merge-tree ancillaryinterrogators +git-mktag plumbingmanipulators +git-mktree plumbingmanipulators +git-mv mainporcelain +git-name-rev plumbinginterrogators +git-pack-objects plumbingmanipulators +git-pack-redundant plumbinginterrogators +git-pack-refs ancillarymanipulators +git-parse-remote synchelpers +git-patch-id purehelpers +git-peek-remote purehelpers +git-prune ancillarymanipulators +git-prune-packed plumbingmanipulators +git-pull mainporcelain +git-push mainporcelain +git-quiltimport foreignscminterface +git-read-tree plumbingmanipulators +git-rebase mainporcelain +git-receive-pack synchelpers +git-reflog ancillarymanipulators +git-relink ancillarymanipulators +git-repack ancillarymanipulators +git-config ancillarymanipulators +git-request-pull foreignscminterface +git-rerere ancillaryinterrogators +git-reset mainporcelain +git-resolve mainporcelain +git-revert mainporcelain +git-rev-list plumbinginterrogators +git-rev-parse ancillaryinterrogators +git-rm mainporcelain +git-runstatus ancillaryinterrogators +git-send-email foreignscminterface +git-send-pack synchingrepositories +git-shell synchelpers +git-shortlog mainporcelain +git-show mainporcelain +git-show-branch ancillaryinterrogators +git-show-index plumbinginterrogators +git-show-ref plumbinginterrogators +git-sh-setup purehelpers +git-ssh-fetch synchingrepositories +git-ssh-upload synchingrepositories +git-status mainporcelain +git-stripspace purehelpers +git-svn foreignscminterface +git-svnimport foreignscminterface +git-symbolic-ref plumbingmanipulators +git-tag mainporcelain +git-tar-tree plumbinginterrogators +git-unpack-file plumbinginterrogators +git-unpack-objects plumbingmanipulators +git-update-index plumbingmanipulators +git-update-ref plumbingmanipulators +git-update-server-info synchingrepositories +git-upload-archive synchelpers +git-upload-pack synchelpers +git-var plumbinginterrogators +git-verify-pack plumbinginterrogators +git-verify-tag ancillaryinterrogators +git-whatchanged ancillaryinterrogators +git-write-tree plumbingmanipulators diff --git a/Documentation/config.txt b/Documentation/config.txt new file mode 100644 index 0000000000..0129b1fd69 --- /dev/null +++ b/Documentation/config.txt @@ -0,0 +1,518 @@ +CONFIGURATION FILE +------------------ + +The git configuration file contains a number of variables that affect +the git command's behavior. `.git/config` file for each repository +is used to store the information for that repository, and +`$HOME/.gitconfig` is used to store per user information to give +fallback values for `.git/config` file. + +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. + +Syntax +~~~~~~ + +The syntax is fairly flexible and permissive; whitespaces are mostly +ignored. The '#' and ';' characters begin comments to the end of line, +blank lines are ignored. + +The file consists of sections and variables. A section begins with +the name of the section in square brackets and continues until the next +section begins. Section names are not case sensitive. Only alphanumeric +characters, '`-`' and '`.`' are allowed in section names. Each variable +must belong to some section, which means that there must be section +header before first setting of a variable. + +Sections can be further divided into subsections. To begin a subsection +put its name in double quotes, separated by space from the section name, +in the section header, like in example below: + +-------- + [section "subsection"] + +-------- + +Subsection names can contain any characters except newline (doublequote +'`"`' and backslash have to be escaped as '`\"`' and '`\\`', +respectively) and are case sensitive. Section header cannot span multiple +lines. Variables may belong directly to a section or to a given subsection. +You can have `[section]` if you have `[section "subsection"]`, but you +don't need to. + +There is also (case insensitive) alternative `[section.subsection]` syntax. +In this syntax subsection names follow the same restrictions as for section +name. + +All the other lines are recognized as setting variables, in the form +'name = value'. If there is no equal sign on the line, the entire line +is taken as 'name' and the variable is recognized as boolean "true". +The variable names are case-insensitive and only alphanumeric +characters and '`-`' are allowed. There can be more than one value +for a given variable; we say then that variable is multivalued. + +Leading and trailing whitespace in a variable value is discarded. +Internal whitespace within a variable value is retained verbatim. + +The values following the equals sign in variable assign are all either +a string, an integer, or a boolean. Boolean values may be given as yes/no, +0/1 or true/false. Case is not significant in boolean values, when +converting value to the canonical form using '--bool' type specifier; +`git-config` will ensure that the output is "true" or "false". + +String values may be entirely or partially enclosed in double quotes. +You need to enclose variable value in double quotes if you want to +preserve leading or trailing whitespace, or if variable value contains +beginning of comment characters (if it contains '#' or ';'). +Double quote '`"`' and backslash '`\`' characters in variable value must +be escaped: use '`\"`' for '`"`' and '`\\`' for '`\`'. + +The following escape sequences (beside '`\"`' and '`\\`') are recognized: +'`\n`' for newline character (NL), '`\t`' for horizontal tabulation (HT, TAB) +and '`\b`' for backspace (BS). No other char escape sequence, nor octal +char sequences are valid. + +Variable value ending in a '`\`' is continued on the next line in the +customary UNIX fashion. + +Some variables may require special value format. + +Example +~~~~~~~ + + # Core variables + [core] + ; Don't trust file modes + filemode = false + + # Our diff algorithm + [diff] + external = "/usr/local/bin/gnu-diff -u" + renames = true + + [branch "devel"] + remote = origin + merge = refs/heads/devel + + # Proxy settings + [core] + gitProxy="ssh" for "ssh://kernel.org/" + gitProxy=default-proxy ; for the rest + +Variables +~~~~~~~~~ + +Note that this list is non-comprehensive and not necessarily complete. +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:: + If false, the executable bit differences between the index and + the working copy are ignored; useful on broken filesystems like FAT. + See gitlink:git-update-index[1]. True by default. + +core.gitProxy:: + A "proxy command" to execute (as 'command host port') instead + of establishing direct connection to the remote server when + using the git protocol for fetching. If the variable value is + in the "COMMAND for DOMAIN" format, the command is applied only + 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 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 + mark them otherwise manually - Git will not detect the file changes + by lstat() calls. This is useful on systems where those are very + slow, such as Microsoft Windows. See gitlink:git-update-index[1]. + False by default. + +core.preferSymlinkRefs:: + Instead of the default "symref" format for HEAD + and other symbolic reference files, use symbolic links. + This is sometimes needed to work with old scripts that + expect HEAD to be a symbolic link. + +core.logAllRefUpdates:: + Updates to a ref <ref> is logged to the file + "$GIT_DIR/logs/<ref>", by appending the new and old + SHA1, the date/time and the reason of the update, but + only when the file exists. If this configuration + variable is set to true, missing "$GIT_DIR/logs/<ref>" + file is automatically created for branch heads. ++ +This information can be used to determine what commit +was the tip of a branch "2 days ago". ++ +This value is true by default in a repository that has +a working directory associated with it, and false by +default in a bare repository. + +core.repositoryFormatVersion:: + Internal variable identifying the repository format and layout + version. + +core.sharedRepository:: + When 'group' (or 'true'), the repository is made shareable between + several users in a group (making sure all the files and objects are + group-writable). When 'all' (or 'world' or 'everybody'), the + repository will be readable by all users, additionally to being + group-shareable. When 'umask' (or 'false'), git will use permissions + reported by umask(2). See gitlink:git-init[1]. False by default. + +core.warnAmbiguousRefs:: + 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. + +core.compression:: + An integer -1..9, indicating the compression level for objects that + are not in a pack file. -1 is the zlib and git default. 0 means no + compression, and 1..9 are various speed/size tradeoffs, 9 being + slowest. + +core.legacyheaders:: + A boolean which enables the legacy object header format in case + you want to interoperate with old clients accessing the object + database directly (where the "http://" and "rsync://" protocols + count as direct access). + +core.packedGitWindowSize:: + Number of bytes of a pack file to map into memory in a + single mapping operation. Larger window sizes may allow + your system to process a smaller number of large pack files + more quickly. Smaller window sizes will negatively affect + performance due to increased calls to the operating system's + memory manager, but may improve performance when accessing + a large number of large pack files. ++ +Default is 1 MiB if NO_MMAP was set at compile time, otherwise 32 +MiB on 32 bit platforms and 1 GiB on 64 bit platforms. This should +be reasonable for all users/operating systems. You probably do +not need to adjust this value. ++ +Common unit suffixes of 'k', 'm', or 'g' are supported. + +core.packedGitLimit:: + Maximum number of bytes to map simultaneously into memory + from pack files. If Git needs to access more than this many + bytes at once to complete an operation it will unmap existing + regions to reclaim virtual address space within the process. ++ +Default is 256 MiB on 32 bit platforms and 8 GiB on 64 bit platforms. +This should be reasonable for all users/operating systems, except on +the largest projects. You probably do not need to adjust this value. ++ +Common unit suffixes of 'k', 'm', or 'g' are supported. + +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. + + If the alias expansion is prefixed with an exclamation point, + it will be treated as a shell command. For example, defining + "alias.new = !gitk --all --not ORIG_HEAD", the invocation + "git new" is equivalent to running the shell command + "gitk --all --not ORIG_HEAD". + +apply.whitespace:: + Tells `git-apply` how to handle whitespaces, in the same way + as the '--whitespace' option. See gitlink:git-apply[1]. + +branch.<name>.remote:: + When in branch <name>, it tells `git fetch` which remote to fetch. + If this option is not given, `git fetch` defaults to remote "origin". + +branch.<name>.merge:: + When in branch <name>, it tells `git fetch` the default refspec to + be marked for merging in FETCH_HEAD. The value has exactly to match + a remote part of one of the refspecs which are fetched from the remote + given by "branch.<name>.remote". + The merge information is used by `git pull` (which at first calls + `git fetch`) to lookup the default branch for merging. Without + this option, `git pull` defaults to merge the first refspec fetched. + Specify multiple values to get an octopus merge. + +color.branch:: + A boolean to enable/disable color in the output of + gitlink:git-branch[1]. May be set to `true` (or `always`), + `false` (or `never`) or `auto`, in which case colors are used + only when the output is to a terminal. Defaults to false. + +color.branch.<slot>:: + Use customized color for branch coloration. `<slot>` is one of + `current` (the current branch), `local` (a local branch), + `remote` (a tracking branch in refs/remotes/), `plain` (other + refs). ++ +The value for these configuration variables is a list of colors (at most +two) and attributes (at most one), separated by spaces. The colors +accepted are `normal`, `black`, `red`, `green`, `yellow`, `blue`, +`magenta`, `cyan` and `white`; the attributes are `bold`, `dim`, `ul`, +`blink` and `reverse`. The first color given is the foreground; the +second is the background. The position of the attribute, if any, +doesn't matter. + +color.diff:: + When true (or `always`), always use colors in patch. + When false (or `never`), never. When set to `auto`, use + colors only when the output is to the terminal. + +color.diff.<slot>:: + Use customized color for diff colorization. `<slot>` specifies + which part of the patch to use the specified color, and is one + of `plain` (context text), `meta` (metainformation), `frag` + (hunk header), `old` (removed lines), `new` (added lines), + `commit` (commit headers), or `whitespace` (highlighting dubious + whitespace). The values of these variables may be specified as + in color.branch.<slot>. + +color.pager:: + A boolean to enable/disable colored output when the pager is in + use (default is true). + +color.status:: + A boolean to enable/disable color in the output of + gitlink:git-status[1]. May be set to `true` (or `always`), + `false` (or `never`) or `auto`, in which case colors are used + only when the output is to a terminal. Defaults to false. + +color.status.<slot>:: + Use customized color for status colorization. `<slot>` is + one of `header` (the header text of the status message), + `added` or `updated` (files which are added but not committed), + `changed` (files which are changed but not added in the index), + or `untracked` (files which are not tracked by git). The values of + these variables may be specified as in color.branch.<slot>. + +diff.renameLimit:: + The number of files to consider when performing the copy/rename + detection; equivalent to the git diff option '-l'. + +diff.renames:: + Tells git to detect renames. If set to any boolean value, it + will enable basic rename detection. If set to "copies" or + "copy", it will detect copies, as well. + +fetch.unpackLimit:: + If the number of objects fetched over the git native + transfer is below this + limit, then the objects will be unpacked into loose object + files. However if the number of received objects equals or + exceeds this limit then the received pack will be stored as + a pack, after adding any missing delta bases. Storing the + pack from a push can make the push operation complete faster, + especially on slow filesystems. + +format.headers:: + Additional email headers to include in a patch to be submitted + by mail. See gitlink:git-format-patch[1]. + +gc.reflogexpire:: + `git reflog expire` removes reflog entries older than + this time; defaults to 90 days. + +gc.reflogexpireunreachable:: + `git reflog expire` removes reflog entries older than + this time and are not reachable from the current tip; + defaults to 30 days. + +gc.rerereresolved:: + Records of conflicted merge you resolved earlier are + kept for this many days when `git rerere gc` is run. + The default is 60 days. See gitlink:git-rerere[1]. + +gc.rerereunresolved:: + Records of conflicted merge you have not resolved are + kept for this many days when `git rerere gc` is run. + The default is 15 days. See gitlink:git-rerere[1]. + +gitcvs.enabled:: + Whether the cvs pserver interface is enabled for this repository. + See gitlink:git-cvsserver[1]. + +gitcvs.logfile:: + Path to a log file where the cvs pserver interface well... logs + various stuff. See gitlink:git-cvsserver[1]. + +http.sslVerify:: + Whether to verify the SSL certificate when fetching or pushing + 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 overridden by the 'GIT_SSL_CERT' environment + variable. + +http.sslKey:: + File containing the SSL private key when fetching or pushing + 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 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 overridden + by the 'GIT_SSL_CAPATH' environment variable. + +http.maxRequests:: + 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 overridden by the 'GIT_HTTP_LOW_SPEED_LIMIT' and + 'GIT_HTTP_LOW_SPEED_TIME' environment variables. + +http.noEPSV:: + A boolean which disables using of EPSV ftp command by curl. + This can helpful with some "poor" ftp servers which doesn't + support EPSV mode. Can be overridden by the 'GIT_CURL_FTP_NO_EPSV' + environment variable. Default is false (curl will use EPSV). + +i18n.commitEncoding:: + Character encoding the commit messages are stored in; git itself + does not care per se, but this information is necessary e.g. when + importing commits from emails or in the gitk graphical history + browser (and possibly at other places in the future or in other + porcelains). See e.g. gitlink:git-mailinfo[1]. Defaults to 'utf-8'. + +i18n.logOutputEncoding:: + Character encoding the commit messages are converted to when + running `git-log` and friends. + +log.showroot:: + If true, the initial commit will be shown as a big creation event. + This is equivalent to a diff against an empty tree. + Tools like gitlink:git-log[1] or gitlink:git-whatchanged[1], which + normally hide the root commit will now show it. True by default. + +merge.summary:: + Whether to include summaries of merged commits in newly created + merge commit messages. False by default. + +merge.verbosity:: + Controls the amount of output shown by the recursive merge + strategy. Level 0 outputs nothing except a final error + message if conflicts were detected. Level 1 outputs only + conflicts, 2 outputs conflicts and file changes. Level 5 and + above outputs debugging information. The default is level 2. + +pack.window:: + The size of the window used by gitlink:git-pack-objects[1] when no + window size is given on the command line. Defaults to 10. + +pull.octopus:: + The default merge strategy to use when pulling multiple branches + at once. + +pull.twohead:: + The default merge strategy to use when pulling a single branch. + +remote.<name>.url:: + The URL of a remote repository. See gitlink:git-fetch[1] or + gitlink:git-push[1]. + +remote.<name>.fetch:: + The default set of "refspec" for gitlink:git-fetch[1]. See + gitlink:git-fetch[1]. + +remote.<name>.push:: + The default set of "refspec" for gitlink:git-push[1]. See + gitlink:git-push[1]. + +remote.<name>.receivepack:: + The default program to execute on the remote side when pushing. See + option \--exec of gitlink:git-push[1]. + +remote.<name>.uploadpack:: + The default program to execute on the remote side when fetching. See + option \--exec of gitlink:git-fetch-pack[1]. + +repack.usedeltabaseoffset:: + Allow gitlink:git-repack[1] to create packs that uses + delta-base offset. Defaults to false. + +show.difftree:: + The default gitlink:git-diff-tree[1] arguments to be used + for gitlink:git-show[1]. + +showbranch.default:: + The default set of branches for gitlink:git-show-branch[1]. + See gitlink:git-show-branch[1]. + +tar.umask:: + By default, gitlink:git-tar-tree[1] sets file and directories modes + to 0666 or 0777. While this is both useful and acceptable for projects + such as the Linux Kernel, it might be excessive for other projects. + With this variable, it becomes possible to tell + gitlink:git-tar-tree[1] to apply a specific umask to the modes above. + The special value "user" indicates that the user's current umask will + be used. This should be enough for most projects, as it will lead to + the same permissions as gitlink:git-checkout[1] would use. The default + value remains 0, which means world read-write. + +user.email:: + Your email address to be recorded in any newly created commits. + 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 overridden by the 'GIT_AUTHOR_NAME' and 'GIT_COMMITTER_NAME' + environment variables. See gitlink:git-commit-tree[1]. + +user.signingkey:: + If gitlink:git-tag[1] is not selecting the key you want it to + automatically when creating a signed tag, you can override the + default selection with this variable. This option is passed + unchanged to gpg's --local-user parameter, so you may specify a key + using any method that gpg supports. + +whatchanged.difftree:: + The default gitlink:git-diff-tree[1] arguments to be used + for gitlink:git-whatchanged[1]. + +imap:: + The configuration variables in the 'imap' section are described + in gitlink:git-imap-send[1]. + +receive.unpackLimit:: + If the number of objects received in a push is below this + limit then the objects will be unpacked into loose object + files. However if the number of received objects equals or + exceeds this limit then the received pack will be stored as + a pack, after adding any missing delta bases. Storing the + pack from a push can make the push operation complete faster, + especially on slow filesystems. + +receive.denyNonFastForwards:: + If set to true, git-receive-pack will deny a ref update which is + not a fast forward. Use this to prevent such an update via a push, + even if that push is forced. This configuration variable is + set when initializing a shared repository. + +transfer.unpackLimit:: + When `fetch.unpackLimit` or `receive.unpackLimit` are + not set, the value of this variable is used instead. + + diff --git a/Documentation/core-intro.txt b/Documentation/core-intro.txt new file mode 100644 index 0000000000..abafefc71c --- /dev/null +++ b/Documentation/core-intro.txt @@ -0,0 +1,590 @@ +//////////////////////////////////////////////////////////////// + + GIT - the stupid content tracker + +//////////////////////////////////////////////////////////////// + +"git" can mean anything, depending on your mood. + + - random three-letter combination that is pronounceable, and not + actually used by any common UNIX command. The fact that it is a + mispronunciation of "get" may or may not be relevant. + - stupid. contemptible and despicable. simple. Take your pick from the + dictionary of slang. + - "global information tracker": you're in a good mood, and it actually + works for you. Angels sing, and a light suddenly fills the room. + - "goddamn idiotic truckload of sh*t": when it breaks + +This is a (not so) stupid but extremely fast directory content manager. +It doesn't do a whole lot at its core, but what it 'does' do is track +directory contents efficiently. + +There are two object abstractions: the "object database", and the +"current directory cache" aka "index". + +The Object Database +~~~~~~~~~~~~~~~~~~~ +The object database is literally just a content-addressable collection +of objects. All objects are named by their content, which is +approximated by the SHA1 hash of the object itself. Objects may refer +to other objects (by referencing their SHA1 hash), and so you can +build up a hierarchy of objects. + +All objects have a statically determined "type" aka "tag", which is +determined at object creation time, and which identifies the format of +the object (i.e. how it is used, and how it can refer to other +objects). There are currently four different object types: "blob", +"tree", "commit" and "tag". + +A "blob" object cannot refer to any other object, and is, like the type +implies, a pure storage object containing some user data. It is used to +actually store the file data, i.e. a blob object is associated with some +particular version of some file. + +A "tree" object is an object that ties one or more "blob" objects into a +directory structure. In addition, a tree object can refer to other tree +objects, thus creating a directory hierarchy. + +A "commit" object ties such directory hierarchies together into +a DAG of revisions - each "commit" is associated with exactly one tree +(the directory hierarchy at the time of the commit). In addition, a +"commit" refers to one or more "parent" commit objects that describe the +history of how we arrived at that directory hierarchy. + +As a special case, a commit object with no parents is called the "root" +object, and is the point of an initial project commit. Each project +must have at least one root, and while you can tie several different +root objects together into one project by creating a commit object which +has two or more separate roots as its ultimate parents, that's probably +just going to confuse people. So aim for the notion of "one root object +per project", even if git itself does not enforce that. + +A "tag" object symbolically identifies and can be used to sign other +objects. It contains the identifier and type of another object, a +symbolic name (of course!) and, optionally, a signature. + +Regardless of object type, all objects share the following +characteristics: they are all deflated with zlib, and have a header +that not only specifies their type, but also provides size information +about the data in the object. It's worth noting that the SHA1 hash +that is used to name the object is the hash of the original data +plus this header, so `sha1sum` 'file' does not match the object name +for 'file'. +(Historical note: in the dawn of the age of git the hash +was the sha1 of the 'compressed' object.) + +As a result, the general consistency of an object can always be tested +independently of the contents or the type of the object: all objects can +be validated by verifying that (a) their hashes match the content of the +file and (b) the object successfully inflates to a stream of bytes that +forms a sequence of <ascii type without space> + <space> + <ascii decimal +size> + <byte\0> + <binary object data>. + +The structured objects can further have their structure and +connectivity to other objects verified. This is generally done with +the `git-fsck` program, which generates a full dependency graph +of all objects, and verifies their internal consistency (in addition +to just verifying their superficial consistency through the hash). + +The object types in some more detail: + +Blob Object +~~~~~~~~~~~ +A "blob" object is nothing but a binary blob of data, and doesn't +refer to anything else. There is no signature or any other +verification of the data, so while the object is consistent (it 'is' +indexed by its sha1 hash, so the data itself is certainly correct), it +has absolutely no other attributes. No name associations, no +permissions. It is purely a blob of data (i.e. normally "file +contents"). + +In particular, since the blob is entirely defined by its data, if two +files in a directory tree (or in multiple different versions of the +repository) have the same contents, they will share the same blob +object. The object is totally independent of its location in the +directory tree, and renaming a file does not change the object that +file is associated with in any way. + +A blob is typically created when gitlink:git-update-index[1] +is run, and its data can be accessed by gitlink:git-cat-file[1]. + +Tree Object +~~~~~~~~~~~ +The next hierarchical object type is the "tree" object. A tree object +is a list of mode/name/blob data, sorted by name. Alternatively, the +mode data may specify a directory mode, in which case instead of +naming a blob, that name is associated with another TREE object. + +Like the "blob" object, a tree object is uniquely determined by the +set contents, and so two separate but identical trees will always +share the exact same object. This is true at all levels, i.e. it's +true for a "leaf" tree (which does not refer to any other trees, only +blobs) as well as for a whole subdirectory. + +For that reason a "tree" object is just a pure data abstraction: it +has no history, no signatures, no verification of validity, except +that since the contents are again protected by the hash itself, we can +trust that the tree is immutable and its contents never change. + +So you can trust the contents of a tree to be valid, the same way you +can trust the contents of a blob, but you don't know where those +contents 'came' from. + +Side note on trees: since a "tree" object is a sorted list of +"filename+content", you can create a diff between two trees without +actually having to unpack two trees. Just ignore all common parts, +and your diff will look right. In other words, you can effectively +(and efficiently) tell the difference between any two random trees by +O(n) where "n" is the size of the difference, rather than the size of +the tree. + +Side note 2 on trees: since the name of a "blob" depends entirely and +exclusively on its contents (i.e. there are no names or permissions +involved), you can see trivial renames or permission changes by +noticing that the blob stayed the same. However, renames with data +changes need a smarter "diff" implementation. + +A tree is created with gitlink:git-write-tree[1] and +its data can be accessed by gitlink:git-ls-tree[1]. +Two trees can be compared with gitlink:git-diff-tree[1]. + +Commit Object +~~~~~~~~~~~~~ +The "commit" object is an object that introduces the notion of +history into the picture. In contrast to the other objects, it +doesn't just describe the physical state of a tree, it describes how +we got there, and why. + +A "commit" is defined by the tree-object that it results in, the +parent commits (zero, one or more) that led up to that point, and a +comment on what happened. Again, a commit is not trusted per se: +the contents are well-defined and "safe" due to the cryptographically +strong signatures at all levels, but there is no reason to believe +that the tree is "good" or that the merge information makes sense. +The parents do not have to actually have any relationship with the +result, for example. + +Note on commits: unlike real SCM's, commits do not contain +rename information or file mode change information. All of that is +implicit in the trees involved (the result tree, and the result trees +of the parents), and describing that makes no sense in this idiotic +file manager. + +A commit is created with gitlink:git-commit-tree[1] and +its data can be accessed by gitlink:git-cat-file[1]. + +Trust +~~~~~ +An aside on the notion of "trust". Trust is really outside the scope +of "git", but it's worth noting a few things. First off, since +everything is hashed with SHA1, you 'can' trust that an object is +intact and has not been messed with by external sources. So the name +of an object uniquely identifies a known state - just not a state that +you may want to trust. + +Furthermore, since the SHA1 signature of a commit refers to the +SHA1 signatures of the tree it is associated with and the signatures +of the parent, a single named commit specifies uniquely a whole set +of history, with full contents. You can't later fake any step of the +way once you have the name of a commit. + +So to introduce some real trust in the system, the only thing you need +to do is to digitally sign just 'one' special note, which includes the +name of a top-level commit. Your digital signature shows others +that you trust that commit, and the immutability of the history of +commits tells others that they can trust the whole history. + +In other words, you can easily validate a whole archive by just +sending out a single email that tells the people the name (SHA1 hash) +of the top commit, and digitally sign that email using something +like GPG/PGP. + +To assist in this, git also provides the tag object... + +Tag Object +~~~~~~~~~~ +Git provides the "tag" object to simplify creating, managing and +exchanging symbolic and signed tokens. The "tag" object at its +simplest simply symbolically identifies another object by containing +the sha1, type and symbolic name. + +However it can optionally contain additional signature information +(which git doesn't care about as long as there's less than 8k of +it). This can then be verified externally to git. + +Note that despite the tag features, "git" itself only handles content +integrity; the trust framework (and signature provision and +verification) has to come from outside. + +A tag is created with gitlink:git-mktag[1], +its data can be accessed by gitlink:git-cat-file[1], +and the signature can be verified by +gitlink:git-verify-tag[1]. + + +The "index" aka "Current Directory Cache" +----------------------------------------- +The index is a simple binary file, which contains an efficient +representation of a virtual directory content at some random time. It +does so by a simple array that associates a set of names, dates, +permissions and content (aka "blob") objects together. The cache is +always kept ordered by name, and names are unique (with a few very +specific rules) at any point in time, but the cache has no long-term +meaning, and can be partially updated at any time. + +In particular, the index certainly does not need to be consistent with +the current directory contents (in fact, most operations will depend on +different ways to make the index 'not' be consistent with the directory +hierarchy), but it has three very important attributes: + +'(a) it can re-generate the full state it caches (not just the +directory structure: it contains pointers to the "blob" objects so +that it can regenerate the data too)' + +As a special case, there is a clear and unambiguous one-way mapping +from a current directory cache to a "tree object", which can be +efficiently created from just the current directory cache without +actually looking at any other data. So a directory cache at any one +time uniquely specifies one and only one "tree" object (but has +additional data to make it easy to match up that tree object with what +has happened in the directory) + +'(b) it has efficient methods for finding inconsistencies between that +cached state ("tree object waiting to be instantiated") and the +current state.' + +'(c) it can additionally efficiently represent information about merge +conflicts between different tree objects, allowing each pathname to be +associated with sufficient information about the trees involved that +you can create a three-way merge between them.' + +Those are the three ONLY things that the directory cache does. It's a +cache, and the normal operation is to re-generate it completely from a +known tree object, or update/compare it with a live tree that is being +developed. If you blow the directory cache away entirely, you generally +haven't lost any information as long as you have the name of the tree +that it described. + +At the same time, the index is at the same time also the +staging area for creating new trees, and creating a new tree always +involves a controlled modification of the index file. In particular, +the index file can have the representation of an intermediate tree that +has not yet been instantiated. So the index can be thought of as a +write-back cache, which can contain dirty information that has not yet +been written back to the backing store. + + + +The Workflow +------------ +Generally, all "git" operations work on the index file. Some operations +work *purely* on the index file (showing the current state of the +index), but most operations move data to and from the index file. Either +from the database or from the working directory. Thus there are four +main combinations: + +1) working directory -> index +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You update the index with information from the working directory with +the gitlink:git-update-index[1] command. You +generally update the index information by just specifying the filename +you want to update, like so: + + git-update-index filename + +but to avoid common mistakes with filename globbing etc, the command +will not normally add totally new entries or remove old entries, +i.e. it will normally just update existing cache entries. + +To tell git that yes, you really do realize that certain files no +longer exist, or that new files should be added, you +should use the `--remove` and `--add` flags respectively. + +NOTE! A `--remove` flag does 'not' mean that subsequent filenames will +necessarily be removed: if the files still exist in your directory +structure, the index will be updated with their new status, not +removed. The only thing `--remove` means is that update-cache will be +considering a removed file to be a valid thing, and if the file really +does not exist any more, it will update the index accordingly. + +As a special case, you can also do `git-update-index --refresh`, which +will refresh the "stat" information of each index to match the current +stat information. It will 'not' update the object status itself, and +it will only update the fields that are used to quickly test whether +an object still matches its old backing store object. + +2) index -> object database +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You write your current index file to a "tree" object with the program + + git-write-tree + +that doesn't come with any options - it will just write out the +current index into the set of tree objects that describe that state, +and it will return the name of the resulting top-level tree. You can +use that tree to re-generate the index at any time by going in the +other direction: + +3) object database -> index +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You read a "tree" file from the object database, and use that to +populate (and overwrite - don't do this if your index contains any +unsaved state that you might want to restore later!) your current +index. Normal operation is just + + git-read-tree <sha1 of tree> + +and your index file will now be equivalent to the tree that you saved +earlier. However, that is only your 'index' file: your working +directory contents have not been modified. + +4) index -> working directory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You update your working directory from the index by "checking out" +files. This is not a very common operation, since normally you'd just +keep your files updated, and rather than write to your working +directory, you'd tell the index files about the changes in your +working directory (i.e. `git-update-index`). + +However, if you decide to jump to a new version, or check out somebody +else's version, or just restore a previous tree, you'd populate your +index file with read-tree, and then you need to check out the result +with + + git-checkout-index filename + +or, if you want to check out all of the index, use `-a`. + +NOTE! git-checkout-index normally refuses to overwrite old files, so +if you have an old version of the tree already checked out, you will +need to use the "-f" flag ('before' the "-a" flag or the filename) to +'force' the checkout. + + +Finally, there are a few odds and ends which are not purely moving +from one representation to the other: + +5) Tying it all together +~~~~~~~~~~~~~~~~~~~~~~~~ +To commit a tree you have instantiated with "git-write-tree", you'd +create a "commit" object that refers to that tree and the history +behind it - most notably the "parent" commits that preceded it in +history. + +Normally a "commit" has one parent: the previous state of the tree +before a certain change was made. However, sometimes it can have two +or more parent commits, in which case we call it a "merge", due to the +fact that such a commit brings together ("merges") two or more +previous states represented by other commits. + +In other words, while a "tree" represents a particular directory state +of a working directory, a "commit" represents that state in "time", +and explains how we got there. + +You create a commit object by giving it the tree that describes the +state at the time of the commit, and a list of parents: + + git-commit-tree <tree> -p <parent> [-p <parent2> ..] + +and then giving the reason for the commit on stdin (either through +redirection from a pipe or file, or by just typing it at the tty). + +git-commit-tree will return the name of the object that represents +that commit, and you should save it away for later use. Normally, +you'd commit a new `HEAD` state, and while git doesn't care where you +save the note about that state, in practice we tend to just write the +result to the file pointed at by `.git/HEAD`, so that we can always see +what the last committed state was. + +Here is an ASCII art by Jon Loeliger that illustrates how +various pieces fit together. + +------------ + + commit-tree + commit obj + +----+ + | | + | | + V V + +-----------+ + | Object DB | + | Backing | + | Store | + +-----------+ + ^ + write-tree | | + tree obj | | + | | read-tree + | | tree obj + V + +-----------+ + | Index | + | "cache" | + +-----------+ + update-index ^ + blob obj | | + | | + checkout-index -u | | checkout-index + stat | | blob obj + V + +-----------+ + | Working | + | Directory | + +-----------+ + +------------ + + +6) Examining the data +~~~~~~~~~~~~~~~~~~~~~ + +You can examine the data represented in the object database and the +index with various helper tools. For every object, you can use +gitlink:git-cat-file[1] to examine details about the +object: + + git-cat-file -t <objectname> + +shows the type of the object, and once you have the type (which is +usually implicit in where you find the object), you can use + + git-cat-file blob|tree|commit|tag <objectname> + +to show its contents. NOTE! Trees have binary content, and as a result +there is a special helper for showing that content, called +`git-ls-tree`, which turns the binary content into a more easily +readable form. + +It's especially instructive to look at "commit" objects, since those +tend to be small and fairly self-explanatory. In particular, if you +follow the convention of having the top commit name in `.git/HEAD`, +you can do + + git-cat-file commit HEAD + +to see what the top commit was. + +7) Merging multiple trees +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Git helps you do a three-way merge, which you can expand to n-way by +repeating the merge procedure arbitrary times until you finally +"commit" the state. The normal situation is that you'd only do one +three-way merge (two parents), and commit it, but if you like to, you +can do multiple parents in one go. + +To do a three-way merge, you need the two sets of "commit" objects +that you want to merge, use those to find the closest common parent (a +third "commit" object), and then use those commit objects to find the +state of the directory ("tree" object) at these points. + +To get the "base" for the merge, you first look up the common parent +of two commits with + + git-merge-base <commit1> <commit2> + +which will return you the commit they are both based on. You should +now look up the "tree" objects of those commits, which you can easily +do with (for example) + + git-cat-file commit <commitname> | head -1 + +since the tree object information is always the first line in a commit +object. + +Once you know the three trees you are going to merge (the one +"original" tree, aka the common case, and the two "result" trees, aka +the branches you want to merge), you do a "merge" read into the +index. This will complain if it has to throw away your old index contents, so you should +make sure that you've committed those - in fact you would normally +always do a merge against your last commit (which should thus match +what you have in your current index anyway). + +To do the merge, do + + git-read-tree -m -u <origtree> <yourtree> <targettree> + +which will do all trivial merge operations for you directly in the +index file, and you can just write the result out with +`git-write-tree`. + +Historical note. We did not have `-u` facility when this +section was first written, so we used to warn that +the merge is done in the index file, not in your +working tree, and your working tree will not match your +index after this step. +This is no longer true. The above command, thanks to `-u` +option, updates your working tree with the merge results for +paths that have been trivially merged. + + +8) Merging multiple trees, continued +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sadly, many merges aren't trivial. If there are files that have +been added.moved or removed, or if both branches have modified the +same file, you will be left with an index tree that contains "merge +entries" in it. Such an index tree can 'NOT' be written out to a tree +object, and you will have to resolve any such merge clashes using +other tools before you can write out the result. + +You can examine such index state with `git-ls-files --unmerged` +command. An example: + +------------------------------------------------ +$ git-read-tree -m $orig HEAD $target +$ git-ls-files --unmerged +100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1 hello.c +100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2 hello.c +100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello.c +------------------------------------------------ + +Each line of the `git-ls-files --unmerged` output begins with +the blob mode bits, blob SHA1, 'stage number', and the +filename. The 'stage number' is git's way to say which tree it +came from: stage 1 corresponds to `$orig` tree, stage 2 `HEAD` +tree, and stage3 `$target` tree. + +Earlier we said that trivial merges are done inside +`git-read-tree -m`. For example, if the file did not change +from `$orig` to `HEAD` nor `$target`, or if the file changed +from `$orig` to `HEAD` and `$orig` to `$target` the same way, +obviously the final outcome is what is in `HEAD`. What the +above example shows is that file `hello.c` was changed from +`$orig` to `HEAD` and `$orig` to `$target` in a different way. +You could resolve this by running your favorite 3-way merge +program, e.g. `diff3` or `merge`, on the blob objects from +these three stages yourself, like this: + +------------------------------------------------ +$ git-cat-file blob 263414f... >hello.c~1 +$ git-cat-file blob 06fa6a2... >hello.c~2 +$ git-cat-file blob cc44c73... >hello.c~3 +$ merge hello.c~2 hello.c~1 hello.c~3 +------------------------------------------------ + +This would leave the merge result in `hello.c~2` file, along +with conflict markers if there are conflicts. After verifying +the merge result makes sense, you can tell git what the final +merge result for this file is by: + + mv -f hello.c~2 hello.c + git-update-index hello.c + +When a path is in unmerged state, running `git-update-index` for +that path tells git to mark the path resolved. + +The above is the description of a git merge at the lowest level, +to help you understand what conceptually happens under the hood. +In practice, nobody, not even git itself, uses three `git-cat-file` +for this. There is `git-merge-index` program that extracts the +stages to temporary files and calls a "merge" script on it: + + git-merge-index git-merge-one-file hello.c + +and that is what higher level `git resolve` is implemented with. diff --git a/Documentation/core-tutorial.txt b/Documentation/core-tutorial.txt new file mode 100644 index 0000000000..9c28bea62e --- /dev/null +++ b/Documentation/core-tutorial.txt @@ -0,0 +1,1691 @@ +A git core tutorial for developers +================================== + +Introduction +------------ + +This is trying to be a short tutorial on setting up and using a git +repository, mainly because being hands-on and using explicit examples is +often the best way of explaining what is going on. + +In normal life, most people wouldn't use the "core" git programs +directly, but rather script around them to make them more palatable. +Understanding the core git stuff may help some people get those scripts +done, though, and it may also be instructive in helping people +understand what it is that the higher-level helper scripts are actually +doing. + +The core git is often called "plumbing", with the prettier user +interfaces on top of it called "porcelain". You may not want to use the +plumbing directly very often, but it can be good to know what the +plumbing does for when the porcelain isn't flushing. + +The material presented here often goes deep describing how things +work internally. If you are mostly interested in using git as a +SCM, you can skip them during your first pass. + +[NOTE] +And those "too deep" descriptions are often marked as Note. + +[NOTE] +If you are already familiar with another version control system, +like CVS, you may want to take a look at +link:everyday.html[Everyday GIT in 20 commands or so] first +before reading this. + + +Creating a git repository +------------------------- + +Creating a new git repository couldn't be easier: all git repositories start +out empty, and the only thing you need to do is find yourself a +subdirectory that you want to use as a working tree - either an empty +one for a totally new project, or an existing working tree that you want +to import into git. + +For our first example, we're going to start a totally new repository from +scratch, with no pre-existing files, and we'll call it `git-tutorial`. +To start up, create a subdirectory for it, change into that +subdirectory, and initialize the git infrastructure with `git-init`: + +------------------------------------------------ +$ mkdir git-tutorial +$ cd git-tutorial +$ git-init +------------------------------------------------ + +to which git will reply + +---------------- +Initialized empty Git repository in .git/ +---------------- + +which is just git's way of saying that you haven't been doing anything +strange, and that it will have created a local `.git` directory setup for +your new project. You will now have a `.git` directory, and you can +inspect that with `ls`. For your new empty project, it should show you +three entries, among other things: + + - a file called `HEAD`, that has `ref: refs/heads/master` in it. + This is similar to a symbolic link and points at + `refs/heads/master` relative to the `HEAD` file. ++ +Don't worry about the fact that the file that the `HEAD` link points to +doesn't even exist yet -- you haven't created the commit that will +start your `HEAD` development branch yet. + + - a subdirectory called `objects`, which will contain all the + objects of your project. You should never have any real reason to + look at the objects directly, but you might want to know that these + objects are what contains all the real 'data' in your repository. + + - a subdirectory called `refs`, which contains references to objects. + +In particular, the `refs` subdirectory will contain two other +subdirectories, named `heads` and `tags` respectively. They do +exactly what their names imply: they contain references to any number +of different 'heads' of development (aka 'branches'), and to any +'tags' that you have created to name specific versions in your +repository. + +One note: the special `master` head is the default branch, which is +why the `.git/HEAD` file was created points to it even if it +doesn't yet exist. Basically, the `HEAD` link is supposed to always +point to the branch you are working on right now, and you always +start out expecting to work on the `master` branch. + +However, this is only a convention, and you can name your branches +anything you want, and don't have to ever even 'have' a `master` +branch. A number of the git tools will assume that `.git/HEAD` is +valid, though. + +[NOTE] +An 'object' is identified by its 160-bit SHA1 hash, aka 'object name', +and a reference to an object is always the 40-byte hex +representation of that SHA1 name. The files in the `refs` +subdirectory are expected to contain these hex references +(usually with a final `\'\n\'` at the end), and you should thus +expect to see a number of 41-byte files containing these +references in these `refs` subdirectories when you actually start +populating your tree. + +[NOTE] +An advanced user may want to take a look at the +link:repository-layout.html[repository layout] document +after finishing this tutorial. + +You have now created your first git repository. Of course, since it's +empty, that's not very useful, so let's start populating it with data. + + +Populating a git repository +--------------------------- + +We'll keep this simple and stupid, so we'll start off with populating a +few trivial files just to get a feel for it. + +Start off with just creating any random files that you want to maintain +in your git repository. We'll start off with a few bad examples, just to +get a feel for how this works: + +------------------------------------------------ +$ echo "Hello World" >hello +$ echo "Silly example" >example +------------------------------------------------ + +you have now created two files in your working tree (aka 'working directory'), +but to actually check in your hard work, you will have to go through two steps: + + - fill in the 'index' file (aka 'cache') with the information about your + working tree state. + + - commit that index file as an object. + +The first step is trivial: when you want to tell git about any changes +to your working tree, you use the `git-update-index` program. That +program normally just takes a list of filenames you want to update, but +to avoid trivial mistakes, it refuses to add new entries to the index +(or remove existing ones) unless you explicitly tell it that you're +adding a new entry with the `\--add` flag (or removing an entry with the +`\--remove`) flag. + +So to populate the index with the two files you just created, you can do + +------------------------------------------------ +$ git-update-index --add hello example +------------------------------------------------ + +and you have now told git to track those two files. + +In fact, as you did that, if you now look into your object directory, +you'll notice that git will have added two new objects to the object +database. If you did exactly the steps above, you should now be able to do + + +---------------- +$ ls .git/objects/??/* +---------------- + +and see two files: + +---------------- +.git/objects/55/7db03de997c86a4a028e1ebd3a1ceb225be238 +.git/objects/f2/4c74a2e500f5ee1332c86b94199f52b1d1d962 +---------------- + +which correspond with the objects with names of `557db...` and +`f24c7...` respectively. + +If you want to, you can use `git-cat-file` to look at those objects, but +you'll have to use the object name, not the filename of the object: + +---------------- +$ git-cat-file -t 557db03de997c86a4a028e1ebd3a1ceb225be238 +---------------- + +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 (i.e., just a +regular file), and you can see the contents with + +---------------- +$ git-cat-file "blob" 557db03 +---------------- + +which will print out "Hello World". The object `557db03` is nothing +more than the contents of your file `hello`. + +[NOTE] +Don't confuse that object with the file `hello` itself. The +object is literally just those specific *contents* of the file, and +however much you later change the contents in file `hello`, the object +we just looked at will never change. Objects are immutable. + +[NOTE] +The second example demonstrates that you can +abbreviate the object name to only the first several +hexadecimal digits in most places. + +Anyway, as we mentioned previously, you normally never actually take a +look at the objects themselves, and typing long 40-character hex +names is not something you'd normally want to do. The above digression +was just to show that `git-update-index` did something magical, and +actually saved away the contents of your files into the git object +database. + +Updating the index did something else too: it created a `.git/index` +file. This is the index that describes your current working tree, and +something you should be very aware of. Again, you normally never worry +about the index file itself, but you should be aware of the fact that +you have not actually really "checked in" your files into git so far, +you've only *told* git about them. + +However, since git knows about them, you can now start using some of the +most basic git commands to manipulate the files or look at their status. + +In particular, let's not even check in the two files into git yet, we'll +start off by adding another line to `hello` first: + +------------------------------------------------ +$ echo "It's a new day for git" >>hello +------------------------------------------------ + +and you can now, since you told git about the previous state of `hello`, ask +git what has changed in the tree compared to your old index, using the +`git-diff-files` command: + +------------ +$ git-diff-files +------------ + +Oops. That wasn't very readable. It just spit out its own internal +version of a `diff`, but that internal version really just tells you +that it has noticed that "hello" has been modified, and that the old object +contents it had have been replaced with something else. + +To make it readable, we can tell git-diff-files to output the +differences as a patch, using the `-p` flag: + +------------ +$ git-diff-files -p +diff --git a/hello b/hello +index 557db03..263414f 100644 +--- a/hello ++++ b/hello +@@ -1 +1,2 @@ + Hello World ++It's a new day for git +---- + +i.e. the diff of the change we caused by adding another line to `hello`. + +In other words, `git-diff-files` always shows us the difference between +what is recorded in the index, and what is currently in the working +tree. That's very useful. + +A common shorthand for `git-diff-files -p` is to just write `git +diff`, which will do the same thing. + +------------ +$ git diff +diff --git a/hello b/hello +index 557db03..263414f 100644 +--- a/hello ++++ b/hello +@@ -1 +1,2 @@ + Hello World ++It's a new day for git +------------ + + +Committing git state +-------------------- + +Now, we want to go to the next stage in git, which is to take the files +that git knows about in the index, and commit them as a real tree. We do +that in two phases: creating a 'tree' object, and committing that 'tree' +object as a 'commit' object together with an explanation of what the +tree was all about, along with information of how we came to that state. + +Creating a tree object is trivial, and is done with `git-write-tree`. +There are no options or other input: git-write-tree will take the +current index state, and write an object that describes that whole +index. In other words, we're now tying together all the different +filenames with their contents (and their permissions), and we're +creating the equivalent of a git "directory" object: + +------------------------------------------------ +$ git-write-tree +------------------------------------------------ + +and this will just output the name of the resulting tree, in this case +(if you have done exactly as I've described) it should be + +---------------- +8988da15d077d4829fc51d8544c097def6644dbb +---------------- + +which is another incomprehensible object name. Again, if you want to, +you can use `git-cat-file -t 8988d\...` to see that this time the object +is not a "blob" object, but a "tree" object (you can also use +`git-cat-file` to actually output the raw object contents, but you'll see +mainly a binary mess, so that's less interesting). + +However -- normally you'd never use `git-write-tree` on its own, because +normally you always commit a tree into a commit object using the +`git-commit-tree` command. In fact, it's easier to not actually use +`git-write-tree` on its own at all, but to just pass its result in as an +argument to `git-commit-tree`. + +`git-commit-tree` normally takes several arguments -- it wants to know +what the 'parent' of a commit was, but since this is the first commit +ever in this new repository, and it has no parents, we only need to pass in +the object name of the tree. However, `git-commit-tree` +also wants to get a commit message +on its standard input, and it will write out the resulting object name for the +commit to its standard output. + +And this is where we create the `.git/refs/heads/master` file +which is pointed at by `HEAD`. This file is supposed to contain +the reference to the top-of-tree of the master branch, and since +that's exactly what `git-commit-tree` spits out, we can do this +all with a sequence of simple shell commands: + +------------------------------------------------ +$ tree=$(git-write-tree) +$ commit=$(echo 'Initial commit' | git-commit-tree $tree) +$ git-update-ref HEAD $commit +------------------------------------------------ + +In this case this creates a totally new commit that is not related to +anything else. Normally you do this only *once* for a project ever, and +all later commits will be parented on top of an earlier commit. + +Again, normally you'd never actually do this by hand. There is a +helpful script called `git commit` that will do all of this for you. So +you could have just written `git commit` +instead, and it would have done the above magic scripting for you. + + +Making a change +--------------- + +Remember how we did the `git-update-index` on file `hello` and then we +changed `hello` afterward, and could compare the new state of `hello` with the +state we saved in the index file? + +Further, remember how I said that `git-write-tree` writes the contents +of the *index* file to the tree, and thus what we just committed was in +fact the *original* contents of the file `hello`, not the new ones. We did +that on purpose, to show the difference between the index state, and the +state in the working tree, and how they don't have to match, even +when we commit things. + +As before, if we do `git-diff-files -p` in our git-tutorial project, +we'll still see the same difference we saw last time: the index file +hasn't changed by the act of committing anything. However, now that we +have committed something, we can also learn to use a new command: +`git-diff-index`. + +Unlike `git-diff-files`, which showed the difference between the index +file and the working tree, `git-diff-index` shows the differences +between a committed *tree* and either the index file or the working +tree. In other words, `git-diff-index` wants a tree to be diffed +against, and before we did the commit, we couldn't do that, because we +didn't have anything to diff against. + +But now we can do + +---------------- +$ git-diff-index -p HEAD +---------------- + +(where `-p` has the same meaning as it did in `git-diff-files`), and it +will show us the same difference, but for a totally different reason. +Now we're comparing the working tree not against the index file, +but against the tree we just wrote. It just so happens that those two +are obviously the same, so we get the same result. + +Again, because this is a common operation, you can also just shorthand +it with + +---------------- +$ git diff HEAD +---------------- + +which ends up doing the above for you. + +In other words, `git-diff-index` normally compares a tree against the +working tree, but when given the `\--cached` flag, it is told to +instead compare against just the index cache contents, and ignore the +current working tree state entirely. Since we just wrote the index +file to HEAD, doing `git-diff-index \--cached -p HEAD` should thus return +an empty set of differences, and that's exactly what it does. + +[NOTE] +================ +`git-diff-index` really always uses the index for its +comparisons, and saying that it compares a tree against the working +tree is thus not strictly accurate. In particular, the list of +files to compare (the "meta-data") *always* comes from the index file, +regardless of whether the `\--cached` flag is used or not. The `\--cached` +flag really only determines whether the file *contents* to be compared +come from the working tree or not. + +This is not hard to understand, as soon as you realize that git simply +never knows (or cares) about files that it is not told about +explicitly. git will never go *looking* for files to compare, it +expects you to tell it what the files are, and that's what the index +is there for. +================ + +However, our next step is to commit the *change* we did, and again, to +understand what's going on, keep in mind the difference between "working +tree contents", "index file" and "committed tree". We have changes +in the working tree that we want to commit, and we always have to +work through the index file, so the first thing we need to do is to +update the index cache: + +------------------------------------------------ +$ git-update-index hello +------------------------------------------------ + +(note how we didn't need the `\--add` flag this time, since git knew +about the file already). + +Note what happens to the different `git-diff-\*` versions here. After +we've updated `hello` in the index, `git-diff-files -p` now shows no +differences, but `git-diff-index -p HEAD` still *does* show that the +current state is different from the state we committed. In fact, now +`git-diff-index` shows the same difference whether we use the `--cached` +flag or not, since now the index is coherent with the working tree. + +Now, since we've updated `hello` in the index, we can commit the new +version. We could do it by writing the tree by hand again, and +committing the tree (this time we'd have to use the `-p HEAD` flag to +tell commit that the HEAD was the *parent* of the new commit, and that +this wasn't an initial commit any more), but you've done that once +already, so let's just use the helpful script this time: + +------------------------------------------------ +$ git commit +------------------------------------------------ + +which starts an editor for you to write the commit message and tells you +a bit about what you have done. + +Write whatever message you want, and all the lines that start with '#' +will be pruned out, and the rest will be used as the commit message for +the change. If you decide you don't want to commit anything after all at +this point (you can continue to edit things and update the index), you +can just leave an empty message. Otherwise `git commit` will commit +the change for you. + +You've now made your first real git commit. And if you're interested in +looking at what `git commit` really does, feel free to investigate: +it's a few very simple shell scripts to generate the helpful (?) commit +message headers, and a few one-liners that actually do the +commit itself (`git-commit`). + + +Inspecting Changes +------------------ + +While creating changes is useful, it's even more useful if you can tell +later what changed. The most useful command for this is another of the +`diff` family, namely `git-diff-tree`. + +`git-diff-tree` can be given two arbitrary trees, and it will tell you the +differences between them. Perhaps even more commonly, though, you can +give it just a single commit object, and it will figure out the parent +of that commit itself, and show the difference directly. Thus, to get +the same diff that we've already seen several times, we can now do + +---------------- +$ git-diff-tree -p HEAD +---------------- + +(again, `-p` means to show the difference as a human-readable patch), +and it will show what the last commit (in `HEAD`) actually changed. + +[NOTE] +============ +Here is an ASCII art by Jon Loeliger that illustrates how +various diff-\* commands compare things. + + diff-tree + +----+ + | | + | | + V V + +-----------+ + | Object DB | + | Backing | + | Store | + +-----------+ + ^ ^ + | | + | | diff-index --cached + | | + diff-index | V + | +-----------+ + | | Index | + | | "cache" | + | +-----------+ + | ^ + | | + | | diff-files + | | + V V + +-----------+ + | Working | + | Directory | + +-----------+ +============ + +More interestingly, you can also give `git-diff-tree` the `--pretty` flag, +which tells it to also show the commit message and author and date of the +commit, and you can tell it to show a whole series of diffs. +Alternatively, you can tell it to be "silent", and not show the diffs at +all, but just show the actual commit message. + +In fact, together with the `git-rev-list` program (which generates a +list of revisions), `git-diff-tree` ends up being a veritable fount of +changes. A trivial (but very useful) script called `git-whatchanged` is +included with git which does exactly this, and shows a log of recent +activities. + +To see the whole history of our pitiful little git-tutorial project, you +can do + +---------------- +$ git log +---------------- + +which shows just the log messages, or if we want to see the log together +with the associated patches use the more complex (and much more +powerful) + +---------------- +$ git-whatchanged -p --root +---------------- + +and you will see exactly what has changed in the repository over its +short history. + +[NOTE] +The `\--root` flag is a flag to `git-diff-tree` to tell it to +show the initial aka 'root' commit too. Normally you'd probably not +want to see the initial import diff, but since the tutorial project +was started from scratch and is so small, we use it to make the result +a bit more interesting. + +With that, you should now be having some inkling of what git does, and +can explore on your own. + +[NOTE] +Most likely, you are not directly using the core +git Plumbing commands, but using Porcelain like Cogito on top +of it. Cogito works a bit differently and you usually do not +have to run `git-update-index` yourself for changed files (you +do tell underlying git about additions and removals via +`cg-add` and `cg-rm` commands). Just before you make a commit +with `cg-commit`, Cogito figures out which files you modified, +and runs `git-update-index` on them for you. + + +Tagging a version +----------------- + +In git, there are two kinds of tags, a "light" one, and an "annotated tag". + +A "light" tag is technically nothing more than a branch, except we put +it in the `.git/refs/tags/` subdirectory instead of calling it a `head`. +So the simplest form of tag involves nothing more than + +------------------------------------------------ +$ git tag my-first-tag +------------------------------------------------ + +which just writes the current `HEAD` into the `.git/refs/tags/my-first-tag` +file, after which point you can then use this symbolic name for that +particular state. You can, for example, do + +---------------- +$ git diff my-first-tag +---------------- + +to diff your current state against that tag (which at this point will +obviously be an empty diff, but if you continue to develop and commit +stuff, you can use your tag as an "anchor-point" to see what has changed +since you tagged it. + +An "annotated tag" is actually a real git object, and contains not only a +pointer to the state you want to tag, but also a small tag name and +message, along with optionally a PGP signature that says that yes, +you really did +that tag. You create these annotated tags with either the `-a` or +`-s` flag to `git tag`: + +---------------- +$ git tag -s <tagname> +---------------- + +which will sign the current `HEAD` (but you can also give it another +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 +like that, while the light-weight tags are useful for any marking you +want to do -- any time you decide that you want to remember a certain +point, just create a private tag for it, and you have a nice symbolic +name for the state at that point. + + +Copying repositories +-------------------- + +git repositories are normally totally self-sufficient and relocatable. +Unlike CVS, for example, there is no separate notion of +"repository" and "working tree". A git repository normally *is* the +working tree, with the local git information hidden in the `.git` +subdirectory. There is nothing else. What you see is what you got. + +[NOTE] +You can tell git to split the git internal information from +the directory that it tracks, but we'll ignore that for now: it's not +how normal projects work, and it's really only meant for special uses. +So the mental model of "the git information is always tied directly to +the working tree that it describes" may not be technically 100% +accurate, but it's a good model for all normal use. + +This has two implications: + + - if you grow bored with the tutorial repository you created (or you've + made a mistake and want to start all over), you can just do simple ++ +---------------- +$ rm -rf git-tutorial +---------------- ++ +and it will be gone. There's no external repository, and there's no +history outside the project you created. + + - if you want to move or duplicate a git repository, you can do so. There + is `git clone` command, but if all you want to do is just to + create a copy of your repository (with all the full history that + went along with it), you can do so with a regular + `cp -a git-tutorial new-git-tutorial`. ++ +Note that when you've moved or copied a git repository, your git index +file (which caches various information, notably some of the "stat" +information for the files involved) will likely need to be refreshed. +So after you do a `cp -a` to create a new copy, you'll want to do ++ +---------------- +$ git-update-index --refresh +---------------- ++ +in the new repository to make sure that the index file is up-to-date. + +Note that the second point is true even across machines. You can +duplicate a remote git repository with *any* regular copy mechanism, be it +`scp`, `rsync` or `wget`. + +When copying a remote repository, you'll want to at a minimum update the +index cache when you do this, and especially with other peoples' +repositories you often want to make sure that the index cache is in some +known state (you don't know *what* they've done and not yet checked in), +so usually you'll precede the `git-update-index` with a + +---------------- +$ git-read-tree --reset HEAD +$ git-update-index --refresh +---------------- + +which will force a total index re-build from the tree pointed to by `HEAD`. +It resets the index contents to `HEAD`, and then the `git-update-index` +makes sure to match up all index entries with the checked-out files. +If the original repository had uncommitted changes in its +working tree, `git-update-index --refresh` notices them and +tells you they need to be updated. + +The above can also be written as simply + +---------------- +$ git reset +---------------- + +and in fact a lot of the common git command combinations can be scripted +with the `git xyz` interfaces. You can learn things by just looking +at what the various git scripts do. For example, `git reset` is the +above two lines implemented in `git-reset`, but some things like +`git status` and `git commit` are slightly more complex scripts around +the basic git commands. + +Many (most?) public remote repositories will not contain any of +the checked out files or even an index file, and will *only* contain the +actual core git files. Such a repository usually doesn't even have the +`.git` subdirectory, but has all the git files directly in the +repository. + +To create your own local live copy of such a "raw" git repository, you'd +first create your own subdirectory for the project, and then copy the +raw repository contents into the `.git` directory. For example, to +create your own copy of the git repository, you'd do the following + +---------------- +$ mkdir my-git +$ cd my-git +$ rsync -rL rsync://rsync.kernel.org/pub/scm/git/git.git/ .git +---------------- + +followed by + +---------------- +$ git-read-tree HEAD +---------------- + +to populate the index. However, now you have populated the index, and +you have all the git internal files, but you will notice that you don't +actually have any of the working tree files to work on. To get +those, you'd check them out with + +---------------- +$ git-checkout-index -u -a +---------------- + +where the `-u` flag means that you want the checkout to keep the index +up-to-date (so that you don't have to refresh it afterward), and the +`-a` flag means "check out all files" (if you have a stale copy or an +older version of a checked out tree you may also need to add the `-f` +flag first, to tell git-checkout-index to *force* overwriting of any old +files). + +Again, this can all be simplified with + +---------------- +$ git clone rsync://rsync.kernel.org/pub/scm/git/git.git/ my-git +$ cd my-git +$ git checkout +---------------- + +which will end up doing all of the above for you. + +You have now successfully copied somebody else's (mine) remote +repository, and checked it out. + + +Creating a new branch +--------------------- + +Branches in git are really nothing more than pointers into the git +object database from within the `.git/refs/` subdirectory, and as we +already discussed, the `HEAD` branch is nothing but a symlink to one of +these object pointers. + +You can at any time create a new branch by just picking an arbitrary +point in the project history, and just writing the SHA1 name of that +object into a file under `.git/refs/heads/`. You can use any filename you +want (and indeed, subdirectories), but the convention is that the +"normal" branch is called `master`. That's just a convention, though, +and nothing enforces it. + +To show that as an example, let's go back to the git-tutorial repository we +used earlier, and create a branch in it. You do that by simply just +saying that you want to check out a new branch: + +------------ +$ git checkout -b mybranch +------------ + +will create a new branch based at the current `HEAD` position, and switch +to it. + +[NOTE] +================================================ +If you make the decision to start your new branch at some +other point in the history than the current `HEAD`, you can do so by +just telling `git checkout` what the base of the checkout would be. +In other words, if you have an earlier tag or branch, you'd just do + +------------ +$ git checkout -b mybranch earlier-commit +------------ + +and it would create the new branch `mybranch` at the earlier commit, +and check out the state at that time. +================================================ + +You can always just jump back to your original `master` branch by doing + +------------ +$ git checkout master +------------ + +(or any other branch-name, for that matter) and if you forget which +branch you happen to be on, a simple + +------------ +$ cat .git/HEAD +------------ + +will tell you where it's pointing. To get the list of branches +you have, you can say + +------------ +$ git branch +------------ + +which is nothing more than a simple script around `ls .git/refs/heads`. +There will be asterisk in front of the branch you are currently on. + +Sometimes you may wish to create a new branch _without_ actually +checking it out and switching to it. If so, just use the command + +------------ +$ git branch <branchname> [startingpoint] +------------ + +which will simply _create_ the branch, but will not do anything further. +You can then later -- once you decide that you want to actually develop +on that branch -- switch to that branch with a regular `git checkout` +with the branchname as the argument. + + +Merging two branches +-------------------- + +One of the ideas of having a branch is that you do some (possibly +experimental) work in it, and eventually merge it back to the main +branch. So assuming you created the above `mybranch` that started out +being the same as the original `master` branch, let's make sure we're in +that branch, and do some work there. + +------------------------------------------------ +$ git checkout mybranch +$ echo "Work, work, work" >>hello +$ git commit -m 'Some work.' -i hello +------------------------------------------------ + +Here, we just added another line to `hello`, and we used a shorthand for +doing both `git-update-index hello` and `git commit` by just giving the +filename directly to `git commit`, with an `-i` flag (it tells +git to 'include' that file in addition to what you have done to +the index file so far when making the commit). The `-m` flag is to give the +commit log message from the command line. + +Now, to make it a bit more interesting, let's assume that somebody else +does some work in the original branch, and simulate that by going back +to the master branch, and editing the same file differently there: + +------------ +$ git checkout master +------------ + +Here, take a moment to look at the contents of `hello`, and notice how they +don't contain the work we just did in `mybranch` -- because that work +hasn't happened in the `master` branch at all. Then do + +------------ +$ echo "Play, play, play" >>hello +$ echo "Lots of fun" >>example +$ git commit -m 'Some fun.' -i hello example +------------ + +since the master branch is obviously in a much better mood. + +Now, you've got two branches, and you decide that you want to merge the +work done. Before we do that, let's introduce a cool graphical tool that +helps you view what's going on: + +---------------- +$ gitk --all +---------------- + +will show you graphically both of your branches (that's what the `\--all` +means: normally it will just show you your current `HEAD`) and their +histories. You can also see exactly how they came to be from a common +source. + +Anyway, let's exit `gitk` (`^Q` or the File menu), and decide that we want +to merge the work we did on the `mybranch` branch into the `master` +branch (which is currently our `HEAD` too). To do that, there's a nice +script called `git merge`, which wants to know which branches you want +to resolve and what the merge is all about: + +------------ +$ git merge "Merge work in mybranch" HEAD mybranch +------------ + +where the first argument is going to be used as the commit message if +the merge can be resolved automatically. + +Now, in this case we've intentionally created a situation where the +merge will need to be fixed up by hand, though, so git will do as much +of it as it can automatically (which in this case is just merge the `example` +file, which had no differences in the `mybranch` branch), and say: + +---------------- + Auto-merging hello + CONFLICT (content): Merge conflict in hello + Automatic merge failed; fix up by hand +---------------- + +It tells you that it did an "Automatic merge", which +failed due to conflicts in `hello`. + +Not to worry. It left the (trivial) conflict in `hello` in the same form you +should already be well used to if you've ever used CVS, so let's just +open `hello` in our editor (whatever that may be), and fix it up somehow. +I'd suggest just making it so that `hello` contains all four lines: + +------------ +Hello World +It's a new day for git +Play, play, play +Work, work, work +------------ + +and once you're happy with your manual merge, just do a + +------------ +$ git commit -i hello +------------ + +which will very loudly warn you that you're now committing a merge +(which is correct, so never mind), and you can write a small merge +message about your adventures in git-merge-land. + +After you're done, start up `gitk \--all` to see graphically what the +history looks like. Notice that `mybranch` still exists, and you can +switch to it, and continue to work with it if you want to. The +`mybranch` branch will not contain the merge, but next time you merge it +from the `master` branch, git will know how you merged it, so you'll not +have to do _that_ merge again. + +Another useful tool, especially if you do not always work in X-Window +environment, is `git show-branch`. + +------------------------------------------------ +$ git show-branch --topo-order master mybranch +* [master] Merge work in mybranch + ! [mybranch] Some work. +-- +- [master] Merge work in mybranch +*+ [mybranch] Some work. +------------------------------------------------ + +The first two lines indicate that it is showing the two branches +and the first line of the commit log message from their +top-of-the-tree commits, you are currently on `master` branch +(notice the asterisk `\*` character), and the first column for +the later output lines is used to show commits contained in the +`master` branch, and the second column for the `mybranch` +branch. Three commits are shown along with their log messages. +All of them have non blank characters in the first column (`*` +shows an ordinary commit on the current branch, `.` is a merge commit), which +means they are now part of the `master` branch. Only the "Some +work" commit has the plus `+` character in the second column, +because `mybranch` has not been merged to incorporate these +commits from the master branch. The string inside brackets +before the commit log message is a short name you can use to +name the commit. In the above example, 'master' and 'mybranch' +are branch heads. 'master~1' is the first parent of 'master' +branch head. Please see 'git-rev-parse' documentation if you +see more complex cases. + +Now, let's pretend you are the one who did all the work in +`mybranch`, and the fruit of your hard work has finally been merged +to the `master` branch. Let's go back to `mybranch`, and run +resolve to get the "upstream changes" back to your branch. + +------------ +$ git checkout mybranch +$ git merge "Merge upstream changes." HEAD master +------------ + +This outputs something like this (the actual commit object names +would be different) + +---------------- +Updating from ae3a2da... to a80b4aa.... +Fast forward + example | 1 + + hello | 1 + + 2 files changed, 2 insertions(+), 0 deletions(-) +---------------- + +Because your branch did not contain anything more than what are +already merged into the `master` branch, the resolve operation did +not actually do a merge. Instead, it just updated the top of +the tree of your branch to that of the `master` branch. This is +often called 'fast forward' merge. + +You can run `gitk \--all` again to see how the commit ancestry +looks like, or run `show-branch`, which tells you this. + +------------------------------------------------ +$ git show-branch master mybranch +! [master] Merge work in mybranch + * [mybranch] Merge work in mybranch +-- +-- [master] Merge work in mybranch +------------------------------------------------ + + +Merging external work +--------------------- + +It's usually much more common that you merge with somebody else than +merging with your own branches, so it's worth pointing out that git +makes that very easy too, and in fact, it's not that different from +doing a `git merge`. In fact, a remote merge ends up being nothing +more than "fetch the work from a remote repository into a temporary tag" +followed by a `git merge`. + +Fetching from a remote repository is done by, unsurprisingly, +`git fetch`: + +---------------- +$ git fetch <remote-repository> +---------------- + +One of the following transports can be used to name the +repository to download from: + +Rsync:: + `rsync://remote.machine/path/to/repo.git/` ++ +Rsync transport is usable for both uploading and downloading, +but is completely unaware of what git does, and can produce +unexpected results when you download from the public repository +while the repository owner is uploading into it via `rsync` +transport. Most notably, it could update the files under +`refs/` which holds the object name of the topmost commits +before uploading the files in `objects/` -- the downloader would +obtain head commit object name while that object itself is still +not available in the repository. For this reason, it is +considered deprecated. + +SSH:: + `remote.machine:/path/to/repo.git/` or ++ +`ssh://remote.machine/path/to/repo.git/` ++ +This transport can be used for both uploading and downloading, +and requires you to have a log-in privilege over `ssh` to the +remote machine. It finds out the set of objects the other side +lacks by exchanging the head commits both ends have and +transfers (close to) minimum set of objects. It is by far the +most efficient way to exchange git objects between repositories. + +Local directory:: + `/path/to/repo.git/` ++ +This transport is the same as SSH transport but uses `sh` to run +both ends on the local machine instead of running other end on +the remote machine via `ssh`. + +git Native:: + `git://remote.machine/path/to/repo.git/` ++ +This transport was designed for anonymous downloading. Like SSH +transport, it finds out the set of objects the downstream side +lacks and transfers (close to) minimum set of objects. + +HTTP(S):: + `http://remote.machine/path/to/repo.git/` ++ +Downloader from http and https URL +first obtains the topmost commit object name from the remote site +by looking at the specified refname under `repo.git/refs/` directory, +and then tries to obtain the +commit object by downloading from `repo.git/objects/xx/xxx\...` +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 behavior, they are +sometimes also called 'commit walkers'. ++ +The 'commit walkers' are sometimes also called 'dumb +transports', because they do not require any git aware smart +server like git Native transport does. Any stock HTTP server +that does not even support directory index would suffice. But +you must prepare your repository with `git-update-server-info` +to help dumb transport downloaders. ++ +There are (confusingly enough) `git-ssh-fetch` and `git-ssh-upload` +programs, which are 'commit walkers'; they outlived their +usefulness when git Native and SSH transports were introduced, +and not used by `git pull` or `git push` scripts. + +Once you fetch from the remote repository, you `resolve` that +with your current branch. + +However -- it's such a common thing to `fetch` and then +immediately `resolve`, that it's called `git pull`, and you can +simply do + +---------------- +$ git pull <remote-repository> +---------------- + +and optionally give a branch-name for the remote end as a second +argument. + +[NOTE] +You could do without using any branches at all, by +keeping as many local repositories as you would like to have +branches, and merging between them with `git pull`, just like +you merge between branches. The advantage of this approach is +that it lets you keep a set of files for each `branch` checked +out and you may find it easier to switch back and forth if you +juggle multiple lines of development simultaneously. Of +course, you will pay the price of more disk usage to hold +multiple working trees, but disk space is cheap these days. + +It is likely that you will be pulling from the same remote +repository from time to time. As a short hand, you can store +the remote repository URL in the local repository's config file +like this: + +------------------------------------------------ +$ git config remote.linus.url http://www.kernel.org/pub/scm/git/git.git/ +------------------------------------------------ + +and use the "linus" keyword with `git pull` instead of the full URL. + +Examples. + +. `git pull linus` +. `git pull linus tag v0.99.1` + +the above are equivalent to: + +. `git pull http://www.kernel.org/pub/scm/git/git.git/ HEAD` +. `git pull http://www.kernel.org/pub/scm/git/git.git/ tag v0.99.1` + + +How does the merge work? +------------------------ + +We said this tutorial shows what plumbing does to help you cope +with the porcelain that isn't flushing, but we so far did not +talk about how the merge really works. If you are following +this tutorial the first time, I'd suggest to skip to "Publishing +your work" section and come back here later. + +OK, still with me? To give us an example to look at, let's go +back to the earlier repository with "hello" and "example" file, +and bring ourselves back to the pre-merge state: + +------------ +$ git show-branch --more=3 master mybranch +! [master] Merge work in mybranch + * [mybranch] Merge work in mybranch +-- +-- [master] Merge work in mybranch ++* [master^2] Some work. ++* [master^] Some fun. +------------ + +Remember, before running `git merge`, our `master` head was at +"Some fun." commit, while our `mybranch` head was at "Some +work." commit. + +------------ +$ git checkout mybranch +$ git reset --hard master^2 +$ git checkout master +$ git reset --hard master^ +------------ + +After rewinding, the commit structure should look like this: + +------------ +$ git show-branch +* [master] Some fun. + ! [mybranch] Some work. +-- + + [mybranch] Some work. +* [master] Some fun. +*+ [mybranch^] New day. +------------ + +Now we are ready to experiment with the merge by hand. + +`git merge` command, when merging two branches, uses 3-way merge +algorithm. First, it finds the common ancestor between them. +The command it uses is `git-merge-base`: + +------------ +$ mb=$(git-merge-base HEAD mybranch) +------------ + +The command writes the commit object name of the common ancestor +to the standard output, so we captured its output to a variable, +because we will be using it in the next step. BTW, the common +ancestor commit is the "New day." commit in this case. You can +tell it by: + +------------ +$ git-name-rev $mb +my-first-tag +------------ + +After finding out a common ancestor commit, the second step is +this: + +------------ +$ git-read-tree -m -u $mb HEAD mybranch +------------ + +This is the same `git-read-tree` command we have already seen, +but it takes three trees, unlike previous examples. This reads +the contents of each tree into different 'stage' in the index +file (the first tree goes to stage 1, the second stage 2, +etc.). After reading three trees into three stages, the paths +that are the same in all three stages are 'collapsed' into stage +0. Also paths that are the same in two of three stages are +collapsed into stage 0, taking the SHA1 from either stage 2 or +stage 3, whichever is different from stage 1 (i.e. only one side +changed from the common ancestor). + +After 'collapsing' operation, paths that are different in three +trees are left in non-zero stages. At this point, you can +inspect the index file with this command: + +------------ +$ git-ls-files --stage +100644 7f8b141b65fdcee47321e399a2598a235a032422 0 example +100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1 hello +100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2 hello +100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello +------------ + +In our example of only two files, we did not have unchanged +files so only 'example' resulted in collapsing, but in real-life +large projects, only small number of files change in one commit, +and this 'collapsing' tends to trivially merge most of the paths +fairly quickly, leaving only a handful the real changes in non-zero +stages. + +To look at only non-zero stages, use `\--unmerged` flag: + +------------ +$ git-ls-files --unmerged +100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1 hello +100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2 hello +100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello +------------ + +The next step of merging is to merge these three versions of the +file, using 3-way merge. This is done by giving +`git-merge-one-file` command as one of the arguments to +`git-merge-index` command: + +------------ +$ git-merge-index git-merge-one-file hello +Auto-merging hello. +merge: warning: conflicts during merge +ERROR: Merge conflict in hello. +fatal: merge program failed +------------ + +`git-merge-one-file` script is called with parameters to +describe those three versions, and is responsible to leave the +merge results in the working tree. +It is a fairly straightforward shell script, and +eventually calls `merge` program from RCS suite to perform a +file-level 3-way merge. In this case, `merge` detects +conflicts, and the merge result with conflict marks is left in +the working tree.. This can be seen if you run `ls-files +--stage` again at this point: + +------------ +$ git-ls-files --stage +100644 7f8b141b65fdcee47321e399a2598a235a032422 0 example +100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1 hello +100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2 hello +100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello +------------ + +This is the state of the index file and the working file after +`git merge` returns control back to you, leaving the conflicting +merge for you to resolve. Notice that the path `hello` is still +unmerged, and what you see with `git diff` at this point is +differences since stage 2 (i.e. your version). + + +Publishing your work +-------------------- + +So, we can use somebody else's work from a remote repository, but +how can *you* prepare a repository to let other people pull from +it? + +Your do your real work in your working tree that has your +primary repository hanging under it as its `.git` subdirectory. +You *could* make that repository accessible remotely and ask +people to pull from it, but in practice that is not the way +things are usually done. A recommended way is to have a public +repository, make it reachable by other people, and when the +changes you made in your primary working tree are in good shape, +update the public repository from it. This is often called +'pushing'. + +[NOTE] +This public repository could further be mirrored, and that is +how git repositories at `kernel.org` are managed. + +Publishing the changes from your local (private) repository to +your remote (public) repository requires a write privilege on +the remote machine. You need to have an SSH account there to +run a single command, `git-receive-pack`. + +First, you need to create an empty repository on the remote +machine that will house your public repository. This empty +repository will be populated and be kept up-to-date by pushing +into it later. Obviously, this repository creation needs to be +done only once. + +[NOTE] +`git push` uses a pair of programs, +`git-send-pack` on your local machine, and `git-receive-pack` +on the remote machine. The communication between the two over +the network internally uses an SSH connection. + +Your private repository's git directory is usually `.git`, but +your public repository is often named after the project name, +i.e. `<project>.git`. Let's create such a public repository for +project `my-git`. After logging into the remote machine, create +an empty directory: + +------------ +$ mkdir my-git.git +------------ + +Then, make that directory into a git repository by running +`git init`, but this time, since its name is not the usual +`.git`, we do things slightly differently: + +------------ +$ GIT_DIR=my-git.git git-init +------------ + +Make sure this directory is available for others you want your +changes to be pulled by via the transport of your choice. Also +you need to make sure that you have the `git-receive-pack` +program on the `$PATH`. + +[NOTE] +Many installations of sshd do not invoke your shell as the login +shell when you directly run programs; what this means is that if +your login shell is `bash`, only `.bashrc` is read and not +`.bash_profile`. As a workaround, make sure `.bashrc` sets up +`$PATH` so that you can run `git-receive-pack` program. + +[NOTE] +If you plan to publish this repository to be accessed over http, +you should do `chmod +x my-git.git/hooks/post-update` at this +point. This makes sure that every time you push into this +repository, `git-update-server-info` is run. + +Your "public repository" is now ready to accept your changes. +Come back to the machine you have your private repository. From +there, run this command: + +------------ +$ git push <public-host>:/path/to/my-git.git master +------------ + +This synchronizes your public repository to match the named +branch head (i.e. `master` in this case) and objects reachable +from them in your current repository. + +As a real example, this is how I update my public git +repository. Kernel.org mirror network takes care of the +propagation to other publicly visible machines: + +------------ +$ git push master.kernel.org:/pub/scm/git/git.git/ +------------ + + +Packing your repository +----------------------- + +Earlier, we saw that one file under `.git/objects/??/` directory +is stored for each git object you create. This representation +is efficient to create atomically and safely, but +not so convenient to transport over the network. Since git objects are +immutable once they are created, there is a way to optimize the +storage by "packing them together". The command + +------------ +$ git repack +------------ + +will do it for you. If you followed the tutorial examples, you +would have accumulated about 17 objects in `.git/objects/??/` +directories by now. `git repack` tells you how many objects it +packed, and stores the packed file in `.git/objects/pack` +directory. + +[NOTE] +You will see two files, `pack-\*.pack` and `pack-\*.idx`, +in `.git/objects/pack` directory. They are closely related to +each other, and if you ever copy them by hand to a different +repository for whatever reason, you should make sure you copy +them together. The former holds all the data from the objects +in the pack, and the latter holds the index for random +access. + +If you are paranoid, running `git-verify-pack` command would +detect if you have a corrupt pack, but do not worry too much. +Our programs are always perfect ;-). + +Once you have packed objects, you do not need to leave the +unpacked objects that are contained in the pack file anymore. + +------------ +$ git prune-packed +------------ + +would remove them for you. + +You can try running `find .git/objects -type f` before and after +you run `git prune-packed` if you are curious. Also `git +count-objects` would tell you how many unpacked objects are in +your repository and how much space they are consuming. + +[NOTE] +`git pull` is slightly cumbersome for HTTP transport, as a +packed repository may contain relatively few objects in a +relatively large pack. If you expect many HTTP pulls from your +public repository you might want to repack & prune often, or +never. + +If you run `git repack` again at this point, it will say +"Nothing to pack". Once you continue your development and +accumulate the changes, running `git repack` again will create a +new pack, that contains objects created since you packed your +repository the last time. We recommend that you pack your project +soon after the initial import (unless you are starting your +project from scratch), and then run `git repack` every once in a +while, depending on how active your project is. + +When a repository is synchronized via `git push` and `git pull` +objects packed in the source repository are usually stored +unpacked in the destination, unless rsync transport is used. +While this allows you to use different packing strategies on +both ends, it also means you may need to repack both +repositories every once in a while. + + +Working with Others +------------------- + +Although git is a truly distributed system, it is often +convenient to organize your project with an informal hierarchy +of developers. Linux kernel development is run this way. There +is a nice illustration (page 17, "Merges to Mainline") in +link:http://tinyurl.com/a2jdg[Randy Dunlap's presentation]. + +It should be stressed that this hierarchy is purely *informal*. +There is nothing fundamental in git that enforces the "chain of +patch flow" this hierarchy implies. You do not have to pull +from only one remote repository. + +A recommended workflow for a "project lead" goes like this: + +1. Prepare your primary repository on your local machine. Your + work is done there. + +2. Prepare a public repository accessible to others. ++ +If other people are pulling from your repository over dumb +transport protocols (HTTP), you need to keep this repository +'dumb transport friendly'. After `git init`, +`$GIT_DIR/hooks/post-update` copied from the standard templates +would contain a call to `git-update-server-info` but the +`post-update` hook itself is disabled by default -- enable it +with `chmod +x post-update`. This makes sure `git-update-server-info` +keeps the necessary files up-to-date. + +3. Push into the public repository from your primary + repository. + +4. `git repack` the public repository. This establishes a big + pack that contains the initial set of objects as the + baseline, and possibly `git prune` if the transport + used for pulling from your repository supports packed + repositories. + +5. Keep working in your primary repository. Your changes + include modifications of your own, patches you receive via + e-mails, and merges resulting from pulling the "public" + repositories of your "subsystem maintainers". ++ +You can repack this private repository whenever you feel like. + +6. Push your changes to the public repository, and announce it + to the public. + +7. Every once in a while, "git repack" the public repository. + Go back to step 5. and continue working. + + +A recommended work cycle for a "subsystem maintainer" who works +on that project and has an own "public repository" goes like this: + +1. Prepare your work repository, by `git clone` the public + repository of the "project lead". The URL used for the + initial cloning is stored in the remote.origin.url + configuration variable. + +2. Prepare a public repository accessible to others, just like + the "project lead" person does. + +3. Copy over the packed files from "project lead" public + repository to your public repository, unless the "project + lead" repository lives on the same machine as yours. In the + latter case, you can use `objects/info/alternates` file to + point at the repository you are borrowing from. + +4. Push into the public repository from your primary + repository. Run `git repack`, and possibly `git prune` if the + transport used for pulling from your repository supports + packed repositories. + +5. Keep working in your primary repository. Your changes + include modifications of your own, patches you receive via + e-mails, and merges resulting from pulling the "public" + repositories of your "project lead" and possibly your + "sub-subsystem maintainers". ++ +You can repack this private repository whenever you feel +like. + +6. Push your changes to your public repository, and ask your + "project lead" and possibly your "sub-subsystem + maintainers" to pull from it. + +7. Every once in a while, `git repack` the public repository. + Go back to step 5. and continue working. + + +A recommended work cycle for an "individual developer" who does +not have a "public" repository is somewhat different. It goes +like this: + +1. Prepare your work repository, by `git clone` the public + repository of the "project lead" (or a "subsystem + maintainer", if you work on a subsystem). The URL used for + the initial cloning is stored in the remote.origin.url + configuration variable. + +2. Do your work in your repository on 'master' branch. + +3. Run `git fetch origin` from the public repository of your + upstream every once in a while. This does only the first + half of `git pull` but does not merge. The head of the + public repository is stored in `.git/refs/remotes/origin/master`. + +4. Use `git cherry origin` to see which ones of your patches + were accepted, and/or use `git rebase origin` to port your + unmerged changes forward to the updated upstream. + +5. Use `git format-patch origin` to prepare patches for e-mail + submission to your upstream and send it out. Go back to + step 2. and continue. + + +Working with Others, Shared Repository Style +-------------------------------------------- + +If you are coming from CVS background, the style of cooperation +suggested in the previous section may be new to you. You do not +have to worry. git supports "shared public repository" style of +cooperation you are probably more familiar with as well. + +See link:cvs-migration.html[git for CVS users] for the details. + +Bundling your work together +--------------------------- + +It is likely that you will be working on more than one thing at +a time. It is easy to manage those more-or-less independent tasks +using branches with git. + +We have already seen how branches work previously, +with "fun and work" example using two branches. The idea is the +same if there are more than two branches. Let's say you started +out from "master" head, and have some new code in the "master" +branch, and two independent fixes in the "commit-fix" and +"diff-fix" branches: + +------------ +$ git show-branch +! [commit-fix] Fix commit message normalization. + ! [diff-fix] Fix rename detection. + * [master] Release candidate #1 +--- + + [diff-fix] Fix rename detection. + + [diff-fix~1] Better common substring algorithm. ++ [commit-fix] Fix commit message normalization. + * [master] Release candidate #1 +++* [diff-fix~2] Pretty-print messages. +------------ + +Both fixes are tested well, and at this point, you want to merge +in both of them. You could merge in 'diff-fix' first and then +'commit-fix' next, like this: + +------------ +$ git merge 'Merge fix in diff-fix' master diff-fix +$ git merge 'Merge fix in commit-fix' master commit-fix +------------ + +Which would result in: + +------------ +$ git show-branch +! [commit-fix] Fix commit message normalization. + ! [diff-fix] Fix rename detection. + * [master] Merge fix in commit-fix +--- + - [master] Merge fix in commit-fix ++ * [commit-fix] Fix commit message normalization. + - [master~1] Merge fix in diff-fix + +* [diff-fix] Fix rename detection. + +* [diff-fix~1] Better common substring algorithm. + * [master~2] Release candidate #1 +++* [master~3] Pretty-print messages. +------------ + +However, there is no particular reason to merge in one branch +first and the other next, when what you have are a set of truly +independent changes (if the order mattered, then they are not +independent by definition). You could instead merge those two +branches into the current branch at once. First let's undo what +we just did and start over. We would want to get the master +branch before these two merges by resetting it to 'master~2': + +------------ +$ git reset --hard master~2 +------------ + +You can make sure 'git show-branch' matches the state before +those two 'git merge' you just did. Then, instead of running +two 'git merge' commands in a row, you would merge these two +branch heads (this is known as 'making an Octopus'): + +------------ +$ git merge commit-fix diff-fix +$ git show-branch +! [commit-fix] Fix commit message normalization. + ! [diff-fix] Fix rename detection. + * [master] Octopus merge of branches 'diff-fix' and 'commit-fix' +--- + - [master] Octopus merge of branches 'diff-fix' and 'commit-fix' ++ * [commit-fix] Fix commit message normalization. + +* [diff-fix] Fix rename detection. + +* [diff-fix~1] Better common substring algorithm. + * [master~1] Release candidate #1 +++* [master~2] Pretty-print messages. +------------ + +Note that you should not do Octopus because you can. An octopus +is a valid thing to do and often makes it easier to view the +commit history if you are merging more than two independent +changes at the same time. However, if you have merge conflicts +with any of the branches you are merging in and need to hand +resolve, that is an indication that the development happened in +those branches were not independent after all, and you should +merge two at a time, documenting how you resolved the conflicts, +and the reason why you preferred changes made in one side over +the other. Otherwise it would make the project history harder +to follow, not easier. + +[ to be continued.. cvsimports ] diff --git a/Documentation/cvs-migration.txt b/Documentation/cvs-migration.txt new file mode 100644 index 0000000000..764cc560b4 --- /dev/null +++ b/Documentation/cvs-migration.txt @@ -0,0 +1,171 @@ +git for CVS users +================= + +Git differs from CVS in that every working tree contains a repository with +a full copy of the project history, and no repository is inherently more +important than any other. However, you can emulate the CVS model by +designating a single shared repository which people can synchronize with; +this document explains how to do that. + +Some basic familiarity with git is required. This +link:tutorial.html[tutorial introduction to git] should be sufficient. + +Developing against a shared repository +-------------------------------------- + +Suppose a shared repository is set up in /pub/repo.git on the host +foo.com. Then as an individual committer you can clone the shared +repository over ssh with: + +------------------------------------------------ +$ git clone foo.com:/pub/repo.git/ my-project +$ cd my-project +------------------------------------------------ + +and hack away. The equivalent of `cvs update` is + +------------------------------------------------ +$ git pull origin +------------------------------------------------ + +which merges in any work that others might have done since the clone +operation. If there are uncommitted changes in your working tree, commit +them first before running git pull. + +[NOTE] +================================ +The `pull` command knows where to get updates from because of certain +configuration variables that were set by the first `git clone` +command; see `git config -l` and the gitlink:git-config[1] man +page for details. +================================ + +You can update the shared repository with your changes by first committing +your changes, and then using the gitlink:git-push[1] command: + +------------------------------------------------ +$ git push origin master +------------------------------------------------ + +to "push" those commits to the shared repository. If someone else has +updated the repository more recently, `git push`, like `cvs commit`, will +complain, in which case you must pull any changes before attempting the +push again. + +In the `git push` command above we specify the name of the remote branch +to update (`master`). If we leave that out, `git push` tries to update +any branches in the remote repository that have the same name as a branch +in the local repository. So the last `push` can be done with either of: + +------------ +$ git push origin +$ git push foo.com:/pub/project.git/ +------------ + +as long as the shared repository does not have any branches +other than `master`. + +Setting Up a Shared Repository +------------------------------ + +We assume you have already created a git repository for your project, +possibly created from scratch or from a tarball (see the +link:tutorial.html[tutorial]), or imported from an already existing CVS +repository (see the next section). + +Assume your existing repo is at /home/alice/myproject. Create a new "bare" +repository (a repository without a working tree) and fetch your project into +it: + +------------------------------------------------ +$ mkdir /pub/my-repo.git +$ cd /pub/my-repo.git +$ git --bare init --shared +$ git --bare fetch /home/alice/myproject master:master +------------------------------------------------ + +Next, give every team member read/write access to this repository. One +easy way to do this is to give all the team members ssh access to the +machine where the repository is hosted. If you don't want to give them a +full shell on the machine, there is a restricted shell which only allows +users to do git pushes and pulls; see gitlink:git-shell[1]. + +Put all the committers in the same group, and make the repository +writable by that group: + +------------------------------------------------ +$ chgrp -R $group /pub/my-repo.git +------------------------------------------------ + +Make sure committers have a umask of at most 027, so that the directories +they create are writable and searchable by other group members. + +Importing a CVS archive +----------------------- + +First, install version 2.1 or higher of cvsps from +link:http://www.cobite.com/cvsps/[http://www.cobite.com/cvsps/] and make +sure it is in your path. Then cd to a checked out CVS working directory +of the project you are interested in and run gitlink:git-cvsimport[1]: + +------------------------------------------- +$ git cvsimport -C <destination> +------------------------------------------- + +This puts a git archive of the named CVS module in the directory +<destination>, which will be created if necessary. + +The import checks out from CVS every revision of every file. Reportedly +cvsimport can average some twenty revisions per second, so for a +medium-sized project this should not take more than a couple of minutes. +Larger projects or remote repositories may take longer. + +The main trunk is stored in the git branch named `origin`, and additional +CVS branches are stored in git branches with the same names. The most +recent version of the main trunk is also left checked out on the `master` +branch, so you can start adding your own changes right away. + +The import is incremental, so if you call it again next month it will +fetch any CVS updates that have been made in the meantime. For this to +work, you must not modify the imported branches; instead, create new +branches for your own changes, and merge in the imported branches as +necessary. + +Advanced Shared Repository Management +------------------------------------- + +Git allows you to specify scripts called "hooks" to be run at certain +points. You can use these, for example, to send all commits to the shared +repository to a mailing list. See link:hooks.html[Hooks used by git]. + +You can enforce finer grained permissions using update hooks. See +link:howto/update-hook-example.txt[Controlling access to branches using +update hooks]. + +Providing CVS Access to a git Repository +---------------------------------------- + +It is also possible to provide true CVS access to a git repository, so +that developers can still use CVS; see gitlink:git-cvsserver[1] for +details. + +Alternative Development Models +------------------------------ + +CVS users are accustomed to giving a group of developers commit access to +a common repository. As we've seen, this is also possible with git. +However, the distributed nature of git allows other development models, +and you may want to first consider whether one of them might be a better +fit for your project. + +For example, you can choose a single person to maintain the project's +primary public repository. Other developers then clone this repository +and each work in their own clone. When they have a series of changes that +they're happy with, they ask the maintainer to pull from the branch +containing the changes. The maintainer reviews their changes and pulls +them into the primary repository, which other developers pull from as +necessary to stay coordinated. The Linux kernel and other projects use +variants of this model. + +With a small group, developers may just pull changes from each other's +repositories without the need for a central maintainer. diff --git a/Documentation/diff-format.txt b/Documentation/diff-format.txt new file mode 100644 index 0000000000..378e72f38f --- /dev/null +++ b/Documentation/diff-format.txt @@ -0,0 +1,214 @@ +The output format from "git-diff-index", "git-diff-tree" and +"git-diff-files" are very similar. + +These commands all compare two sets of things; what is +compared differs: + +git-diff-index <tree-ish>:: + compares the <tree-ish> and the files on the filesystem. + +git-diff-index --cached <tree-ish>:: + compares the <tree-ish> and the index. + +git-diff-tree [-r] <tree-ish-1> <tree-ish-2> [<pattern>...]:: + compares the trees named by the two arguments. + +git-diff-files [<pattern>...]:: + compares the index and the files on the filesystem. + + +An output line is formatted this way: + +------------------------------------------------ +in-place edit :100644 100644 bcd1234... 0123456... M file0 +copy-edit :100644 100644 abcd123... 1234567... C68 file1 file2 +rename-edit :100644 100644 abcd123... 1234567... R86 file1 file3 +create :000000 100644 0000000... 1234567... A file4 +delete :100644 000000 1234567... 0000000... D file5 +unmerged :000000 000000 0000000... 0000000... U file6 +------------------------------------------------ + +That is, from the left to the right: + +. a colon. +. mode for "src"; 000000 if creation or unmerged. +. a space. +. mode for "dst"; 000000 if deletion or unmerged. +. a space. +. sha1 for "src"; 0\{40\} if creation or unmerged. +. a space. +. sha1 for "dst"; 0\{40\} if creation, unmerged or "look at work tree". +. a space. +. status, followed by optional "score" number. +. a tab or a NUL when '-z' option is used. +. path for "src" +. a tab or a NUL when '-z' option is used; only exists for C or R. +. path for "dst"; only exists for C or R. +. an LF or a NUL when '-z' option is used, to terminate the record. + +<sha1> is shown as all 0's if a file is new on the filesystem +and it is out of sync with the index. + +Example: + +------------------------------------------------ +:100644 100644 5be4a4...... 000000...... M file.c +------------------------------------------------ + +When `-z` option is not used, TAB, LF, and backslash characters +in pathnames are represented as `\t`, `\n`, and `\\`, +respectively. + + +Generating patches with -p +-------------------------- + +When "git-diff-index", "git-diff-tree", or "git-diff-files" are run +with a '-p' option, they do not produce the output described above; +instead they produce a patch file. You can customize the creation +of such patches via the GIT_EXTERNAL_DIFF and the GIT_DIFF_OPTS +environment variables. + +What the -p option produces is slightly different from the traditional +diff format. + +1. It is preceded with a "git diff" header, that looks like + this: + + diff --git a/file1 b/file2 ++ +The `a/` and `b/` filenames are the same unless rename/copy is +involved. Especially, even for a creation or a deletion, +`/dev/null` is _not_ used in place of `a/` or `b/` filenames. ++ +When rename/copy is involved, `file1` and `file2` show the +name of the source file of the rename/copy and the name of +the file that rename/copy produces, respectively. + +2. It is followed by one or more extended header lines: + + old mode <mode> + new mode <mode> + deleted file mode <mode> + new file mode <mode> + copy from <path> + copy to <path> + rename from <path> + rename to <path> + similarity index <number> + dissimilarity index <number> + index <hash>..<hash> <mode> + +3. TAB, LF, double quote and backslash characters in pathnames + are represented as `\t`, `\n`, `\"` and `\\`, respectively. + If there is need for such substitution then the whole + pathname is put in double quotes. + + +combined diff format +-------------------- + +git-diff-tree and git-diff-files can take '-c' or '--cc' option +to produce 'combined diff', which looks like this: + +------------ +diff --combined describe.c +index fabadb8,cc95eb0..4866510 +--- a/describe.c ++++ b/describe.c +@@@ -98,20 -98,12 +98,20 @@@ + return (a_date > b_date) ? -1 : (a_date == b_date) ? 0 : 1; + } + +- static void describe(char *arg) + -static void describe(struct commit *cmit, int last_one) +++static void describe(char *arg, int last_one) + { + + unsigned char sha1[20]; + + struct commit *cmit; + struct commit_list *list; + static int initialized = 0; + struct commit_name *n; + + + if (get_sha1(arg, sha1) < 0) + + usage(describe_usage); + + cmit = lookup_commit_reference(sha1); + + if (!cmit) + + usage(describe_usage); + + + if (!initialized) { + initialized = 1; + for_each_ref(get_name); +------------ + +1. It is preceded with a "git diff" header, that looks like + this (when '-c' option is used): + + diff --combined file ++ +or like this (when '--cc' option is used): + + diff --c file + +2. It is followed by one or more extended header lines + (this example shows a merge with two parents): + + index <hash>,<hash>..<hash> + mode <mode>,<mode>..<mode> + new file mode <mode> + deleted file mode <mode>,<mode> ++ +The `mode <mode>,<mode>..<mode>` line appears only if at least one of +the <mode> is different from the rest. Extended headers with +information about detected contents movement (renames and +copying detection) are designed to work with diff of two +<tree-ish> and are not used by combined diff format. + +3. It is followed by two-line from-file/to-file header + + --- a/file + +++ b/file ++ +Similar to two-line header for traditional 'unified' diff +format, `/dev/null` is used to signal created or deleted +files. + +4. Chunk header format is modified to prevent people from + accidentally feeding it to `patch -p1`. Combined diff format + was created for review of merge commit changes, and was not + meant for apply. The change is similar to the change in the + extended 'index' header: + + @@@ <from-file-range> <from-file-range> <to-file-range> @@@ ++ +There are (number of parents + 1) `@` characters in the chunk +header for combined diff format. + +Unlike the traditional 'unified' diff format, which shows two +files A and B with a single column that has `-` (minus -- +appears in A but removed in B), `+` (plus -- missing in A but +added to B), or `" "` (space -- unchanged) prefix, this format +compares two or more files file1, file2,... with one file X, and +shows how X differs from each of fileN. One column for each of +fileN is prepended to the output line to note how X's line is +different from it. + +A `-` character in the column N means that the line appears in +fileN but it does not appear in the result. A `+` character +in the column N means that the line appears in the last file, +and fileN does not have that line (in other words, the line was +added, from the point of view of that parent). + +In the above example output, the function signature was changed +from both files (hence two `-` removals from both file1 and +file2, plus `++` to mean one line that was added does not appear +in either file1 nor file2). Also two other lines are the same +from file1 but do not appear in file2 (hence prefixed with ` +`). + +When shown by `git diff-tree -c`, it compares the parents of a +merge commit with the merge result (i.e. file1..fileN are the +parents). When shown by `git diff-files -c`, it compares the +two unresolved merge parents with the working tree file +(i.e. file1 is stage 2 aka "our version", file2 is stage 3 aka +"their version"). + diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt new file mode 100644 index 0000000000..019a39f2bf --- /dev/null +++ b/Documentation/diff-options.txt @@ -0,0 +1,160 @@ +-p:: + Generate patch (see section on generating patches) + +-u:: + Synonym for "-p". + +--raw:: + Generate the raw format. + +--patch-with-raw:: + Synonym for "-p --raw". + +--stat[=width[,name-width]]:: + Generate a diffstat. You can override the default + output width for 80-column terminal by "--stat=width". + The width of the filename part can be controlled by + giving another width to it separated by a comma. + +--numstat:: + Similar to \--stat, but shows number of added and + deleted lines in decimal notation and pathname without + abbreviation, to make it more machine friendly. For + binary files, outputs two `-` instead of saying + `0 0`. + +--shortstat:: + Output only the last line of the --stat format containing total + number of modified files, as well as number of added and deleted + lines. + +--summary:: + Output a condensed summary of extended header information + such as creations, renames and mode changes. + +--patch-with-stat:: + Synonym for "-p --stat". + +-z:: + \0 line termination on output + +--name-only:: + Show only names of changed files. + +--name-status:: + Show only names and status of changed files. + +--color:: + Show colored diff. + +--no-color:: + Turn off colored diff, even when the configuration file + gives the default to color output. + +--color-words:: + Show colored word diff, i.e. color words which have changed. + +--no-renames:: + Turn off rename detection, even when the configuration + file gives the default to do so. + +--check:: + Warn if changes introduce trailing whitespace + or an indent that uses a space before a tab. + +--full-index:: + Instead of the first handful characters, show full + object name of pre- and post-image blob on the "index" + line when generating a patch format output. + +--binary:: + In addition to --full-index, output "binary diff" that + can be applied with "git apply". + +--abbrev[=<n>]:: + Instead of showing the full 40-byte hexadecimal object + name in diff-raw format output and diff-tree header + lines, show only handful hexdigits prefix. This is + independent of --full-index option above, which controls + the diff-patch output format. Non default number of + digits can be specified with --abbrev=<n>. + +-B:: + Break complete rewrite changes into pairs of delete and create. + +-M:: + Detect renames. + +-C:: + Detect copies as well as renames. + +--diff-filter=[ACDMRTUXB*]:: + Select only files that are Added (`A`), Copied (`C`), + Deleted (`D`), Modified (`M`), Renamed (`R`), have their + type (mode) changed (`T`), are Unmerged (`U`), are + Unknown (`X`), or have had their pairing Broken (`B`). + Any combination of the filter characters may be used. + When `*` (All-or-none) is added to the combination, all + paths are selected if there is any file that matches + other criteria in the comparison; if there is no file + that matches other criteria, nothing is selected. + +--find-copies-harder:: + For performance reasons, by default, -C option finds copies only + if the original file of the copy was modified in the same + changeset. This flag makes the command + inspect unmodified files as candidates for the source of + copy. This is a very expensive operation for large + projects, so use it with caution. + +-l<num>:: + -M and -C options require O(n^2) processing time where n + is the number of potential rename/copy targets. This + option prevents rename/copy detection from running if + the number of rename/copy targets exceeds the specified + number. + +-S<string>:: + Look for differences that contain the change in <string>. + +--pickaxe-all:: + When -S finds a change, show all the changes in that + changeset, not just the files that contain the change + in <string>. + +--pickaxe-regex:: + Make the <string> not a plain string but an extended POSIX + regex to match. + +-O<orderfile>:: + Output the patch in the order specified in the + <orderfile>, which has one shell glob pattern per line. + +-R:: + Swap two inputs; that is, show differences from index or + on-disk file to tree contents. + +--text:: + Treat all files as text. + +-a:: + Shorthand for "--text". + +--ignore-space-change:: + Ignore changes in amount of white space. This ignores white + space at line end, and consider all other sequences of one or + more white space characters to be equivalent. + +-b:: + Shorthand for "--ignore-space-change". + +--ignore-all-space:: + Ignore white space when comparing lines. This ignores + difference even if one line has white space where the other + line has none. + +-w:: + Shorthand for "--ignore-all-space". + +For more detailed explanation on these common options, see also +link:diffcore.html[diffcore documentation]. diff --git a/Documentation/diffcore.txt b/Documentation/diffcore.txt new file mode 100644 index 0000000000..cb4e562004 --- /dev/null +++ b/Documentation/diffcore.txt @@ -0,0 +1,275 @@ +Tweaking diff output +==================== +June 2005 + + +Introduction +------------ + +The diff commands git-diff-index, git-diff-files, git-diff-tree, and +git-diff-stages can be told to manipulate differences they find in +unconventional ways before showing diff(1) output. The manipulation +is collectively called "diffcore transformation". This short note +describes what they are and how to use them to produce diff outputs +that are easier to understand than the conventional kind. + + +The chain of operation +---------------------- + +The git-diff-* family works by first comparing two sets of +files: + + - git-diff-index compares contents of a "tree" object and the + working directory (when '\--cached' flag is not used) or a + "tree" object and the index file (when '\--cached' flag is + used); + + - git-diff-files compares contents of the index file and the + working directory; + + - git-diff-tree compares contents of two "tree" objects; + + - git-diff-stages compares contents of blobs at two stages in an + unmerged index file. + +In all of these cases, the commands themselves compare +corresponding paths in the two sets of files. The result of +comparison is passed from these commands to what is internally +called "diffcore", in a format similar to what is output when +the -p option is not used. E.g. + +------------------------------------------------ +in-place edit :100644 100644 bcd1234... 0123456... M file0 +create :000000 100644 0000000... 1234567... A file4 +delete :100644 000000 1234567... 0000000... D file5 +unmerged :000000 000000 0000000... 0000000... U file6 +------------------------------------------------ + +The diffcore mechanism is fed a list of such comparison results +(each of which is called "filepair", although at this point each +of them talks about a single file), and transforms such a list +into another list. There are currently 6 such transformations: + +- diffcore-pathspec +- diffcore-break +- diffcore-rename +- diffcore-merge-broken +- diffcore-pickaxe +- diffcore-order + +These are applied in sequence. The set of filepairs git-diff-\* +commands find are used as the input to diffcore-pathspec, and +the output from diffcore-pathspec is used as the input to the +next transformation. The final result is then passed to the +output routine and generates either diff-raw format (see Output +format sections of the manual for git-diff-\* commands) or +diff-patch format. + + +diffcore-pathspec: For Ignoring Files Outside Our Consideration +--------------------------------------------------------------- + +The first transformation in the chain is diffcore-pathspec, and +is controlled by giving the pathname parameters to the +git-diff-* commands on the command line. The pathspec is used +to limit the world diff operates in. It removes the filepairs +outside the specified set of pathnames. E.g. If the input set +of filepairs included: + +------------------------------------------------ +:100644 100644 bcd1234... 0123456... M junkfile +------------------------------------------------ + +but the command invocation was "git-diff-files myfile", then the +junkfile entry would be removed from the list because only "myfile" +is under consideration. + +Implementation note. For performance reasons, git-diff-tree +uses the pathname parameters on the command line to cull set of +filepairs it feeds the diffcore mechanism itself, and does not +use diffcore-pathspec, but the end result is the same. + + +diffcore-break: For Splitting Up "Complete Rewrites" +---------------------------------------------------- + +The second transformation in the chain is diffcore-break, and is +controlled by the -B option to the git-diff-* commands. This is +used to detect a filepair that represents "complete rewrite" and +break such filepair into two filepairs that represent delete and +create. E.g. If the input contained this filepair: + +------------------------------------------------ +:100644 100644 bcd1234... 0123456... M file0 +------------------------------------------------ + +and if it detects that the file "file0" is completely rewritten, +it changes it to: + +------------------------------------------------ +:100644 000000 bcd1234... 0000000... D file0 +:000000 100644 0000000... 0123456... A file0 +------------------------------------------------ + +For the purpose of breaking a filepair, diffcore-break examines +the extent of changes between the contents of the files before +and after modification (i.e. the contents that have "bcd1234..." +and "0123456..." as their SHA1 content ID, in the above +example). The amount of deletion of original contents and +insertion of new material are added together, and if it exceeds +the "break score", the filepair is broken into two. The break +score defaults to 50% of the size of the smaller of the original +and the result (i.e. if the edit shrinks the file, the size of +the result is used; if the edit lengthens the file, the size of +the original is used), and can be customized by giving a number +after "-B" option (e.g. "-B75" to tell it to use 75%). + + +diffcore-rename: For Detection Renames and Copies +------------------------------------------------- + +This transformation is used to detect renames and copies, and is +controlled by the -M option (to detect renames) and the -C option +(to detect copies as well) to the git-diff-* commands. If the +input contained these filepairs: + +------------------------------------------------ +:100644 000000 0123456... 0000000... D fileX +:000000 100644 0000000... 0123456... A file0 +------------------------------------------------ + +and the contents of the deleted file fileX is similar enough to +the contents of the created file file0, then rename detection +merges these filepairs and creates: + +------------------------------------------------ +:100644 100644 0123456... 0123456... R100 fileX file0 +------------------------------------------------ + +When the "-C" option is used, the original contents of modified files, +and deleted files (and also unmodified files, if the +"\--find-copies-harder" option is used) are considered as candidates +of the source files in rename/copy operation. If the input were like +these filepairs, that talk about a modified file fileY and a newly +created file file0: + +------------------------------------------------ +:100644 100644 0123456... 1234567... M fileY +:000000 100644 0000000... bcd3456... A file0 +------------------------------------------------ + +the original contents of fileY and the resulting contents of +file0 are compared, and if they are similar enough, they are +changed to: + +------------------------------------------------ +:100644 100644 0123456... 1234567... M fileY +:100644 100644 0123456... bcd3456... C100 fileY file0 +------------------------------------------------ + +In both rename and copy detection, the same "extent of changes" +algorithm used in diffcore-break is used to determine if two +files are "similar enough", and can be customized to use +a similarity score different from the default of 50% by giving a +number after the "-M" or "-C" option (e.g. "-M8" to tell it to use +8/10 = 80%). + +Note. When the "-C" option is used with `\--find-copies-harder` +option, git-diff-\* commands feed unmodified filepairs to +diffcore mechanism as well as modified ones. This lets the copy +detector consider unmodified files as copy source candidates at +the expense of making it slower. Without `\--find-copies-harder`, +git-diff-\* commands can detect copies only if the file that was +copied happened to have been modified in the same changeset. + + +diffcore-merge-broken: For Putting "Complete Rewrites" Back Together +-------------------------------------------------------------------- + +This transformation is used to merge filepairs broken by +diffcore-break, and not transformed into rename/copy by +diffcore-rename, back into a single modification. This always +runs when diffcore-break is used. + +For the purpose of merging broken filepairs back, it uses a +different "extent of changes" computation from the ones used by +diffcore-break and diffcore-rename. It counts only the deletion +from the original, and does not count insertion. If you removed +only 10 lines from a 100-line document, even if you added 910 +new lines to make a new 1000-line document, you did not do a +complete rewrite. diffcore-break breaks such a case in order to +help diffcore-rename to consider such filepairs as candidate of +rename/copy detection, but if filepairs broken that way were not +matched with other filepairs to create rename/copy, then this +transformation merges them back into the original +"modification". + +The "extent of changes" parameter can be tweaked from the +default 80% (that is, unless more than 80% of the original +material is deleted, the broken pairs are merged back into a +single modification) by giving a second number to -B option, +like these: + +* -B50/60 (give 50% "break score" to diffcore-break, use 60% + for diffcore-merge-broken). + +* -B/60 (the same as above, since diffcore-break defaults to 50%). + +Note that earlier implementation left a broken pair as a separate +creation and deletion patches. This was an unnecessary hack and +the latest implementation always merges all the broken pairs +back into modifications, but the resulting patch output is +formatted differently for easier review in case of such +a complete rewrite by showing the entire contents of old version +prefixed with '-', followed by the entire contents of new +version prefixed with '+'. + + +diffcore-pickaxe: For Detecting Addition/Deletion of Specified String +--------------------------------------------------------------------- + +This transformation is used to find filepairs that represent +changes that touch a specified string, and is controlled by the +-S option and the `\--pickaxe-all` option to the git-diff-* +commands. + +When diffcore-pickaxe is in use, it checks if there are +filepairs whose "original" side has the specified string and +whose "result" side does not. Such a filepair represents "the +string appeared in this changeset". It also checks for the +opposite case that loses the specified string. + +When `\--pickaxe-all` is not in effect, diffcore-pickaxe leaves +only such filepairs that touch the specified string in its +output. When `\--pickaxe-all` is used, diffcore-pickaxe leaves all +filepairs intact if there is such a filepair, or makes the +output empty otherwise. The latter behaviour is designed to +make reviewing of the changes in the context of the whole +changeset easier. + + +diffcore-order: For Sorting the Output Based on Filenames +--------------------------------------------------------- + +This is used to reorder the filepairs according to the user's +(or project's) taste, and is controlled by the -O option to the +git-diff-* commands. + +This takes a text file each of whose lines is a shell glob +pattern. Filepairs that match a glob pattern on an earlier line +in the file are output before ones that match a later line, and +filepairs that do not match any glob pattern are output last. + +As an example, a typical orderfile for the core git probably +would look like this: + +------------------------------------------------ +README +Makefile +Documentation +*.h +*.c +t +------------------------------------------------ + diff --git a/Documentation/docbook-xsl.css b/Documentation/docbook-xsl.css new file mode 100644 index 0000000000..8821e305dd --- /dev/null +++ b/Documentation/docbook-xsl.css @@ -0,0 +1,286 @@ +/*
+ CSS stylesheet for XHTML produced by DocBook XSL stylesheets.
+ Tested with XSL stylesheets 1.61.2, 1.67.2
+*/
+
+span.strong {
+ font-weight: bold;
+}
+
+body blockquote {
+ margin-top: .75em;
+ line-height: 1.5;
+ margin-bottom: .75em;
+}
+
+html body {
+ margin: 1em 5% 1em 5%;
+ line-height: 1.2;
+}
+
+body div {
+ margin: 0;
+}
+
+h1, h2, h3, h4, h5, h6,
+div.toc p b,
+div.list-of-figures p b,
+div.list-of-tables p b,
+div.abstract p.title
+{
+ color: #527bbd;
+ font-family: tahoma, verdana, sans-serif;
+}
+
+div.toc p:first-child,
+div.list-of-figures p:first-child,
+div.list-of-tables p:first-child,
+div.example p.title
+{
+ margin-bottom: 0.2em;
+}
+
+body h1 {
+ margin: .0em 0 0 -4%;
+ line-height: 1.3;
+ border-bottom: 2px solid silver;
+}
+
+body h2 {
+ margin: 0.5em 0 0 -4%;
+ line-height: 1.3;
+ border-bottom: 2px solid silver;
+}
+
+body h3 {
+ margin: .8em 0 0 -3%;
+ line-height: 1.3;
+}
+
+body h4 {
+ margin: .8em 0 0 -3%;
+ line-height: 1.3;
+}
+
+body h5 {
+ margin: .8em 0 0 -2%;
+ line-height: 1.3;
+}
+
+body h6 {
+ margin: .8em 0 0 -1%;
+ line-height: 1.3;
+}
+
+body hr {
+ border: none; /* Broken on IE6 */
+}
+div.footnotes hr {
+ border: 1px solid silver;
+}
+
+div.navheader th, div.navheader td, div.navfooter td {
+ font-family: sans-serif;
+ font-size: 0.9em;
+ font-weight: bold;
+ color: #527bbd;
+}
+div.navheader img, div.navfooter img {
+ border-style: none;
+}
+div.navheader a, div.navfooter a {
+ font-weight: normal;
+}
+div.navfooter hr {
+ border: 1px solid silver;
+}
+
+body td {
+ line-height: 1.2
+}
+
+body th {
+ line-height: 1.2;
+}
+
+ol {
+ line-height: 1.2;
+}
+
+ul, body dir, body menu {
+ line-height: 1.2;
+}
+
+html {
+ margin: 0;
+ padding: 0;
+}
+
+body h1, body h2, body h3, body h4, body h5, body h6 {
+ margin-left: 0
+}
+
+body pre {
+ margin: 0.5em 10% 0.5em 1em;
+ line-height: 1.0;
+ color: navy;
+}
+
+tt.literal, code.literal {
+ color: navy;
+}
+
+div.literallayout p {
+ padding: 0em;
+ margin: 0em;
+}
+
+div.literallayout {
+ font-family: monospace;
+# margin: 0.5em 10% 0.5em 1em;
+ margin: 0em;
+ color: navy;
+ border: 1px solid silver;
+ background: #f4f4f4;
+ padding: 0.5em;
+}
+
+.programlisting, .screen {
+ border: 1px solid silver;
+ background: #f4f4f4;
+ margin: 0.5em 10% 0.5em 0;
+ padding: 0.5em 1em;
+}
+
+div.sidebar {
+ background: #ffffee;
+ margin: 1.0em 10% 0.5em 0;
+ padding: 0.5em 1em;
+ border: 1px solid silver;
+}
+div.sidebar * { padding: 0; }
+div.sidebar div { margin: 0; }
+div.sidebar p.title {
+ font-family: sans-serif;
+ margin-top: 0.5em;
+ margin-bottom: 0.2em;
+}
+
+div.bibliomixed {
+ margin: 0.5em 5% 0.5em 1em;
+}
+
+div.glossary dt {
+ font-weight: bold;
+}
+div.glossary dd p {
+ margin-top: 0.2em;
+}
+
+dl {
+ margin: .8em 0;
+ line-height: 1.2;
+}
+
+dt {
+ margin-top: 0.5em;
+}
+
+dt span.term {
+ font-style: italic;
+}
+
+div.variablelist dd p {
+ margin-top: 0;
+}
+
+div.itemizedlist li, div.orderedlist li {
+ margin-left: -0.8em;
+ margin-top: 0.5em;
+}
+
+ul, ol {
+ list-style-position: outside;
+}
+
+div.sidebar ul, div.sidebar ol {
+ margin-left: 2.8em;
+}
+
+div.itemizedlist p.title,
+div.orderedlist p.title,
+div.variablelist p.title
+{
+ margin-bottom: -0.8em;
+}
+
+div.revhistory table {
+ border-collapse: collapse;
+ border: none;
+}
+div.revhistory th {
+ border: none;
+ color: #527bbd;
+ font-family: tahoma, verdana, sans-serif;
+}
+div.revhistory td {
+ border: 1px solid silver;
+}
+
+/* Keep TOC and index lines close together. */
+div.toc dl, div.toc dt,
+div.list-of-figures dl, div.list-of-figures dt,
+div.list-of-tables dl, div.list-of-tables dt,
+div.indexdiv dl, div.indexdiv dt
+{
+ line-height: normal;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+/*
+ Table styling does not work because of overriding attributes in
+ generated HTML.
+*/
+div.table table,
+div.informaltable table
+{
+ margin-left: 0;
+ margin-right: 5%;
+ margin-bottom: 0.8em;
+}
+div.informaltable table
+{
+ margin-top: 0.4em
+}
+div.table thead,
+div.table tfoot,
+div.table tbody,
+div.informaltable thead,
+div.informaltable tfoot,
+div.informaltable tbody
+{
+ /* No effect in IE6. */
+ border-top: 2px solid #527bbd;
+ border-bottom: 2px solid #527bbd;
+}
+div.table thead, div.table tfoot,
+div.informaltable thead, div.informaltable tfoot
+{
+ font-weight: bold;
+}
+
+div.mediaobject img {
+ border: 1px solid silver;
+ margin-bottom: 0.8em;
+}
+div.figure p.title,
+div.table p.title
+{
+ margin-top: 1em;
+ margin-bottom: 0.4em;
+}
+
+@media print {
+ div.navheader, div.navfooter { display: none; }
+}
diff --git a/Documentation/everyday.txt b/Documentation/everyday.txt new file mode 100644 index 0000000000..08c61b1f1a --- /dev/null +++ b/Documentation/everyday.txt @@ -0,0 +1,465 @@ +Everyday GIT With 20 Commands Or So +=================================== + +<<Basic Repository>> commands are needed by people who have a +repository --- that is everybody, because every working tree of +git is a repository. + +In addition, <<Individual Developer (Standalone)>> commands are +essential for anybody who makes a commit, even for somebody who +works alone. + +If you work with other people, you will need commands listed in +the <<Individual Developer (Participant)>> section as well. + +People who play the <<Integrator>> role need to learn some more +commands in addition to the above. + +<<Repository Administration>> commands are for system +administrators who are responsible for the care and feeding +of git repositories. + + +Basic Repository[[Basic Repository]] +------------------------------------ + +Everybody uses these commands to maintain git repositories. + + * gitlink:git-init[1] or gitlink:git-clone[1] to create a + new repository. + + * gitlink:git-fsck[1] to check the repository for errors. + + * gitlink:git-prune[1] to remove unused objects in the repository. + + * gitlink:git-repack[1] to pack loose objects for efficiency. + + * gitlink:git-gc[1] to do common housekeeping tasks such as + repack and prune. + +Examples +~~~~~~~~ + +Check health and remove cruft.:: ++ +------------ +$ git fsck <1> +$ git count-objects <2> +$ git repack <3> +$ git gc <4> +------------ ++ +<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 +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> it is easier to use `git gc` than individual housekeeping commands +such as `prune` and `repack`. This runs `repack -a -d`. + +Repack a small project into single pack.:: ++ +------------ +$ git repack -a -d <1> +$ git prune +------------ ++ +<1> pack all the objects reachable from the refs into one pack, +then remove the other packs. + + +Individual Developer (Standalone)[[Individual Developer (Standalone)]] +---------------------------------------------------------------------- + +A standalone individual developer does not exchange patches with +other people, and works alone in a single repository, using the +following commands. + + * gitlink:git-show-branch[1] to see where you are. + + * gitlink:git-log[1] to see what happened. + + * gitlink:git-checkout[1] and gitlink:git-branch[1] to switch + branches. + + * gitlink:git-add[1] to manage the index file. + + * gitlink:git-diff[1] and gitlink:git-status[1] to see what + you are in the middle of doing. + + * gitlink:git-commit[1] to advance the current branch. + + * gitlink:git-reset[1] and gitlink:git-checkout[1] (with + pathname parameters) to undo changes. + + * gitlink:git-merge[1] to merge between local branches. + + * gitlink:git-rebase[1] to maintain topic branches. + + * gitlink:git-tag[1] to mark known point. + +Examples +~~~~~~~~ + +Use a tarball as a starting point for a new repository.:: ++ +------------ +$ tar zxf frotz.tar.gz +$ cd frotz +$ git-init +$ git add . <1> +$ git commit -m 'import of frotz source tree.' +$ git tag v2.43 <2> +------------ ++ +<1> add everything under the current directory. +<2> make a lightweight, unannotated tag. + +Create a topic branch and develop.:: ++ +------------ +$ git checkout -b alsa-audio <1> +$ edit/compile/test +$ git checkout -- curses/ux_audio_oss.c <2> +$ git add curses/ux_audio_alsa.c <3> +$ edit/compile/test +$ git diff HEAD <4> +$ git commit -a -s <5> +$ edit/compile/test +$ git reset --soft HEAD^ <6> +$ edit/compile/test +$ git diff ORIG_HEAD <7> +$ git commit -a -c ORIG_HEAD <8> +$ git checkout master <9> +$ git merge alsa-audio <10> +$ git log --since='3 days ago' <11> +$ git log v2.43.. curses/ <12> +------------ ++ +<1> create a new topic branch. +<2> revert your botched changes in `curses/ux_audio_oss.c`. +<3> you need to tell git if you added a new file; removal and +modification will be caught if you do `git commit -a` later. +<4> to see what changes you are committing. +<5> commit everything as you have tested, with your sign-off. +<6> take the last commit back, keeping what is in the working tree. +<7> look at the changes since the premature commit we took back. +<8> redo the commit undone in the previous step, using the message +you originally wrote. +<9> switch to the master branch. +<10> merge a topic branch into your master branch. +<11> review commit logs; other forms to limit output can be +combined and include `\--max-count=10` (show 10 commits), +`\--until=2005-12-10`, etc. +<12> view only the changes that touch what's in `curses/` +directory, since `v2.43` tag. + + +Individual Developer (Participant)[[Individual Developer (Participant)]] +------------------------------------------------------------------------ + +A developer working as a participant in a group project needs to +learn how to communicate with others, and uses these commands in +addition to the ones needed by a standalone developer. + + * gitlink:git-clone[1] from the upstream to prime your local + repository. + + * gitlink:git-pull[1] and gitlink:git-fetch[1] from "origin" + to keep up-to-date with the upstream. + + * gitlink:git-push[1] to shared repository, if you adopt CVS + style shared repository workflow. + + * gitlink:git-format-patch[1] to prepare e-mail submission, if + you adopt Linux kernel-style public forum workflow. + +Examples +~~~~~~~~ + +Clone the upstream and work on it. Feed changes to upstream.:: ++ +------------ +$ git clone git://git.kernel.org/pub/scm/.../torvalds/linux-2.6 my2.6 +$ cd my2.6 +$ edit/compile/test; git commit -a -s <1> +$ git format-patch origin <2> +$ git pull <3> +$ git log -p ORIG_HEAD.. arch/i386 include/asm-i386 <4> +$ git pull git://git.kernel.org/pub/.../jgarzik/libata-dev.git ALL <5> +$ git reset --hard ORIG_HEAD <6> +$ git prune <7> +$ git fetch --tags <8> +------------ ++ +<1> repeat as needed. +<2> extract patches from your branch for e-mail submission. +<3> `git pull` fetches from `origin` by default and merges into the +current branch. +<4> immediately after pulling, look at the changes done upstream +since last time we checked, only in the +area we are interested in. +<5> fetch from a specific branch from a specific repository and merge. +<6> revert the pull. +<7> garbage collect leftover objects from reverted pull. +<8> from time to time, obtain official tags from the `origin` +and store them under `.git/refs/tags/`. + + +Push into another repository.:: ++ +------------ +satellite$ git clone mothership:frotz frotz <1> +satellite$ cd frotz +satellite$ git config --get-regexp '^(remote|branch)\.' <2> +remote.origin.url mothership:frotz +remote.origin.fetch refs/heads/*:refs/remotes/origin/* +branch.master.remote origin +branch.master.merge refs/heads/master +satellite$ git config remote.origin.push \ + master:refs/remotes/satellite/master <3> +satellite$ edit/compile/test/commit +satellite$ git push origin <4> + +mothership$ cd frotz +mothership$ git checkout master +mothership$ git merge satellite/master <5> +------------ ++ +<1> mothership machine has a frotz repository under your home +directory; clone from it to start a repository on the satellite +machine. +<2> clone sets these configuration variables by default. +It arranges `git pull` to fetch and store the branches of mothership +machine to local `remotes/origin/*` tracking branches. +<3> arrange `git push` to push local `master` branch to +`remotes/satellite/master` branch of the mothership machine. +<4> push will stash our work away on `remotes/satellite/master` +tracking branch on the mothership machine. You could use this as +a back-up method. +<5> on mothership machine, merge the work done on the satellite +machine into the master branch. + +Branch off of a specific tag.:: ++ +------------ +$ git checkout -b private2.6.14 v2.6.14 <1> +$ edit/compile/test; git commit -a +$ git checkout master +$ git format-patch -k -m --stdout v2.6.14..private2.6.14 | + git am -3 -k <2> +------------ ++ +<1> create a private branch based on a well known (but somewhat behind) +tag. +<2> forward port all changes in `private2.6.14` branch to `master` branch +without a formal "merging". + + +Integrator[[Integrator]] +------------------------ + +A fairly central person acting as the integrator in a group +project receives changes made by others, reviews and integrates +them and publishes the result for others to use, using these +commands in addition to the ones needed by participants. + + * gitlink:git-am[1] to apply patches e-mailed in from your + contributors. + + * gitlink:git-pull[1] to merge from your trusted lieutenants. + + * gitlink:git-format-patch[1] to prepare and send suggested + alternative to contributors. + + * gitlink:git-revert[1] to undo botched commits. + + * gitlink:git-push[1] to publish the bleeding edge. + + +Examples +~~~~~~~~ + +My typical GIT day.:: ++ +------------ +$ git status <1> +$ git show-branch <2> +$ mailx <3> +& s 2 3 4 5 ./+to-apply +& s 7 8 ./+hold-linus +& q +$ git checkout -b topic/one master +$ git am -3 -i -s -u ./+to-apply <4> +$ compile/test +$ git checkout -b hold/linus && git am -3 -i -s -u ./+hold-linus <5> +$ git checkout topic/one && git rebase master <6> +$ git checkout pu && git reset --hard next <7> +$ git merge topic/one topic/two && git merge hold/linus <8> +$ git checkout maint +$ git cherry-pick master~4 <9> +$ compile/test +$ git tag -s -m 'GIT 0.99.9x' v0.99.9x <10> +$ git fetch ko && git show-branch master maint 'tags/ko-*' <11> +$ git push ko <12> +$ git push ko v0.99.9x <13> +------------ ++ +<1> see what I was in the middle of doing, if any. +<2> see what topic branches I have and think about how ready +they are. +<3> read mails, save ones that are applicable, and save others +that are not quite ready. +<4> apply them, interactively, with my sign-offs. +<5> create topic branch as needed and apply, again with my +sign-offs. +<6> rebase internal topic branch that has not been merged to the +master, nor exposed as a part of a stable branch. +<7> restart `pu` every time from the next. +<8> and bundle topic branches still cooking. +<9> backport a critical fix. +<10> create a signed tag. +<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: next:refs/tags/ko-next +Pull: maint:refs/tags/ko-maint +Push: master +Push: next +Push: +pu +Push: maint +------------ ++ +In the output from `git show-branch`, `master` should have +everything `ko-master` has, and `next` should have +everything `ko-next` has. + +<12> push out the bleeding edge. +<13> push the tag out, too. + + +Repository Administration[[Repository Administration]] +------------------------------------------------------ + +A repository administrator uses the following tools to set up +and maintain access to the repository by developers. + + * gitlink:git-daemon[1] to allow anonymous download from + repository. + + * gitlink:git-shell[1] can be used as a 'restricted login shell' + for shared central repository users. + +link:howto/update-hook-example.txt[update hook howto] has a good +example of managing a shared central repository. + + +Examples +~~~~~~~~ +We assume the following in /etc/services:: ++ +------------ +$ grep 9418 /etc/services +git 9418/tcp # Git Version Control System +------------ + +Run git-daemon to serve /pub/scm from inetd.:: ++ +------------ +$ grep git /etc/inetd.conf +git stream tcp nowait nobody \ + /usr/bin/git-daemon git-daemon --inetd --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 --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.:: ++ +------------ +$ grep git /etc/passwd <1> +alice:x:1000:1000::/home/alice:/usr/bin/git-shell +bob:x:1001:1001::/home/bob:/usr/bin/git-shell +cindy:x:1002:1002::/home/cindy:/usr/bin/git-shell +david:x:1003:1003::/home/david:/usr/bin/git-shell +$ grep git /etc/shells <2> +/usr/bin/git-shell +------------ ++ +<1> log-in shell is set to /usr/bin/git-shell, which does not +allow anything but `git push` and `git pull`. The users should +get an ssh access to the machine. +<2> in many distributions /etc/shells needs to list what is used +as the login shell. + +CVS-style shared repository.:: ++ +------------ +$ grep git /etc/group <1> +git:x:9418:alice,bob,cindy,david +$ cd /home/devo.git +$ ls -l <2> + lrwxrwxrwx 1 david git 17 Dec 4 22:40 HEAD -> refs/heads/master + drwxrwsr-x 2 david git 4096 Dec 4 22:40 branches + -rw-rw-r-- 1 david git 84 Dec 4 22:40 config + -rw-rw-r-- 1 david git 58 Dec 4 22:40 description + drwxrwsr-x 2 david git 4096 Dec 4 22:40 hooks + -rw-rw-r-- 1 david git 37504 Dec 4 22:40 index + drwxrwsr-x 2 david git 4096 Dec 4 22:40 info + drwxrwsr-x 4 david git 4096 Dec 4 22:40 objects + drwxrwsr-x 4 david git 4096 Nov 7 14:58 refs + drwxrwsr-x 2 david git 4096 Dec 4 22:40 remotes +$ ls -l hooks/update <3> + -r-xr-xr-x 1 david git 3536 Dec 4 22:40 update +$ cat info/allowed-users <4> +refs/heads/master alice\|cindy +refs/heads/doc-update bob +refs/tags/v[0-9]* david +------------ ++ +<1> place the developers into the same git group. +<2> and make the shared repository writable by the group. +<3> use update-hook example by Carl from Documentation/howto/ +for branch policy control. +<4> alice and cindy can push into master, only bob can push into doc-update. +david is the release manager and is the only person who can +create and push version tags. + +HTTP server to support dumb protocol transfer.:: ++ +------------ +dev$ git update-server-info <1> +dev$ ftp user@isp.example.com <2> +ftp> cp -r .git /home/user/myproject.git +------------ ++ +<1> make sure your info/refs and objects/info/packs are up-to-date +<2> upload to public HTTP server hosted by your ISP. diff --git a/Documentation/fetch-options.txt b/Documentation/fetch-options.txt new file mode 100644 index 0000000000..5b4d184a73 --- /dev/null +++ b/Documentation/fetch-options.txt @@ -0,0 +1,48 @@ +-a, \--append:: + Append ref names and object names of fetched refs to the + existing contents of `.git/FETCH_HEAD`. Without this + option old data in `.git/FETCH_HEAD` will be overwritten. + +\--upload-pack <upload-pack>:: + When given, and the repository to fetch from is handled + by 'git-fetch-pack', '--exec=<upload-pack>' is passed to + the command to specify non-default path for the command + run on the other end. + +-f, \--force:: + When `git-fetch` is used with `<rbranch>:<lbranch>` + refspec, it refuses to update the local branch + `<lbranch>` unless the remote branch `<rbranch>` it + fetches is a descendant of `<lbranch>`. This option + overrides that check. + +\--no-tags:: + By default, `git-fetch` fetches tags that point at + objects that are downloaded from the remote repository + and stores them locally. This option disables this + automatic tag following. + +-t, \--tags:: + Most of the tags are fetched automatically as branch + heads are downloaded, but tags that do not point at + objects reachable from the branch heads that are being + tracked will not be fetched by this mechanism. This + flag lets all tags and their associated objects be + downloaded. + +-k, \--keep:: + Keep downloaded pack. + +-u, \--update-head-ok:: + By default `git-fetch` refuses to update the head which + corresponds to the current branch. This flag disables the + check. This is purely for the internal use for `git-pull` + to communicate with `git-fetch`, and unless you are + implementing your own Porcelain you are not supposed to + use it. + +\--depth=<depth>:: + Deepen the history of a 'shallow' repository created by + `git clone` with `--depth=<depth>` option (see gitlink:git-clone[1]) + by the specified number of commits. + diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt new file mode 100644 index 0000000000..b73a99d61f --- /dev/null +++ b/Documentation/git-add.txt @@ -0,0 +1,215 @@ +git-add(1) +========== + +NAME +---- +git-add - Add file contents to the changeset to be committed next + +SYNOPSIS +-------- +'git-add' [-n] [-v] [-f] [--interactive | -i] [--] <file>... + +DESCRIPTION +----------- +All the changed file contents to be committed together in a single set +of changes must be "added" with the 'add' command before using the +'commit' command. This is not only for adding new files. Even modified +files must be added to the set of changes about to be committed. + +This command can be performed multiple times before a commit. The added +content corresponds to the state of specified file(s) at the time the +'add' command is used. This means the 'commit' command will not consider +subsequent changes to already added content if it is not added again before +the commit. + +The 'git status' command can be used to obtain a summary of what is included +for the next commit. + +This command can be used to add ignored files with `-f` (force) +option, but they have to be +explicitly and exactly specified from the command line. File globbing +and recursive behaviour do not add ignored files. + +Please see gitlink:git-commit[1] for alternative ways to add content to a +commit. + + +OPTIONS +------- +<file>...:: + Files to add content from. Fileglobs (e.g. `*.c`) can + be given to add all matching files. Also a + leading directory name (e.g. `dir` to add `dir/file1` + and `dir/file2`) can be given to add all files in the + directory, recursively. + +-n:: + Don't actually add the file(s), just show if they exist. + +-v:: + Be verbose. + +-f:: + Allow adding otherwise ignored files. + +\i, \--interactive:: + Add modified contents in the working tree interactively to + the index. + +\--:: + This option can be used to separate command-line options from + the list of files, (useful when filenames might be mistaken + for command-line options). + + +EXAMPLES +-------- +git-add Documentation/\\*.txt:: + + Adds content from all `\*.txt` files under `Documentation` + directory and its subdirectories. ++ +Note that the asterisk `\*` is quoted from the shell in this +example; this lets the command to include the files from +subdirectories of `Documentation/` directory. + +git-add git-*.sh:: + + Considers adding content from all git-*.sh scripts. + Because this example lets shell expand the asterisk + (i.e. you are listing the files explicitly), it does not + consider `subdir/git-foo.sh`. + +Interactive mode +---------------- +When the command enters the interactive mode, it shows the +output of the 'status' subcommand, and then goes into its +interactive command loop. + +The command loop shows the list of subcommands available, and +gives a prompt "What now> ". In general, when the prompt ends +with a single '>', you can pick only one of the choices given +and type return, like this: + +------------ + *** Commands *** + 1: status 2: update 3: revert 4: add untracked + 5: patch 6: diff 7: quit 8: help + What now> 1 +------------ + +You also could say "s" or "sta" or "status" above as long as the +choice is unique. + +The main command loop has 6 subcommands (plus help and quit). + +status:: + + This shows the change between HEAD and index (i.e. what will be + committed if you say "git commit"), and between index and + working tree files (i.e. what you could stage further before + "git commit" using "git-add") for each path. A sample output + looks like this: ++ +------------ + staged unstaged path + 1: binary nothing foo.png + 2: +403/-35 +1/-1 git-add--interactive.perl +------------ ++ +It shows that foo.png has differences from HEAD (but that is +binary so line count cannot be shown) and there is no +difference between indexed copy and the working tree +version (if the working tree version were also different, +'binary' would have been shown in place of 'nothing'). The +other file, git-add--interactive.perl, has 403 lines added +and 35 lines deleted if you commit what is in the index, but +working tree file has further modifications (one addition and +one deletion). + +update:: + + This shows the status information and gives prompt + "Update>>". When the prompt ends with double '>>', you can + make more than one selection, concatenated with whitespace or + comma. Also you can say ranges. E.g. "2-5 7,9" to choose + 2,3,4,5,7,9 from the list. You can say '*' to choose + everything. ++ +What you chose are then highlighted with '*', +like this: ++ +------------ + staged unstaged path + 1: binary nothing foo.png +* 2: +403/-35 +1/-1 git-add--interactive.perl +------------ ++ +To remove selection, prefix the input with `-` +like this: ++ +------------ +Update>> -2 +------------ ++ +After making the selection, answer with an empty line to stage the +contents of working tree files for selected paths in the index. + +revert:: + + This has a very similar UI to 'update', and the staged + information for selected paths are reverted to that of the + HEAD version. Reverting new paths makes them untracked. + +add untracked:: + + This has a very similar UI to 'update' and + 'revert', and lets you add untracked paths to the index. + +patch:: + + This lets you choose one path out of 'status' like selection. + After choosing the path, it presents diff between the index + and the working tree file and asks you if you want to stage + the change of each hunk. You can say: + + y - add the change from that hunk to index + n - do not add the change from that hunk to index + a - add the change from that hunk and all the rest to index + d - do not the change from that hunk nor any of the rest to index + j - do not decide on this hunk now, and view the next + undecided hunk + J - do not decide on this hunk now, and view the next hunk + k - do not decide on this hunk now, and view the previous + undecided hunk + K - do not decide on this hunk now, and view the previous hunk ++ +After deciding the fate for all hunks, if there is any hunk +that was chosen, the index is updated with the selected hunks. + +diff:: + + This lets you review what will be committed (i.e. between + HEAD and index). + + +See Also +-------- +gitlink:git-status[1] +gitlink:git-rm[1] +gitlink:git-mv[1] +gitlink:git-commit[1] +gitlink:git-update-index[1] + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt new file mode 100644 index 0000000000..4fb1d84413 --- /dev/null +++ b/Documentation/git-am.txt @@ -0,0 +1,124 @@ +git-am(1) +========= + +NAME +---- +git-am - Apply a series of patches from a mailbox + + +SYNOPSIS +-------- +[verse] +'git-am' [--signoff] [--dotest=<dir>] [--utf8 | --no-utf8] [--binary] [--3way] + [--interactive] [--whitespace=<option>] [-C<n>] [-p<n>] + <mbox>... +'git-am' [--skip | --resolved] + +DESCRIPTION +----------- +Splits mail messages in a mailbox into commit log message, +authorship information and patches, and applies them to the +current branch. + +OPTIONS +------- +<mbox>...:: + The list of mailbox files to read patches from. If you do not + supply this argument, reads from the standard input. + +--signoff:: + Add `Signed-off-by:` line to the commit message, using + the committer identity of yourself. + +--dotest=<dir>:: + Instead of `.dotest` directory, use <dir> as a working + area to store extracted patches. + +--keep:: + Pass `-k` flag to `git-mailinfo` (see gitlink:git-mailinfo[1]). + +--utf8:: + Pass `-u` flag to `git-mailinfo` (see gitlink:git-mailinfo[1]). + The proposed commit log message taken from the e-mail + are re-coded into UTF-8 encoding (configuration variable + `i18n.commitencoding` can be used to specify project's + preferred encoding if it is not UTF-8). ++ +This was optional in prior versions of git, but now it is the +default. You could use `--no-utf8` to override this. + +--no-utf8:: + Do not pass `-u` flag to `git-mailinfo` (see + gitlink:git-mailinfo[1]). + +--binary:: + Pass `--allow-binary-replacement` flag to `git-apply` + (see gitlink:git-apply[1]). + +--3way:: + When the patch does not apply cleanly, fall back on + 3-way merge, if the patch records the identity of blobs + it is supposed to apply to, and we have those blobs + locally. + +--skip:: + Skip the current patch. This is only meaningful when + restarting an aborted patch. + +--whitespace=<option>:: + This flag is passed to the `git-apply` program that applies + the patch. + +-C<n>, -p<n>:: + These flag are passed to the `git-apply` program that applies + the patch. + +--interactive:: + Run interactively, just like git-applymbox. + +--resolved:: + After a patch failure (e.g. attempting to apply + conflicting patch), the user has applied it by hand and + the index file stores the result of the application. + Make a commit using the authorship and commit log + extracted from the e-mail message and the current index + file, and continue. + +DISCUSSION +---------- + +When initially invoking it, you give it names of the mailboxes +to crunch. Upon seeing the first patch that does not apply, it +aborts in the middle, just like 'git-applymbox' does. You can +recover from this in one of two ways: + +. skip the current one by re-running the command with '--skip' + option. + +. hand resolve the conflict in the working directory, and update + the index file to bring it in a state that the patch should + have produced. Then run the command with '--resolved' option. + +The command refuses to process new mailboxes while `.dotest` +directory exists, so if you decide to start over from scratch, +run `rm -f .dotest` before running the command with mailbox +names. + + +SEE ALSO +-------- +gitlink:git-applymbox[1], gitlink:git-applypatch[1], gitlink:git-apply[1]. + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Petr Baudis, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-annotate.txt b/Documentation/git-annotate.txt new file mode 100644 index 0000000000..7baf73111b --- /dev/null +++ b/Documentation/git-annotate.txt @@ -0,0 +1,44 @@ +git-annotate(1) +=============== + +NAME +---- +git-annotate - Annotate file lines with commit info + +SYNOPSIS +-------- +git-annotate [options] file [revision] + +DESCRIPTION +----------- +Annotates each line in the given file with information from the commit +which introduced the line. Optionally annotate from a given revision. + +OPTIONS +------- +-l, --long:: + Show long rev (Defaults off). + +-t, --time:: + Show raw timestamp (Defaults off). + +-r, --rename:: + Follow renames (Defaults on). + +-S, --rev-file <revs-file>:: + Use revs from revs-file instead of calling git-rev-list. + +-h, --help:: + Show help message. + +SEE ALSO +-------- +gitlink:git-blame[1] + +AUTHOR +------ +Written by Ryan Anderson <ryan@michonline.com>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-apply.txt b/Documentation/git-apply.txt new file mode 100644 index 0000000000..065ba1bf24 --- /dev/null +++ b/Documentation/git-apply.txt @@ -0,0 +1,185 @@ +git-apply(1) +============ + +NAME +---- +git-apply - Apply a patch on a git index file and a working tree + + +SYNOPSIS +-------- +[verse] +'git-apply' [--stat] [--numstat] [--summary] [--check] [--index] [--apply] + [--no-add] [--index-info] [--allow-binary-replacement | --binary] + [-R | --reverse] [--reject] [-z] [-pNUM] [-CNUM] [--inaccurate-eof] + [--whitespace=<nowarn|warn|error|error-all|strip>] [--exclude=PATH] + [--cached] [--verbose] [<patch>...] + +DESCRIPTION +----------- +Reads supplied diff output and applies it on a git index file +and a work tree. + +OPTIONS +------- +<patch>...:: + The files to read patch from. '-' can be used to read + from the standard input. + +--stat:: + Instead of applying the patch, output diffstat for the + input. Turns off "apply". + +--numstat:: + Similar to \--stat, but shows number of added and + deleted lines in decimal notation and pathname without + abbreviation, to make it more machine friendly. For + binary files, outputs two `-` instead of saying + `0 0`. Turns off "apply". + +--summary:: + Instead of applying the patch, output a condensed + summary of information obtained from git diff extended + headers, such as creations, renames and mode changes. + Turns off "apply". + +--check:: + Instead of applying the patch, see if the patch is + applicable to the current work tree and/or the index + file and detects errors. Turns off "apply". + +--index:: + When --check is in effect, or when applying the patch + (which is the default when none of the options that + disables it is in effect), make sure the patch is + applicable to what the current index file records. If + the file to be patched in the work tree is not + up-to-date, it is flagged as an error. This flag also + causes the index file to be updated. + +--cached:: + Apply a patch without touching the working tree. Instead, take the + cached data, apply the patch, and store the result in the index, + without using the working tree. This implies '--index'. + +--index-info:: + Newer git-diff output has embedded 'index information' + for each blob to help identify the original version that + the patch applies to. When this flag is given, and if + the original version of the blob is available locally, + outputs information about them to the standard output. + +-R, --reverse:: + Apply the patch in reverse. + +--reject:: + For atomicity, gitlink:git-apply[1] by default fails the whole patch and + does not touch the working tree when some of the hunks + do not apply. This option makes it apply + the parts of the patch that are applicable, and leave the + rejected hunks in corresponding *.rej files. + +-z:: + When showing the index information, do not munge paths, + but use NUL terminated machine readable format. Without + this flag, the pathnames output will have TAB, LF, and + backslash characters replaced with `\t`, `\n`, and `\\`, + respectively. + +-p<n>:: + Remove <n> leading slashes from traditional diff paths. The + default is 1. + +-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 must match. By default no context is + ever ignored. + +--unidiff-zero:: + By default, gitlink:git-apply[1] expects that the patch being + applied is a unified diff with at least one line of context. + This provides good safety measures, but breaks down when + applying a diff generated with --unified=0. To bypass these + checks use '--unidiff-zero'. ++ +Note, for the reasons stated above usage of context-free patches are +discouraged. + +--apply:: + If you use any of the options marked "Turns off + 'apply'" above, gitlink:git-apply[1] reads and outputs the + information you asked without actually applying the + patch. Give this flag after those flags to also apply + the patch. + +--no-add:: + When applying a patch, ignore additions made by the + patch. This can be used to extract common part between + two files by first running `diff` on them and applying + the result with this option, which would apply the + deletion part but not addition part. + +--allow-binary-replacement, --binary:: + Historically we did not allow binary patch applied + without an explicit permission from the user, and this + flag was the way to do so. Currently we always allow binary + patch application, so this is a no-op. + +--exclude=<path-pattern>:: + Don't apply changes to files matching the given path pattern. This can + be useful when importing patchsets, where you want to exclude certain + files or directories. + +--whitespace=<option>:: + When applying a patch, detect a new or modified line + that ends with trailing whitespaces (this includes a + line that solely consists of whitespaces). By default, + the command outputs warning messages and applies the + patch. + When gitlink:git-apply[1] is used for statistics and not applying a + patch, it defaults to `nowarn`. + You can use different `<option>` to control this + behavior: ++ +* `nowarn` turns off the trailing whitespace warning. +* `warn` outputs warnings for a few such errors, but applies the + patch (default). +* `error` outputs warnings for a few such errors, and refuses + to apply the patch. +* `error-all` is similar to `error` but shows all errors. +* `strip` outputs warnings for a few such errors, strips out the + trailing whitespaces and applies the patch. + +--inaccurate-eof:: + Under certain circumstances, some versions of diff do not correctly + detect a missing new-line at the end of the file. As a result, patches + created by such diff programs do not record incomplete lines + correctly. This option adds support for applying such patches by + working around this bug. + +--verbose:: + Report progress to stderr. By default, only a message about the + current patch being applied will be printed. This option will cause + additional information to be reported. + +Configuration +------------- + +apply.whitespace:: + When no `--whitespace` flag is given from the command + line, this configuration item is used as the default. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-applymbox.txt b/Documentation/git-applymbox.txt new file mode 100644 index 0000000000..95dc65a583 --- /dev/null +++ b/Documentation/git-applymbox.txt @@ -0,0 +1,92 @@ +git-applymbox(1) +================ + +NAME +---- +git-applymbox - Apply a series of patches in a mailbox + + +SYNOPSIS +-------- +'git-applymbox' [-u] [-k] [-q] [-m] ( -c .dotest/<num> | <mbox> ) [ <signoff> ] + +DESCRIPTION +----------- +Splits mail messages in a mailbox into commit log message, +authorship information and patches, and applies them to the +current branch. + + +OPTIONS +------- +-q:: + Apply patches interactively. The user will be given + opportunity to edit the log message and the patch before + attempting to apply it. + +-k:: + Usually the program 'cleans up' the Subject: header line + to extract the title line for the commit log message, + among which (1) remove 'Re:' or 're:', (2) leading + whitespaces, (3) '[' up to ']', typically '[PATCH]', and + then prepends "[PATCH] ". This flag forbids this + munging, and is most useful when used to read back 'git + format-patch --mbox' output. + +-m:: + Patches are applied with `git-apply` command, and unless + it cleanly applies without fuzz, the processing fails. + With this flag, if a tree that the patch applies cleanly + is found in a repository, the patch is applied to the + tree and then a 3-way merge between the resulting tree + and the current tree. + +-u:: + The commit log message, author name and author email are + taken from the e-mail, and after minimally decoding MIME + transfer encoding, re-coded in UTF-8 by transliterating + them. This used to be optional but now it is the default. ++ +Note that the patch is always used as-is without charset +conversion, even with this flag. + +-c .dotest/<num>:: + When the patch contained in an e-mail does not cleanly + apply, the command exits with an error message. The + patch and extracted message are found in .dotest/, and + you could re-run 'git applymbox' with '-c .dotest/<num>' + flag to restart the process after inspecting and fixing + them. + +<mbox>:: + The name of the file that contains the e-mail messages + with patches. This file should be in the UNIX mailbox + format. See 'SubmittingPatches' document to learn about + the formatting convention for e-mail submission. + +<signoff>:: + The name of the file that contains your "Signed-off-by" + line. See 'SubmittingPatches' document to learn what + "Signed-off-by" line means. You can also just say + 'yes', 'true', 'me', or 'please' to use an automatically + generated "Signed-off-by" line based on your committer + identity. + + +SEE ALSO +-------- +gitlink:git-am[1], gitlink:git-applypatch[1]. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-applypatch.txt b/Documentation/git-applypatch.txt new file mode 100644 index 0000000000..451434a757 --- /dev/null +++ b/Documentation/git-applypatch.txt @@ -0,0 +1,53 @@ +git-applypatch(1) +================= + +NAME +---- +git-applypatch - Apply one patch extracted from an e-mail + + +SYNOPSIS +-------- +'git-applypatch' <msg> <patch> <info> [<signoff>] + +DESCRIPTION +----------- +This is usually not what an end user wants to run directly. See +gitlink:git-am[1] instead. + +Takes three files <msg>, <patch>, and <info> prepared from an +e-mail message by 'git-mailinfo', and creates a commit. It is +usually not necessary to use this command directly. + +This command can run `applypatch-msg`, `pre-applypatch`, and +`post-applypatch` hooks. See link:hooks.html[hooks] for more +information. + + +OPTIONS +------- +<msg>:: + Commit log message (sans the first line, which comes + from e-mail Subject stored in <info>). + +<patch>:: + The patch to apply. + +<info>:: + Author and subject information extracted from e-mail, + used on "author" line and as the first line of the + commit log message. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-archimport.txt b/Documentation/git-archimport.txt new file mode 100644 index 0000000000..5a13187a87 --- /dev/null +++ b/Documentation/git-archimport.txt @@ -0,0 +1,106 @@ +git-archimport(1) +================= + +NAME +---- +git-archimport - Import an Arch repository into git + + +SYNOPSIS +-------- +[verse] +'git-archimport' [-h] [-v] [-o] [-a] [-f] [-T] [-D depth] [-t tempdir] + <archive/branch> [ <archive/branch> ] + +DESCRIPTION +----------- +Imports a project from one or more Arch repositories. It will follow branches +and repositories within the namespaces defined by the <archive/branch> +parameters supplied. If it cannot find the remote branch a merge comes from +it will just import it as a regular commit. If it can find it, it will mark it +as a merge whenever possible (see discussion below). + +The script expects you to provide the key roots where it can start the import +from an 'initial import' or 'tag' type of Arch commit. It will follow and +import new branches within the provided roots. + +It expects to be dealing with one project only. If it sees +branches that have different roots, it will refuse to run. In that case, +edit your <archive/branch> parameters to define clearly the scope of the +import. + +`git-archimport` uses `tla` extensively in the background to access the +Arch repository. +Make sure you have a recent version of `tla` available in the path. `tla` must +know about the repositories you pass to `git-archimport`. + +For the initial import `git-archimport` expects to find itself in an empty +directory. To follow the development of a project that uses Arch, rerun +`git-archimport` with the same parameters as the initial import to perform +incremental imports. + +MERGES +------ +Patch merge data from Arch is used to mark merges in git as well. git +does not care much about tracking patches, and only considers a merge when a +branch incorporates all the commits since the point they forked. The end result +is that git will have a good idea of how far branches have diverged. So the +import process does lose some patch-trading metadata. + +Fortunately, when you try and merge branches imported from Arch, +git will find a good merge base, and it has a good chance of identifying +patches that have been traded out-of-sequence between the branches. + +OPTIONS +------- + +-h:: + Display usage. + +-v:: + Verbose output. + +-T:: + Many tags. Will create a tag for every commit, reflecting the commit + name in the Arch repository. + +-f:: + Use the fast patchset import strategy. This can be significantly + faster for large trees, but cannot handle directory renames or + permissions changes. The default strategy is slow and safe. + +-o:: + Use this for compatibility with old-style branch names used by + earlier versions of git-archimport. Old-style branch names + were category--branch, whereas new-style branch names are + archive,category--branch--version. + +-D <depth>:: + Follow merge ancestry and attempt to import trees that have been + merged from. Specify a depth greater than 1 if patch logs have been + pruned. + +-a:: + Attempt to auto-register archives at http://mirrors.sourcecontrol.net + This is particularly useful with the -D option. + +-t <tmpdir>:: + Override the default tempdir. + + +<archive/branch>:: + Archive/branch identifier in a format that `tla log` understands. + + +Author +------ +Written by Martin Langhoff <martin@catalyst.net.nz>. + +Documentation +-------------- +Documentation by Junio C Hamano, Martin Langhoff and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-archive.txt b/Documentation/git-archive.txt new file mode 100644 index 0000000000..493474b2ee --- /dev/null +++ b/Documentation/git-archive.txt @@ -0,0 +1,113 @@ +git-archive(1) +============== + +NAME +---- +git-archive - Creates an archive of files from a named tree + + +SYNOPSIS +-------- +'git-archive' --format=<fmt> [--list] [--prefix=<prefix>/] [<extra>] + [--remote=<repo>] <tree-ish> [path...] + +DESCRIPTION +----------- +Creates an archive of the specified format containing the tree +structure for the named tree. If <prefix> is specified it is +prepended to the filenames in the archive. + +'git-archive' behaves differently when given a tree ID versus when +given a commit ID or tag ID. In the first case the current time is +used as modification time of each file in the archive. In the latter +case the commit time as recorded in the referenced commit object is +used instead. Additionally the commit ID is stored in a global +extended pax header if the tar format is used; it can be extracted +using 'git-get-tar-commit-id'. In ZIP files it is stored as a file +comment. + +OPTIONS +------- + +--format=<fmt>:: + Format of the resulting archive: 'tar', 'zip'... + +--list:: + Show all available formats. + +--prefix=<prefix>/:: + Prepend <prefix>/ to each filename in the archive. + +<extra>:: + This can be any options that the archiver backend understand. + See next section. + +--remote=<repo>:: + Instead of making a tar archive from local repository, + retrieve a tar archive from a remote repository. + +<tree-ish>:: + The tree or commit to produce an archive for. + +path:: + If one or more paths are specified, include only these in the + archive, otherwise include all files and subdirectories. + +BACKEND EXTRA OPTIONS +--------------------- + +zip +~~~ +-0:: + Store the files instead of deflating them. +-9:: + Highest and slowest compression level. You can specify any + number from 1 to 9 to adjust compression speed and ratio. + + +CONFIGURATION +------------- +By default, file and directories modes are set to 0666 or 0777 in tar +archives. It is possible to change this by setting the "umask" variable +in the repository configuration as follows : + +[tar] + umask = 002 ;# group friendly + +The special umask value "user" indicates that the user's current umask +will be used instead. The default value remains 0, which means world +readable/writable files and directories. + +EXAMPLES +-------- +git archive --format=tar --prefix=junk/ HEAD | (cd /var/tmp/ && tar xf -):: + + 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 archive --format=tar --prefix=git-1.4.0/ v1.4.0 | gzip >git-1.4.0.tar.gz:: + + Create a compressed tarball for v1.4.0 release. + +git archive --format=tar --prefix=git-1.4.0/ v1.4.0{caret}\{tree\} | gzip >git-1.4.0.tar.gz:: + + Create a compressed tarball for v1.4.0 release, but without a + global extended pax header. + +git archive --format=zip --prefix=git-docs/ HEAD:Documentation/ > git-1.4.0-docs.zip:: + + Put everything in the current head's Documentation/ directory + into 'git-1.4.0-docs.zip', with the prefix 'git-docs/'. + +Author +------ +Written by Franck Bui-Huu and Rene Scharfe. + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-bisect.txt b/Documentation/git-bisect.txt new file mode 100644 index 0000000000..16ec7269b2 --- /dev/null +++ b/Documentation/git-bisect.txt @@ -0,0 +1,136 @@ +git-bisect(1) +============= + +NAME +---- +git-bisect - Find the change that introduced a bug by binary search + + +SYNOPSIS +-------- +'git bisect' <subcommand> <options> + +DESCRIPTION +----------- +The command takes various subcommands, and different options +depending on the subcommand: + + git bisect start [<paths>...] + git bisect bad <rev> + git bisect good <rev> + git bisect reset [<branch>] + git bisect visualize + git bisect replay <logfile> + git bisect log + +This command uses 'git-rev-list --bisect' option to help drive +the binary search process to find which change introduced a bug, +given an old "good" commit object name and a later "bad" commit +object name. + +The way you use it is: + +------------------------------------------------ +$ git bisect start +$ git bisect bad # Current version is bad +$ git bisect good v2.6.13-rc2 # v2.6.13-rc2 was the last version + # tested that was good +------------------------------------------------ + +When you give at least one bad and one good versions, it will +bisect the revision tree and say something like: + +------------------------------------------------ +Bisecting: 675 revisions left to test after this +------------------------------------------------ + +and check out the state in the middle. Now, compile that kernel, and boot +it. Now, let's say that this booted kernel works fine, then just do + +------------------------------------------------ +$ git bisect good # this one is good +------------------------------------------------ + +which will now say + +------------------------------------------------ +Bisecting: 337 revisions left to test after this +------------------------------------------------ + +and you continue along, compiling that one, testing it, and depending on +whether it is good or bad, you say "git bisect good" or "git bisect bad", +and ask for the next bisection. + +Until you have no more left, and you'll have been left with the first bad +kernel rev in "refs/bisect/bad". + +Oh, and then after you want to reset to the original head, do a + +------------------------------------------------ +$ git bisect reset +------------------------------------------------ + +to get back to the master branch, instead of being in one of the bisection +branches ("git bisect start" will do that for you too, actually: it will +reset the bisection state, and before it does that it checks that you're +not using some old bisection branch). + +During the bisection process, you can say + +------------ +$ git bisect visualize +------------ + +to see the currently remaining suspects in `gitk`. + +The good/bad input is logged, and `git bisect +log` shows what you have done so far. You can truncate its +output somewhere and save it in a file, and run + +------------ +$ git bisect replay that-file +------------ + +if you find later you made a mistake telling good/bad about a +revision. + +If in a middle of bisect session, you know what the bisect +suggested to try next is not a good one to test (e.g. the change +the commit introduces is known not to work in your environment +and you know it does not have anything to do with the bug you +are chasing), you may want to find a near-by commit and try that +instead. It goes something like this: + +------------ +$ git bisect good/bad # previous round was good/bad. +Bisecting: 337 revisions left to test after this +$ git bisect visualize # oops, that is uninteresting. +$ git reset --hard HEAD~3 # try 3 revs before what + # was suggested +------------ + +Then compile and test the one you chose to try. After that, +tell bisect what the result was as usual. + +You can further cut down the number of trials if you know what +part of the tree is involved in the problem you are tracking +down, by giving paths parameters when you say `bisect start`, +like this: + +------------ +$ git bisect start arch/i386 include/asm-i386 +------------ + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-blame.txt b/Documentation/git-blame.txt new file mode 100644 index 0000000000..5c9888d014 --- /dev/null +++ b/Documentation/git-blame.txt @@ -0,0 +1,223 @@ +git-blame(1) +============ + +NAME +---- +git-blame - Show what revision and author last modified each line of a file + +SYNOPSIS +-------- +[verse] +'git-blame' [-c] [-l] [-t] [-f] [-n] [-p] [--incremental] [-L n,m] [-S <revs-file>] + [-M] [-C] [-C] [--since=<date>] [<rev> | --contents <file>] [--] <file> + +DESCRIPTION +----------- + +Annotates each line in the given file with information from the revision which +last modified the line. Optionally, start annotating from the given revision. + +Also it can limit the range of lines annotated. + +This report doesn't tell you anything about lines which have been deleted or +replaced; you need to use a tool such as gitlink:git-diff[1] or the "pickaxe" +interface briefly mentioned in the following paragraph. + +Apart from supporting file annotation, git also supports searching the +development history for when a code snippet occurred in a change. This makes it +possible to track when a code snippet was added to a file, moved or copied +between files, and eventually deleted or replaced. It works by searching for +a text string in the diff. A small example: + +----------------------------------------------------------------------------- +$ git log --pretty=oneline -S'blame_usage' +5040f17eba15504bad66b14a645bddd9b015ebb7 blame -S <ancestry-file> +ea4c7f9bf69e781dd0cd88d2bccb2bf5cc15c9a7 git-blame: Make the output +----------------------------------------------------------------------------- + +OPTIONS +------- +-c, --compatibility:: + Use the same output mode as gitlink:git-annotate[1] (Default: off). + +-L n,m:: + Annotate only the specified line range (lines count from 1). + +-l, --long:: + 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 gitlink:git-rev-list[1]. + +-f, --show-name:: + Show filename in the original commit. By default + filename is shown if there is any line that came from a + file with different name, due to rename detection. + +-n, --show-number:: + Show line number in the original commit (Default: off). + +-p, --porcelain:: + Show in a format designed for machine consumption. + +--incremental:: + Show the result incrementally in a format designed for + machine consumption. + +--contents <file>:: + When <rev> is not specified, the command annotates the + changes starting backwards from the working tree copy. + This flag makes the command pretend as if the working + tree copy has the contents of he named file (specify + `-` to make the command read from the standard input). + +-M:: + Detect moving lines in the file as well. When a commit + moves a block of lines in a file (e.g. the original file + has A and then B, and the commit changes it to B and + then A), traditional 'blame' algorithm typically blames + the lines that were moved up (i.e. B) to the parent and + assigns blame to the lines that were moved down (i.e. A) + to the child commit. With this option, both groups of + lines are blamed on the parent. + +-C:: + In addition to `-M`, detect lines copied from other + files that were modified in the same commit. This is + useful when you reorganize your program and move code + around across files. When this option is given twice, + the command looks for copies from all other files in the + parent for the commit that creates the file in addition. + +-h, --help:: + Show help message. + + +THE PORCELAIN FORMAT +-------------------- + +In this format, each line is output after a header; the +header at the minimum has the first line which has: + +- 40-byte SHA-1 of the commit the line is attributed to; +- the line number of the line in the original file; +- the line number of the line in the final file; +- on a line that starts a group of line from a different + commit than the previous one, the number of lines in this + group. On subsequent lines this field is absent. + +This header line is followed by the following information +at least once for each commit: + +- author name ("author"), email ("author-mail"), time + ("author-time"), and timezone ("author-tz"); similarly + for committer. +- filename in the commit the line is attributed to. +- the first line of the commit log message ("summary"). + +The contents of the actual line is output after the above +header, prefixed by a TAB. This is to allow adding more +header elements later. + + +SPECIFYING RANGES +----------------- + +Unlike `git-blame` and `git-annotate` in older git, the extent +of annotation can be limited to both line ranges and revision +ranges. When you are interested in finding the origin for +ll. 40-60 for file `foo`, you can use `-L` option like these +(they mean the same thing -- both ask for 21 lines starting at +line 40): + + git blame -L 40,60 foo + git blame -L 40,+21 foo + +Also you can use regular expression to specify the line range. + + git blame -L '/^sub hello {/,/^}$/' foo + +would limit the annotation to the body of `hello` subroutine. + +When you are not interested in changes older than the version +v2.6.18, or changes older than 3 weeks, you can use revision +range specifiers similar to `git-rev-list`: + + git blame v2.6.18.. -- foo + git blame --since=3.weeks -- foo + +When revision range specifiers are used to limit the annotation, +lines that have not changed since the range boundary (either the +commit v2.6.18 or the most recent commit that is more than 3 +weeks old in the above example) are blamed for that range +boundary commit. + +A particularly useful way is to see if an added file have lines +created by copy-and-paste from existing files. Sometimes this +indicates that the developer was being sloppy and did not +refactor the code properly. You can first find the commit that +introduced the file with: + + git log --diff-filter=A --pretty=short -- foo + +and then annotate the change between the commit and its +parents, using `commit{caret}!` notation: + + git blame -C -C -f $commit^! -- foo + + +INCREMENTAL OUTPUT +------------------ + +When called with `--incremental` option, the command outputs the +result as it is built. The output generally will talk about +lines touched by more recent commits first (i.e. the lines will +be annotated out of order) and is meant to be used by +interactive viewers. + +The output format is similar to the Porcelain format, but it +does not contain the actual lines from the file that is being +annotated. + +. Each blame entry always starts with a line of: + + <40-byte hex sha1> <sourceline> <resultline> <num_lines> ++ +Line numbers count from 1. + +. The first time that commit shows up in the stream, it has various + other information about it printed out with a one-word tag at the + beginning of each line about that "extended commit info" (author, + email, committer, dates, summary etc). + +. Unlike Porcelain format, the filename information is always + given and terminates the entry: + + "filename" <whitespace-quoted-filename-goes-here> ++ +and thus it's really quite easy to parse for some line- and word-oriented +parser (which should be quite natural for most scripting languages). ++ +[NOTE] +For people who do parsing: to make it more robust, just ignore any +lines in between the first and last one ("<sha1>" and "filename" lines) +where you don't recognize the tag-words (or care about that particular +one) at the beginning of the "extended information" lines. That way, if +there is ever added information (like the commit encoding or extended +commit commentary), a blame viewer won't ever care. + + +SEE ALSO +-------- +gitlink:git-annotate[1] + +AUTHOR +------ +Written by Junio C Hamano <junkio@cox.net> + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt new file mode 100644 index 0000000000..aa1fdd402a --- /dev/null +++ b/Documentation/git-branch.txt @@ -0,0 +1,150 @@ +git-branch(1) +============= + +NAME +---- +git-branch - List, create, or delete branches + +SYNOPSIS +-------- +[verse] +'git-branch' [--color | --no-color] [-r | -a] [-v [--abbrev=<length>]] +'git-branch' [-l] [-f] <branchname> [<start-point>] +'git-branch' (-m | -M) [<oldbranch>] <newbranch> +'git-branch' (-d | -D) [-r] <branchname>... + +DESCRIPTION +----------- +With no arguments given a list of existing branches +will be shown, the current branch will be highlighted with an asterisk. +Option `-r` causes the remote-tracking branches to be listed, +and option `-a` shows both. + +In its second form, a new branch named <branchname> will be created. +It will start out with a head equal to the one given as <start-point>. +If no <start-point> is given, the branch will be created with a head +equal to that of the currently checked out branch. + +With a '-m' or '-M' option, <oldbranch> will be renamed to <newbranch>. +If <oldbranch> had a corresponding reflog, it is renamed to match +<newbranch>, and a reflog entry is created to remember the branch +renaming. If <newbranch> exists, -M must be used to force the rename +to happen. + +With a `-d` or `-D` option, `<branchname>` will be deleted. You may +specify more than one branch for deletion. If the branch currently +has a ref log then the ref log will also be deleted. Use -r together with -d +to delete remote-tracking branches. + + +OPTIONS +------- +-d:: + Delete a branch. The branch must be fully merged. + +-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. + +-m:: + Move/rename a branch and the corresponding reflog. + +-M:: + Move/rename a branch even if the new branchname already exists. + +--color:: + Color branches to highlight current, local, and remote branches. + +--no-color:: + Turn off branch colors, even when the configuration file gives the + default to color output. + +-r:: + List or delete (if used with -d) the remote-tracking branches. + +-a:: + List both remote-tracking branches and local branches. + +-v:: + Show sha1 and commit subject line for each head. + +--abbrev=<length>:: + Alter minimum display length for sha1 in output listing, + default value is 7. + +<branchname>:: + The name of the branch to create or delete. + The new branch name must pass all checks defined by + gitlink:git-check-ref-format[1]. Some of these checks + may restrict the characters allowed in a branch name. + +<start-point>:: + The new branch will be created with a HEAD equal to this. It may + be given as a branch name, a commit-id, or a tag. If this option + is omitted, the current branch is assumed. + +<oldbranch>:: + The name of an existing branch to rename. + +<newbranch>:: + The new name for an existing branch. The same restrictions as for + <branchname> applies. + + +Examples +-------- + +Start development off of a known tag:: ++ +------------ +$ git clone git://git.kernel.org/pub/scm/.../linux-2.6 my2.6 +$ cd my2.6 +$ git branch my2.6.14 v2.6.14 <1> +$ git checkout my2.6.14 +------------ ++ +<1> This step and the next one could be combined into a single step with +"checkout -b my2.6.14 v2.6.14". + +Delete unneeded branch:: ++ +------------ +$ git clone git://git.kernel.org/.../git.git my.git +$ cd my.git +$ git branch -d -r todo html man <1> +$ git branch -D test <2> +------------ ++ +<1> delete remote-tracking branches "todo", "html", "man" +<2> delete "test" branch even if the "master" branch does not have all +commits from todo branch. + + +Notes +----- + +If you are creating a branch that you want to immediately checkout, it's +easier to use the git checkout command with its `-b` option to create +a branch and check it out with a single command. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-cat-file.txt b/Documentation/git-cat-file.txt new file mode 100644 index 0000000000..075c0d05ef --- /dev/null +++ b/Documentation/git-cat-file.txt @@ -0,0 +1,74 @@ +git-cat-file(1) +=============== + +NAME +---- +git-cat-file - Provide content or type/size information for repository objects + + +SYNOPSIS +-------- +'git-cat-file' [-t | -s | -e | -p | <type>] <object> + +DESCRIPTION +----------- +Provides content or type of objects in the repository. The type +is required unless '-t' or '-p' is used to find the object type, +or '-s' is used to find the object size. + +OPTIONS +------- +<object>:: + The name of the object to show. + For a more complete list of ways to spell object names, see + "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1]. + +-t:: + Instead of the content, show the object type identified by + <object>. + +-s:: + Instead of the content, show the object size identified by + <object>. + +-e:: + Suppress all output; instead exit with zero status if <object> + exists and is a valid object. + +-p:: + Pretty-print the contents of <object> based on its type. + +<type>:: + Typically this matches the real type of <object> but asking + for a type that can trivially be dereferenced from the given + <object> is also permitted. An example is to ask for a + "tree" with <object> being a commit object that contains it, + or to ask for a "blob" with <object> being a tag object that + points at it. + +OUTPUT +------ +If '-t' is specified, one of the <type>. + +If '-s' is specified, the size of the <object> in bytes. + +If '-e' is specified, no output. + +If '-p' is specified, the contents of <object> are pretty-printed. + +Otherwise the raw (though uncompressed) contents of the <object> will +be returned. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-check-ref-format.txt b/Documentation/git-check-ref-format.txt new file mode 100644 index 0000000000..13a5f43049 --- /dev/null +++ b/Documentation/git-check-ref-format.txt @@ -0,0 +1,55 @@ +git-check-ref-format(1) +======================= + +NAME +---- +git-check-ref-format - Make sure ref name is well formed + +SYNOPSIS +-------- +'git-check-ref-format' <refname> + +DESCRIPTION +----------- +Checks if a given 'refname' is acceptable, and exits non-zero if +it is not. + +A reference is used in git to specify branches and tags. A +branch head is stored under `$GIT_DIR/refs/heads` directory, and +a tag is stored under `$GIT_DIR/refs/tags` directory. git +imposes the following rules on how refs are named: + +. 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; + +. It cannot have ASCII control character (i.e. bytes whose + values are lower than \040, or \177 `DEL`), space, tilde `~`, + caret `{caret}`, colon `:`, question-mark `?`, asterisk `*`, + or open bracket `[` anywhere; + +. It cannot end with a slash `/`. + +These rules makes it easy for shell script based tools to parse +refnames, pathname expansion by the shell when a refname is used +unquoted (by mistake), and also avoids ambiguities in certain +refname expressions (see gitlink:git-rev-parse[1]). Namely: + +. double-dot `..` are often used as in `ref1..ref2`, and in some + context this notation means `{caret}ref1 ref2` (i.e. not in + ref1 and in ref2). + +. tilde `~` and caret `{caret}` are used to introduce postfix + 'nth parent' and 'peel onion' operation. + +. colon `:` is used as in `srcref:dstref` to mean "use srcref\'s + value and store it in dstref" in fetch and push operations. + It may also be used to select a specific object such as with + gitlink:git-cat-file[1] "git-cat-file blob v1.3.3:refs.c". + + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-checkout-index.txt b/Documentation/git-checkout-index.txt new file mode 100644 index 0000000000..6dd6db04bb --- /dev/null +++ b/Documentation/git-checkout-index.txt @@ -0,0 +1,185 @@ +git-checkout-index(1) +===================== + +NAME +---- +git-checkout-index - Copy files from the index to the working tree + + +SYNOPSIS +-------- +[verse] +'git-checkout-index' [-u] [-q] [-a] [-f] [-n] [--prefix=<string>] + [--stage=<number>|all] + [--temp] + [-z] [--stdin] + [--] [<file>]\* + +DESCRIPTION +----------- +Will copy all files listed from the index to the working directory +(not overwriting existing files). + +OPTIONS +------- +-u|--index:: + update stat information for the checked out entries in + the index file. + +-q|--quiet:: + be quiet if files exist or are not in the index + +-f|--force:: + forces overwrite of existing files + +-a|--all:: + checks out all files in the index. Cannot be used + together with explicit filenames. + +-n|--no-create:: + Don't checkout new files, only refresh files already checked + out. + +--prefix=<string>:: + When creating files, prepend <string> (usually a directory + including a trailing /) + +--stage=<number>|all:: + Instead of checking out unmerged entries, copy out the + files from named stage. <number> must be between 1 and 3. + Note: --stage=all automatically implies --temp. + +--temp:: + Instead of copying the files to the working directory + write the content to temporary files. The temporary name + associations will be written to stdout. + +--stdin:: + Instead of taking list of paths from the command line, + read list of paths from the standard input. Paths are + separated by LF (i.e. one path per line) by default. + +-z:: + Only meaningful with `--stdin`; paths are separated with + NUL character instead of LF. + +\--:: + Do not interpret any more arguments as options. + +The order of the flags used to matter, but not anymore. + +Just doing `git-checkout-index` does nothing. You probably meant +`git-checkout-index -a`. And if you want to force it, you want +`git-checkout-index -f -a`. + +Intuitiveness is not the goal here. Repeatability is. The reason for +the "no arguments means no work" behavior is that from scripts you are +supposed to be able to do: + +---------------- +$ find . -name '*.h' -print0 | xargs -0 git-checkout-index -f -- +---------------- + +which will force all existing `*.h` files to be replaced with their +cached copies. If an empty command line implied "all", then this would +force-refresh everything in the index, which was not the point. But +since git-checkout-index accepts --stdin it would be faster to use: + +---------------- +$ find . -name '*.h' -print0 | git-checkout-index -f -z --stdin +---------------- + +The `--` is just a good idea when you know the rest will be filenames; +it will prevent problems with a filename of, for example, `-a`. +Using `--` is probably a good policy in scripts. + + +Using --temp or --stage=all +--------------------------- +When `--temp` is used (or implied by `--stage=all`) +`git-checkout-index` will create a temporary file for each index +entry being checked out. The index will not be updated with stat +information. These options can be useful if the caller needs all +stages of all unmerged entries so that the unmerged files can be +processed by an external merge tool. + +A listing will be written to stdout providing the association of +temporary file names to tracked path names. The listing format +has two variations: + + . tempname TAB path RS ++ +The first format is what gets used when `--stage` is omitted or +is not `--stage=all`. The field tempname is the temporary file +name holding the file content and path is the tracked path name in +the index. Only the requested entries are output. + + . stage1temp SP stage2temp SP stage3tmp TAB path RS ++ +The second format is what gets used when `--stage=all`. The three +stage temporary fields (stage1temp, stage2temp, stage3temp) list the +name of the temporary file if there is a stage entry in the index +or `.` if there is no stage entry. Paths which only have a stage 0 +entry will always be omitted from the output. + +In both formats RS (the record separator) is newline by default +but will be the null byte if -z was passed on the command line. +The temporary file names are always safe strings; they will never +contain directory separators or whitespace characters. The path +field is always relative to the current directory and the temporary +file names are always relative to the top level directory. + +If the object being copied out to a temporary file is a symbolic +link the content of the link will be written to a normal file. It is +up to the end-user or the Porcelain to make use of this information. + + +EXAMPLES +-------- +To update and refresh only the files already checked out:: ++ +---------------- +$ git-checkout-index -n -f -a && git-update-index --ignore-missing --refresh +---------------- + +Using `git-checkout-index` to "export an entire tree":: + The prefix ability basically makes it trivial to use + `git-checkout-index` as an "export as tree" function. + Just read the desired tree into the index, and do: ++ +---------------- +$ git-checkout-index --prefix=git-export-dir/ -a +---------------- ++ +`git-checkout-index` will "export" the index into the specified +directory. ++ +The final "/" is important. The exported name is literally just +prefixed with the specified string. Contrast this with the +following example. + +Export files with a prefix:: ++ +---------------- +$ git-checkout-index --prefix=.merged- Makefile +---------------- ++ +This will check out the currently cached copy of `Makefile` +into the file `.merged-Makefile`. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + + +Documentation +-------------- +Documentation by David Greaves, +Junio C Hamano and the git-list <git@vger.kernel.org>. + + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt new file mode 100644 index 0000000000..55c9289438 --- /dev/null +++ b/Documentation/git-checkout.txt @@ -0,0 +1,211 @@ +git-checkout(1) +=============== + +NAME +---- +git-checkout - Checkout and switch to a branch + +SYNOPSIS +-------- +[verse] +'git-checkout' [-q] [-f] [-b <new_branch> [-l]] [-m] [<branch>] +'git-checkout' [<tree-ish>] <paths>... + +DESCRIPTION +----------- + +When <paths> are not given, this command switches branches by +updating the index and working tree to reflect the specified +branch, <branch>, and updating HEAD to be <branch> or, if +specified, <new_branch>. Using -b will cause <new_branch> to +be created. + +When <paths> are given, this command does *not* switch +branches. It updates the named paths in the working tree from +the index file (i.e. it runs `git-checkout-index -f -u`), or a +named commit. In +this case, `-f` and `-b` options are meaningless and giving +either of them results in an error. <tree-ish> argument can be +used to specify a specific tree-ish (i.e. commit, tag or tree) +to update the index for the given paths before updating the +working tree. + + +OPTIONS +------- +-q:: + Quiet, supress feedback messages. + +-f:: + Force a re-read of everything. + +-b:: + Create a new branch named <new_branch> and start it at + <branch>. The new branch name must pass all checks defined + 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 + which you are switching, the command refuses to switch + branches in order to preserve your modifications in context. + However, with this option, a three-way merge between the current + branch, your working tree contents, and the new branch + is done, and you will be on the new branch. ++ +When a merge conflict happens, the index entries for conflicting +paths are left unmerged, and you need to resolve the conflicts +and mark the resolved paths with `git update-index`. + +<new_branch>:: + Name for the new branch. + +<branch>:: + Branch to checkout; may be any object ID that resolves to a + commit. Defaults to HEAD. ++ +When this parameter names a non-branch (but still a valid commit object), +your HEAD becomes 'detached'. + + +Detached HEAD +------------- + +It is sometimes useful to be able to 'checkout' a commit that is +not at the tip of one of your branches. The most obvious +example is to check out the commit at a tagged official release +point, like this: + +------------ +$ git checkout v2.6.18 +------------ + +Earlier versions of git did not allow this and asked you to +create a temporary branch using `-b` option, but starting from +version 1.5.0, the above command 'detaches' your HEAD from the +current branch and directly point at the commit named by the tag +(`v2.6.18` in the above example). + +You can use usual git commands while in this state. You can use +`git-reset --hard $othercommit` to further move around, for +example. You can make changes and create a new commit on top of +a detached HEAD. You can even create a merge by using `git +merge $othercommit`. + +The state you are in while your HEAD is detached is not recorded +by any branch (which is natural --- you are not on any branch). +What this means is that you can discard your temporary commits +and merges by switching back to an existing branch (e.g. `git +checkout master`), and a later `git prune` or `git gc` would +garbage-collect them. + +The command would refuse to switch back to make sure that you do +not discard your temporary state by mistake when your detached +HEAD is not pointed at by any existing ref. If you did want to +save your state (e.g. "I was interested in the fifth commit from +the top of 'master' branch", or "I made two commits to fix minor +bugs while on a detached HEAD" -- and if you do not want to lose +these facts), you can create a new branch and switch to it with +`git checkout -b newbranch` so that you can keep building on +that state, or tag it first so that you can come back to it +later and switch to the branch you wanted to switch to with `git +tag that_state; git checkout master`. On the other hand, if you +did want to discard the temporary state, you can give `-f` +option (e.g. `git checkout -f master`) to override this +behaviour. + + +EXAMPLES +-------- + +. The following sequence checks out the `master` branch, reverts +the `Makefile` to two revisions back, deletes hello.c by +mistake, and gets it back from the index. ++ +------------ +$ git checkout master <1> +$ git checkout master~2 Makefile <2> +$ rm -f hello.c +$ git checkout hello.c <3> +------------ ++ +<1> switch branch +<2> take out a file out of other commit +<3> restore hello.c from HEAD of current branch ++ +If you have an unfortunate branch that is named `hello.c`, this +step would be confused as an instruction to switch to that branch. +You should instead write: ++ +------------ +$ git checkout -- hello.c +------------ + +. After working in a wrong branch, switching to the correct +branch would be done using: ++ +------------ +$ git checkout mytopic +------------ ++ +However, your "wrong" branch and correct "mytopic" branch may +differ in files that you have locally modified, in which case, +the above checkout would fail like this: ++ +------------ +$ git checkout mytopic +fatal: Entry 'frotz' not uptodate. Cannot merge. +------------ ++ +You can give the `-m` flag to the command, which would try a +three-way merge: ++ +------------ +$ git checkout -m mytopic +Auto-merging frotz +------------ ++ +After this three-way merge, the local modifications are _not_ +registered in your index file, so `git diff` would show you what +changes you made since the tip of the new branch. + +. When a merge conflict happens during switching branches with +the `-m` option, you would see something like this: ++ +------------ +$ git checkout -m mytopic +Auto-merging frotz +merge: warning: conflicts during merge +ERROR: Merge conflict in frotz +fatal: merge program failed +------------ ++ +At this point, `git diff` shows the changes cleanly merged as in +the previous example, as well as the changes in the conflicted +files. Edit and resolve the conflict and mark it resolved with +`git update-index` as usual: ++ +------------ +$ edit frotz +$ git update-index frotz +------------ + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-cherry-pick.txt b/Documentation/git-cherry-pick.txt new file mode 100644 index 0000000000..3149d08da8 --- /dev/null +++ b/Documentation/git-cherry-pick.txt @@ -0,0 +1,71 @@ +git-cherry-pick(1) +================== + +NAME +---- +git-cherry-pick - Apply the change introduced by an existing commit + +SYNOPSIS +-------- +'git-cherry-pick' [--edit] [-n] [-x] <commit> + +DESCRIPTION +----------- +Given one existing commit, apply the change the patch introduces, and record a +new commit that records it. This requires your working tree to be clean (no +modifications from the HEAD commit). + +OPTIONS +------- +<commit>:: + Commit to cherry-pick. + For a more complete list of ways to spell commits, see + "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1]. + +-e|--edit:: + With this option, `git-cherry-pick` will let you edit the commit + message prior committing. + +-x:: + Cause the command to append which commit was + cherry-picked after the original commit message when + making a commit. Do not use this option if you are + cherry-picking from your private branch because the + information is useless to the recipient. If on the + other hand you are cherry-picking between two publicly + visible branches (e.g. backporting a fix to a + maintenance branch for an older release from a + development branch), adding this information can be + useful. + +-r|--replay:: + It used to be that the command defaulted to do `-x` + described above, and `-r` was to disable it. Now the + default is not to do `-x` so this option is a no-op. + +-n|--no-commit:: + Usually the command automatically creates a commit with + a commit log message stating which commit was + cherry-picked. This flag applies the change necessary + to cherry-pick the named commit to your working tree, + but does not make the commit. In addition, when this + option is used, your working tree does not have to match + the HEAD commit. The cherry-pick is done against the + beginning state of your working tree. ++ +This is useful when cherry-picking more than one commits' +effect to your working tree in a row. + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-cherry.txt b/Documentation/git-cherry.txt new file mode 100644 index 0000000000..27b67b81a5 --- /dev/null +++ b/Documentation/git-cherry.txt @@ -0,0 +1,67 @@ +git-cherry(1) +============= + +NAME +---- +git-cherry - Find commits not merged upstream + +SYNOPSIS +-------- +'git-cherry' [-v] <upstream> [<head>] [<limit>] + +DESCRIPTION +----------- +The changeset (or "diff") of each commit between the fork-point and <head> +is compared against each commit between the fork-point and <upstream>. + +Every commit that doesn't exist in the <upstream> branch +has its id (sha1) reported, prefixed by a symbol. The ones that have +equivalent change already +in the <upstream> branch are prefixed with a minus (-) sign, and those +that only exist in the <head> branch are prefixed with a plus (+) symbol: + + __*__*__*__*__> <upstream> + / + fork-point + \__+__+__-__+__+__-__+__> <head> + + +If a <limit> has been given then the commits along the <head> branch up +to and including <limit> are not reported: + + __*__*__*__*__> <upstream> + / + fork-point + \__*__*__<limit>__-__+__> <head> + + +Because git-cherry compares the changeset rather than the commit id +(sha1), you can use git-cherry to find out if a commit you made locally +has been applied <upstream> under a different commit id. For example, +this will happen if you're feeding patches <upstream> via email rather +than pushing or pulling commits directly. + + +OPTIONS +------- +-v:: + Verbose. + +<upstream>:: + Upstream branch to compare against. + +<head>:: + Working branch; defaults to HEAD. + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-clean.txt b/Documentation/git-clean.txt new file mode 100644 index 0000000000..c61afbcdba --- /dev/null +++ b/Documentation/git-clean.txt @@ -0,0 +1,53 @@ +git-clean(1) +============ + +NAME +---- +git-clean - Remove untracked files from the working tree + +SYNOPSIS +-------- +[verse] +'git-clean' [-d] [-n] [-q] [-x | -X] [--] <paths>... + +DESCRIPTION +----------- +Removes files unknown to git. This allows to clean the working tree +from files that are not under version control. If the '-x' option is +specified, ignored files are also removed, allowing to remove all +build products. +When optional `<paths>...` arguments are given, the paths +affected are further limited to those that match them. + + +OPTIONS +------- +-d:: + Remove untracked directories in addition to untracked files. + +-n:: + Don't actually remove anything, just show what would be done. + +-q:: + Be quiet, only report errors, but not the files that are + successfully removed. + +-x:: + Don't use the ignore rules. This allows removing all untracked + files, including build products. This can be used (possibly in + conjunction with gitlink:git-reset[1]) to create a pristine + working directory to test a clean build. + +-X:: + Remove only files ignored by git. This may be useful to rebuild + everything from scratch, but keep manually created files. + + +Author +------ +Written by Pavel Roskin <proski@gnu.org> + + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt new file mode 100644 index 0000000000..707376f22c --- /dev/null +++ b/Documentation/git-clone.txt @@ -0,0 +1,178 @@ +git-clone(1) +============ + +NAME +---- +git-clone - Clones a repository into a new directory + + +SYNOPSIS +-------- +[verse] +'git-clone' [--template=<template_directory>] [-l [-s]] [-q] [-n] [--bare] + [-o <name>] [-u <upload-pack>] [--reference <repository>] + [--depth=<depth>] <repository> [<directory>] + +DESCRIPTION +----------- + +Clones a repository into a newly created directory, creates +remote-tracking branches for each branch in the cloned repository +(visible using `git branch -r`), and creates and checks out an initial +branch equal to the cloned repository's currently active branch. + +After the clone, a plain `git fetch` without arguments will update +all the remote-tracking branches, and a `git pull` without +arguments will in addition merge the remote master branch into the +current master branch, if any. + +This default configuration is achieved by creating references to +the remote branch heads under `$GIT_DIR/refs/remotes/origin` and +by initializing `remote.origin.url` and `remote.origin.fetch` +configuration variables. + + +OPTIONS +------- +--local:: +-l:: + When the repository to clone from is on a local machine, + this flag bypasses normal "git aware" transport + mechanism and clones the repository by making a copy of + HEAD and everything under objects and refs directories. + The files under .git/objects/ directory are hardlinked + to save space when possible. + +--shared:: +-s:: + When the repository to clone is on the local machine, + instead of using hard links, automatically setup + .git/objects/info/alternates to share the objects + with the source repository. The resulting repository + starts out without any object of its own. + +--reference <repository>:: + If the reference repository is on the local machine + automatically setup .git/objects/info/alternates to + obtain objects from the reference repository. Using + an already existing repository as an alternate will + require less objects to be copied from the repository + being cloned, reducing network and local storage costs. + +--quiet:: +-q:: + Operate quietly. This flag is passed to "rsync" and + "git-fetch-pack" commands when given. + +-n:: + No checkout of HEAD is performed after the clone is complete. + +--bare:: + Make a 'bare' GIT repository. That is, instead of + creating `<directory>` and placing the administrative + files in `<directory>/.git`, make the `<directory>` + itself the `$GIT_DIR`. This obviously implies the `-n` + because there is nowhere to check out the working tree. + Also the branch heads at the remote are copied directly + to corresponding local branch heads, without mapping + them to `refs/remotes/origin/`. When this option is + used, neither remote-tracking branches nor the related + configuration variables are created. + +--origin <name>:: +-o <name>:: + Instead of using the remote name 'origin' to keep track + of the upstream repository, use <name> instead. + +--upload-pack <upload-pack>:: +-u <upload-pack>:: + When given, and the repository to clone from is handled + by 'git-fetch-pack', '--exec=<upload-pack>' is passed to + 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`. + +--depth=<depth>:: + Create a 'shallow' clone with a history truncated to the + specified number of revs. A shallow repository has + number of limitations (you cannot clone or fetch from + it, nor push from nor into it), but is adequate if you + want to only look at near the tip of a large project + with a long history, and would want to send in a fixes + as patches. + +<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" + 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 + is not allowed. + +Examples +-------- + +Clone from upstream:: ++ +------------ +$ git clone git://git.kernel.org/pub/scm/.../linux-2.6 my2.6 +$ cd my2.6 +$ make +------------ + + +Make a local clone that borrows from the current directory, without checking things out:: ++ +------------ +$ git clone -l -s -n . ../copy +$ cd copy +$ git show-branch +------------ + + +Clone from upstream while borrowing from an existing local directory:: ++ +------------ +$ git clone --reference my2.6 \ + git://git.kernel.org/pub/scm/.../linux-2.7 \ + my2.7 +$ cd my2.7 +------------ + + +Create a bare repository to publish your changes to the public:: ++ +------------ +$ git clone --bare -l /home/proj/.git /pub/scm/proj.git +------------ + + +Create a repository on the kernel.org machine that borrows from Linus:: ++ +------------ +$ git clone --bare -l -s /pub/scm/.../torvalds/linux-2.6.git \ + /pub/scm/.../me/subsys-2.6.git +------------ + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-commit-tree.txt b/Documentation/git-commit-tree.txt new file mode 100644 index 0000000000..cf25507f8f --- /dev/null +++ b/Documentation/git-commit-tree.txt @@ -0,0 +1,108 @@ +git-commit-tree(1) +================== + +NAME +---- +git-commit-tree - Create a new commit object + + +SYNOPSIS +-------- +'git-commit-tree' <tree> [-p <parent commit>]\* < changelog + +DESCRIPTION +----------- +This is usually not what an end user wants to run directly. See +gitlink:git-commit[1] instead. + +Creates a new commit object based on the provided tree object and +emits the new commit object id on stdout. If no parent is given then +it is considered to be an initial tree. + +A commit object usually has 1 parent (a commit after a change) or up +to 16 parents. More than one parent represents a merge of branches +that led to them. + +While a tree represents a particular directory state of a working +directory, a commit represents that state in "time", and explains how +to get there. + +Normally a commit would identify a new "HEAD" state, and while git +doesn't care where you save the note about that state, in practice we +tend to just write the result to the file that is pointed at by +`.git/HEAD`, so that we can always see what the last committed +state was. + +OPTIONS +------- +<tree>:: + An existing tree object + +-p <parent commit>:: + Each '-p' indicates the id of a parent commit object. + + +Commit Information +------------------ + +A commit encapsulates: + +- all parent object ids +- author name, email and date +- committer name and email and the commit time. + +If not provided, "git-commit-tree" uses your name, hostname and domain to +provide author and committer info. This can be overridden by +either `.git/config` file, or using the following environment variables. + + GIT_AUTHOR_NAME + GIT_AUTHOR_EMAIL + GIT_AUTHOR_DATE + GIT_COMMITTER_NAME + GIT_COMMITTER_EMAIL + +(nb "<", ">" and "\n"s are stripped) + +In `.git/config` file, the following items are used for GIT_AUTHOR_NAME and +GIT_AUTHOR_EMAIL: + + [user] + name = "Your Name" + email = "your@email.address.xz" + +A commit comment is read from stdin (max 999 chars). If a changelog +entry is not provided via "<" redirection, "git-commit-tree" will just wait +for one to be entered and terminated with ^D. + + +Diagnostics +----------- +You don't exist. Go away!:: + The passwd(5) gecos field couldn't be read +Your parents must have hated you!:: + The password(5) gecos field is longer than a giant static buffer. +Your sysadmin must hate you!:: + The password(5) name field is longer than a giant static buffer. + +Discussion +---------- + +include::i18n.txt[] + +See Also +-------- +gitlink:git-write-tree[1] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-commit.txt b/Documentation/git-commit.txt new file mode 100644 index 0000000000..2187eee416 --- /dev/null +++ b/Documentation/git-commit.txt @@ -0,0 +1,257 @@ +git-commit(1) +============= + +NAME +---- +git-commit - Record changes to the repository + +SYNOPSIS +-------- +[verse] +'git-commit' [-a] [-s] [-v] [(-c | -C) <commit> | -F <file> | -m <msg> | + --amend] [--no-verify] [-e] [--author <author>] + [--] [[-i | -o ]<file>...] + +DESCRIPTION +----------- +Use 'git commit' when you want to record your changes into the repository +along with a log message describing what the commit is about. All changes +to be committed must be explicitly identified using one of the following +methods: + +1. by using gitlink:git-add[1] to incrementally "add" changes to the + next commit before using the 'commit' command (Note: even modified + files must be "added"); + +2. by using gitlink:git-rm[1] to identify content removal for the next + commit, again before using the 'commit' command; + +3. by directly listing files containing changes to be committed as arguments + to the 'commit' command, in which cases only those files alone will be + considered for the commit; + +4. by using the -a switch with the 'commit' command to automatically "add" + changes from all known files i.e. files that have already been committed + before, and to automatically "rm" files that have been + removed from the working tree, and perform the actual commit. + +The gitlink:git-status[1] command can be used to obtain a +summary of what is included by any of the above for the next +commit by giving the same set of parameters you would give to +this command. + +If you make a commit and then found a mistake immediately after +that, you can recover from it with gitlink:git-reset[1]. + + +OPTIONS +------- +-a|--all:: + Tell the command to automatically stage files that have + been modified and deleted, but new files you have not + told git about are not affected. + +-c or -C <commit>:: + Take existing commit object, and reuse the log message + and the authorship information (including the timestamp) + when creating the commit. With '-C', the editor is not + invoked; with '-c' the user can further edit the commit + message. + +-F <file>:: + Take the commit message from the given file. Use '-' to + read the message from the standard input. + +--author <author>:: + Override the author name used in the commit. Use + `A U Thor <author@example.com>` format. + +-m <msg>:: + Use the given <msg> as the commit message. + +-s|--signoff:: + Add Signed-off-by line at the end of the commit message. + +--no-verify:: + This option bypasses the pre-commit hook. + See also link:hooks.html[hooks]. + +-e|--edit:: + The message taken from file with `-F`, command line with + `-m`, and from file with `-C` are usually used as the + commit log message unmodified. This option lets you + further edit the message taken from these sources. + +--amend:: + + Used to amend the tip of the current branch. Prepare the tree + object you would want to replace the latest commit as usual + (this includes the usual -i/-o and explicit paths), and the + commit log editor is seeded with the commit message from the + tip of the current branch. The commit you create replaces the + current tip -- if it was a merge, it will have the parents of + the current tip as parents -- so the current top commit is + discarded. ++ +-- +It is a rough equivalent for: +------ + $ git reset --soft HEAD^ + $ ... do something else to come up with the right tree ... + $ git commit -c ORIG_HEAD + +------ +but can be used to amend a merge commit. +-- + +-i|--include:: + Before making a commit out of staged contents so far, + stage the contents of paths given on the command line + as well. This is usually not what you want unless you + are concluding a conflicted merge. + +-q|--quiet:: + Suppress commit summary message. + +\--:: + Do not interpret any more arguments as options. + +<file>...:: + When files are given on the command line, the command + commits the contents of the named files, without + recording the changes already staged. The contents of + these files are also staged for the next commit on top + of what have been staged before. + + +EXAMPLES +-------- +When recording your own work, the contents of modified files in +your working tree are temporarily stored to a staging area +called the "index" with gitlink:git-add[1]. Removal +of a file is staged with gitlink:git-rm[1]. After building the +state to be committed incrementally with these commands, `git +commit` (without any pathname parameter) is used to record what +has been staged so far. This is the most basic form of the +command. An example: + +------------ +$ edit hello.c +$ git rm goodbye.c +$ git add hello.c +$ git commit +------------ + +Instead of staging files after each individual change, you can +tell `git commit` to notice the changes to the files whose +contents are tracked in +your working tree and do corresponding `git add` and `git rm` +for you. That is, this example does the same as the earlier +example if there is no other change in your working tree: + +------------ +$ edit hello.c +$ rm goodbye.c +$ git commit -a +------------ + +The command `git commit -a` first looks at your working tree, +notices that you have modified hello.c and removed goodbye.c, +and performs necessary `git add` and `git rm` for you. + +After staging changes to many files, you can alter the order the +changes are recorded in, by giving pathnames to `git commit`. +When pathnames are given, the command makes a commit that +only records the changes made to the named paths: + +------------ +$ edit hello.c hello.h +$ git add hello.c hello.h +$ edit Makefile +$ git commit Makefile +------------ + +This makes a commit that records the modification to `Makefile`. +The changes staged for `hello.c` and `hello.h` are not included +in the resulting commit. However, their changes are not lost -- +they are still staged and merely held back. After the above +sequence, if you do: + +------------ +$ git commit +------------ + +this second commit would record the changes to `hello.c` and +`hello.h` as expected. + +After a merge (initiated by either gitlink:git-merge[1] or +gitlink:git-pull[1]) stops because of conflicts, cleanly merged +paths are already staged to be committed for you, and paths that +conflicted are left in unmerged state. You would have to first +check which paths are conflicting with gitlink:git-status[1] +and after fixing them manually in your working tree, you would +stage the result as usual with gitlink:git-add[1]: + +------------ +$ git status | grep unmerged +unmerged: hello.c +$ edit hello.c +$ git add hello.c +------------ + +After resolving conflicts and staging the result, `git ls-files -u` +would stop mentioning the conflicted path. When you are done, +run `git commit` to finally record the merge: + +------------ +$ git commit +------------ + +As with the case to record your own changes, you can use `-a` +option to save typing. One difference is that during a merge +resolution, you cannot use `git commit` with pathnames to +alter the order the changes are committed, because the merge +should be recorded as a single commit. In fact, the command +refuses to run when given pathnames (but see `-i` option). + + +DISCUSSION +---------- + +Though not required, it's a good idea to begin the commit message +with a single short (less than 50 character) line summarizing the +change, followed by a blank line and then a more thorough description. +Tools that turn commits into email, for example, use the first line +on the Subject: line and the rest of the commit in the body. + +include::i18n.txt[] + +ENVIRONMENT VARIABLES +--------------------- +The command specified by either the VISUAL or EDITOR environment +variables is used to edit the commit log message. + +HOOKS +----- +This command can run `commit-msg`, `pre-commit`, and +`post-commit` hooks. See link:hooks.html[hooks] for more +information. + + +SEE ALSO +-------- +gitlink:git-add[1], +gitlink:git-rm[1], +gitlink:git-mv[1], +gitlink:git-merge[1], +gitlink:git-commit-tree[1] + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and +Junio C Hamano <junkio@cox.net> + + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt new file mode 100644 index 0000000000..6624484fe1 --- /dev/null +++ b/Documentation/git-config.txt @@ -0,0 +1,227 @@ +git-config(1) +============= + +NAME +---- +git-config - Get and set repository or global options + + +SYNOPSIS +-------- +[verse] +'git-config' [--global] [type] name [value [value_regex]] +'git-config' [--global] [type] --add name value +'git-config' [--global] [type] --replace-all name [value [value_regex]] +'git-config' [--global] [type] --get name [value_regex] +'git-config' [--global] [type] --get-all name [value_regex] +'git-config' [--global] [type] --unset name [value_regex] +'git-config' [--global] [type] --unset-all name [value_regex] +'git-config' [--global] -l | --list + +DESCRIPTION +----------- +You can query/set/replace/unset options with this command. The name is +actually the section and the key separated by a dot, and the value will be +escaped. + +Multiple lines can be added to an option by using the '--add' option. +If you want to update or unset an option which can occur on multiple +lines, a POSIX regexp `value_regex` needs to be given. Only the +existing values that match the regexp are updated or unset. If +you want to handle the lines that do *not* match the regex, just +prepend a single exclamation mark in front (see EXAMPLES). + +The type specifier can be either '--int' or '--bool', which will make +'git-config' ensure that the variable(s) are of the given type and +convert the value to the canonical form (simple decimal number for int, +a "true" or "false" string for bool). If no type specifier is passed, +no checks or transformations are performed on the value. + +This command will fail if: + +. The .git/config file is invalid, +. Can not write to .git/config, +. no section was provided, +. the section or key is invalid, +. you try to unset an option which does not exist, +. you try to unset/set an option for which multiple lines match, or +. you use --global option without $HOME being properly set. + + +OPTIONS +------- + +--replace-all:: + Default behavior is to replace at most one line. This replaces + all lines matching the key (and optionally the value_regex). + +--add:: + Adds a new line to the option without altering any existing + values. This is the same as providing '^$' as the value_regex. + +--get:: + Get the value for a given key (optionally filtered by a regex + matching the value). Returns error code 1 if the key was not + found and error code 2 if multiple key values were found. + +--get-all:: + Like get, but does not fail if the number of values for the key + is not exactly one. + +--get-regexp:: + Like --get-all, but interprets the name as a regular expression. + +--global:: + Use global ~/.gitconfig file rather than the repository .git/config. + +--unset:: + Remove the line matching the key from config file. + +--unset-all:: + Remove all matching lines from config file. + +-l, --list:: + List all variables set in config file. + +--bool:: + git-config will ensure that the output is "true" or "false" + +--int:: + git-config will ensure that the output is a simple + decimal number. An optional value suffix of 'k', 'm', or 'g' + in the config file will cause the value to be multiplied + by 1024, 1048576, or 1073741824 prior to output. + + +ENVIRONMENT +----------- + +GIT_CONFIG:: + Take the configuration from the given file instead of .git/config. + Using the "--global" option forces this to ~/.gitconfig. + +GIT_CONFIG_LOCAL:: + Currently the same as $GIT_CONFIG; when Git will support global + configuration files, this will cause it to take the configuration + from the global configuration file in addition to the given file. + + +EXAMPLE +------- + +Given a .git/config like this: + + # + # This is the config file, and + # a '#' or ';' character indicates + # a comment + # + + ; core variables + [core] + ; Don't trust file modes + filemode = false + + ; Our diff algorithm + [diff] + external = "/usr/local/bin/gnu-diff -u" + renames = true + + ; Proxy settings + [core] + gitproxy="ssh" for "ssh://kernel.org/" + gitproxy="proxy-command" for kernel.org + gitproxy="myprotocol-command" for "my://" + gitproxy=default-proxy ; for all the rest + +you can set the filemode to true with + +------------ +% git config core.filemode true +------------ + +The hypothetical proxy command entries actually have a postfix to discern +what URL they apply to. Here is how to change the entry for kernel.org +to "ssh". + +------------ +% git config core.gitproxy '"ssh" for kernel.org' 'for kernel.org$' +------------ + +This makes sure that only the key/value pair for kernel.org is replaced. + +To delete the entry for renames, do + +------------ +% git config --unset diff.renames +------------ + +If you want to delete an entry for a multivar (like core.gitproxy above), +you have to provide a regex matching the value of exactly one line. + +To query the value for a given key, do + +------------ +% git config --get core.filemode +------------ + +or + +------------ +% git config core.filemode +------------ + +or, to query a multivar: + +------------ +% git config --get core.gitproxy "for kernel.org$" +------------ + +If you want to know all the values for a multivar, do: + +------------ +% git config --get-all core.gitproxy +------------ + +If you like to live dangerous, you can replace *all* core.gitproxy by a +new one with + +------------ +% git config --replace-all core.gitproxy ssh +------------ + +However, if you really only want to replace the line for the default proxy, +i.e. the one without a "for ..." postfix, do something like this: + +------------ +% git config core.gitproxy ssh '! for ' +------------ + +To actually match only values with an exclamation mark, you have to + +------------ +% git config section.key value '[!]' +------------ + +To add a new proxy, without altering any of the existing ones, use + +------------ +% git config core.gitproxy '"proxy" for example.com' +------------ + + +include::config.txt[] + + +Author +------ +Written by Johannes Schindelin <Johannes.Schindelin@gmx.de> + +Documentation +-------------- +Documentation by Johannes Schindelin, Petr Baudis and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-convert-objects.txt b/Documentation/git-convert-objects.txt new file mode 100644 index 0000000000..b1220c06e1 --- /dev/null +++ b/Documentation/git-convert-objects.txt @@ -0,0 +1,29 @@ +git-convert-objects(1) +====================== + +NAME +---- +git-convert-objects - Converts old-style git repository + + +SYNOPSIS +-------- +'git-convert-objects' + +DESCRIPTION +----------- +Converts old-style git repository to the latest format + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-count-objects.txt b/Documentation/git-count-objects.txt new file mode 100644 index 0000000000..91c8c92c76 --- /dev/null +++ b/Documentation/git-count-objects.txt @@ -0,0 +1,38 @@ +git-count-objects(1) +==================== + +NAME +---- +git-count-objects - Count unpacked number of objects and their disk consumption + +SYNOPSIS +-------- +'git-count-objects' [-v] + +DESCRIPTION +----------- +This counts the number of unpacked object files and disk space consumed by +them, to help you decide when it is a good time to repack. + + +OPTIONS +------- +-v:: + In addition to the number of loose objects and disk + space consumed, it reports the number of in-pack + objects, number of packs, and number of objects that can be + removed by running `git-prune-packed`. + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-cvsexportcommit.txt b/Documentation/git-cvsexportcommit.txt new file mode 100644 index 0000000000..27d531b888 --- /dev/null +++ b/Documentation/git-cvsexportcommit.txt @@ -0,0 +1,90 @@ +git-cvsexportcommit(1) +====================== + +NAME +---- +git-cvsexportcommit - Export a single commit to a CVS checkout + + +SYNOPSIS +-------- +'git-cvsexportcommit' [-h] [-v] [-c] [-P] [-p] [-a] [-f] [-m msgprefix] [PARENTCOMMIT] COMMITID + + +DESCRIPTION +----------- +Exports a commit from GIT to a CVS checkout, making it easier +to merge patches from a git repository into a CVS repository. + +Execute it from the root of the CVS working copy. GIT_DIR must be defined. +See examples below. + +It does its best to do the safe thing, it will check that the files are +unchanged and up to date in the CVS checkout, and it will not autocommit +by default. + +Supports file additions, removals, and commits that affect binary files. + +If the commit is a merge commit, you must tell git-cvsapplycommit what parent +should the changeset be done against. + +OPTIONS +------- + +-c:: + Commit automatically if the patch applied cleanly. It will not + commit if any hunks fail to apply or there were other problems. + +-p:: + Be pedantic (paranoid) when applying patches. Invokes patch with + --fuzz=0 + +-a:: + Add authorship information. Adds Author line, and Committer (if + different from Author) to the message. + +-f:: + Force the merge even if the files are not up to date. + +-P:: + Force the parent commit, even if it is not a direct parent. + +-m:: + Prepend the commit message with the provided prefix. + Useful for patch series and the like. + +-v:: + Verbose. + +EXAMPLES +-------- + +Merge one patch into CVS:: ++ +------------ +$ export GIT_DIR=~/project/.git +$ cd ~/project_cvs_checkout +$ git-cvsexportcommit -v <commit-sha1> +$ cvs commit -F .mgs <files> +------------ + +Merge pending patches into CVS automatically -- only if you really know what you are doing :: ++ +------------ +$ export GIT_DIR=~/project/.git +$ cd ~/project_cvs_checkout +$ git-cherry cvshead myhead | sed -n 's/^+ //p' | xargs -l1 git-cvsexportcommit -c -p -v +------------ + +Author +------ +Written by Martin Langhoff <martin@catalyst.net.nz> + +Documentation +-------------- +Documentation by Martin Langhoff <martin@catalyst.net.nz> + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-cvsimport.txt b/Documentation/git-cvsimport.txt new file mode 100644 index 0000000000..f5450de74a --- /dev/null +++ b/Documentation/git-cvsimport.txt @@ -0,0 +1,154 @@ +git-cvsimport(1) +================ + +NAME +---- +git-cvsimport - Salvage your data out of another SCM people love to hate + + +SYNOPSIS +-------- +[verse] +'git-cvsimport' [-o <branch-for-HEAD>] [-h] [-v] [-d <CVSROOT>] [-s <subst>] + [-p <options-for-cvsps>] [-C <git_repository>] [-i] [-P <file>] + [-m] [-M regex] [<CVS_module>] + + +DESCRIPTION +----------- +Imports a CVS repository into git. It will either create a new +repository, or incrementally import into an existing one. + +Splitting the CVS log into patch sets is done by 'cvsps'. +At least version 2.1 is required. + +You should *never* do any work of your own on the branches that are +created by git-cvsimport. The initial import will create and populate a +"master" branch from the CVS repository's main branch which you're free +to work with; after that, you need to 'git merge' incremental imports, or +any CVS branches, yourself. + +OPTIONS +------- +-d <CVSROOT>:: + The root of the CVS archive. May be local (a simple path) or remote; + 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 index remain untouched and will + not create them if they do not exist. + +-k:: + Kill keywords: will extract files with -kk from the CVS archive + to avoid noisy changesets. Highly recommended, but off by default + to preserve compatibility with early imported trees. + +-u:: + Convert underscores in tag and branch names to dots. + +-o <branch-for-HEAD>:: + The 'HEAD' branch from CVS is imported to the 'origin' branch within + the git repository, as 'HEAD' already has a special meaning for git. + Use this option if you want to import into a different branch. ++ +Use '-o master' for continuing an import that was initially done by +the old cvs2git tool. + +-p <options-for-cvsps>:: + Additional options for cvsps. + 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. + +-P <cvsps-output-file>:: + Instead of calling cvsps, read the provided cvsps output file. Useful + for debugging or when cvsps is being handled outside cvsimport. + +-m:: + Attempt to detect merges based on the commit message. This option + will enable default regexes that try to capture the name source + branch name from the commit message. + +-M <regex>:: + Attempt to detect merges based on the commit message with a custom + regex. It can be used with -m to also see the default regexes. + You must escape forward slashes. + +-v:: + Verbosity: let 'cvsimport' report what it is doing. + +<CVS_module>:: + The CVS module you want to import. Relative to <CVSROOT>. + +-h:: + Print a short usage message and exit. + +-z <fuzz>:: + Pass the timestamp fuzz factor to cvsps, in seconds. If unset, + cvsps defaults to 300s. + +-s <subst>:: + Substitute the character "/" in branch names with <subst> + +-A <author-conv-file>:: + CVS by default uses the Unix username when writing its + commit logs. Using this option and an author-conv-file + in this format + +-a:: + Import all commits, including recent ones. cvsimport by default + skips commits that have a timestamp less than 10 minutes ago. + +-S <regex>:: + Skip paths matching the regex. + +-L <limit>:: + Limit the number of commits imported. Workaround for cases where + cvsimport leaks memory. + ++ +--------- + exon=Andreas Ericsson <ae@op5.se> + spawn=Simon Pawn <spawn@frog-pond.org> + +--------- ++ +git-cvsimport will make it appear as those authors had +their GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL set properly +all along. ++ +For convenience, this data is saved to $GIT_DIR/cvs-authors +each time the -A option is provided and read from that same +file each time git-cvsimport is run. ++ +It is not recommended to use this feature if you intend to +export changes back to CVS again later with +gitlink:git-cvsexportcommit[1]. + +OUTPUT +------ +If '-v' is specified, the script reports what it is doing. + +Otherwise, success is indicated the Unix way, i.e. by simply exiting with +a zero exit status. + + +Author +------ +Written by Matthias Urlichs <smurf@smurf.noris.de>, with help from +various participants of the git-list <git@vger.kernel.org>. + +Documentation +-------------- +Documentation by Matthias Urlichs <smurf@smurf.noris.de>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-cvsserver.txt b/Documentation/git-cvsserver.txt new file mode 100644 index 0000000000..e328db3797 --- /dev/null +++ b/Documentation/git-cvsserver.txt @@ -0,0 +1,161 @@ +git-cvsserver(1) +================ + +NAME +---- +git-cvsserver - A CVS server emulator for git + +SYNOPSIS +-------- +[verse] +export CVS_SERVER=git-cvsserver +'cvs' -d :ext:user@server/path/repo.git co <HEAD_name> + +DESCRIPTION +----------- + +This application is a CVS emulation layer for git. + +It is highly functional. However, not all methods are implemented, +and for those methods that are implemented, +not all switches are implemented. + +Testing has been done using both the CLI CVS client, and the Eclipse CVS +plugin. Most functionality works fine with both of these clients. + +LIMITATIONS +----------- + +Currently cvsserver works over SSH connections for read/write clients, and +over pserver for anonymous CVS access. + +CVS clients cannot tag, branch or perform GIT merges. + +INSTALLATION +------------ + +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 + +------ + 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. +-- +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. +-- +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 + +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 +------------------------ + +To get a checkout with the Eclipse CVS client: + +1. Select "Create a new project -> From CVS checkout" +2. Create a new location. See the notes below for details on how to choose the + right protocol. +3. Browse the 'modules' available. It will give you a list of the heads in + the repository. You will not be able to browse the tree from there. Only + the heads. +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 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', +you will definitely want to have SSH keys setup. + +Alternatively, you can just use the non-standard extssh protocol that Eclipse +offer. In that case CVS_SERVER is ignored, and you will have to replace +the cvs utility on the server with git-cvsserver or manipulate your .bashrc +so that calling 'cvs' effectively calls git-cvsserver. + +Clients known to work +--------------------- + +CVS 1.12.9 on Debian +CVS 1.11.17 on MacOSX (from Fink package) +Eclipse 3.0, 3.1.2 on MacOSX (see Eclipse CVS Client Notes) +TortoiseCVS + +Operations supported +-------------------- + +All the operations required for normal use are supported, including +checkout, diff, status, update, log, add, remove, commit. +Legacy monitoring operations are not supported (edit, watch and related). +Exports and tagging (tags and branches) are not supported at this stage. + +The server will set the -k mode to binary when relevant. In proper GIT +tradition, the contents of the files are always respected. +No keyword expansion or newline munging is supported. + +Dependencies +------------ + +git-cvsserver depends on DBD::SQLite. + +Copyright and Authors +--------------------- + +This program is copyright The Open University UK - 2006. + +Authors: Martyn Smith <martyn@catalyst.net.nz> + Martin Langhoff <martin@catalyst.net.nz> + with ideas and patches from participants of the git-list <git@vger.kernel.org>. + +Documentation +-------------- +Documentation by Martyn Smith <martyn@catalyst.net.nz> and Martin Langhoff <martin@catalyst.net.nz> Matthias Urlichs <smurf@smurf.noris.de>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-daemon.txt b/Documentation/git-daemon.txt new file mode 100644 index 0000000000..9ddab71203 --- /dev/null +++ b/Documentation/git-daemon.txt @@ -0,0 +1,238 @@ +git-daemon(1) +============= + +NAME +---- +git-daemon - A really simple server for git repositories + +SYNOPSIS +-------- +[verse] +'git-daemon' [--verbose] [--syslog] [--export-all] + [--timeout=n] [--init-timeout=n] [--strict-paths] + [--base-path=path] [--user-path | --user-path=path] + [--interpolated-path=pathtemplate] + [--reuseaddr] [--detach] [--pid-file=file] + [--enable=service] [--disable=service] + [--allow-override=service] [--forbid-override=service] + [--inetd | [--listen=host_or_ipaddr] [--port=n] [--user=user [--group=group]] + [directory...] + +DESCRIPTION +----------- +A really simple TCP git daemon that normally listens on port "DEFAULT_GIT_PORT" +aka 9418. It waits for a connection asking for a service, and will serve +that service if it is enabled. + +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 +for export this way (unless the '--export-all' parameter is specified). If you +pass some directory paths as 'git-daemon' arguments, you can further restrict +the offers to a whitelist comprising of those. + +By default, only `upload-pack` service is enabled, which serves +`git-fetch-pack` and `git-peek-remote` clients that are invoked +from `git-fetch`, `git-ls-remote`, and `git-clone`. + +This is ideally suited for read-only updates, i.e., pulling from +git repositories. + +An `upload-archive` also exists to serve `git-archive`. + +OPTIONS +------- +--strict-paths:: + Match paths exactly (i.e. don't allow "/foo/repo" when the real path is + "/foo/repo.git" or "/foo/repo/.git") and don't do user-relative paths. + git-daemon will refuse to start when this option is enabled and no + whitelist is specified. + +--base-path:: + Remap all the path requests as relative to the given path. + This is sort of "GIT root" - if you run git-daemon with + '--base-path=/srv/git' on example.com, then if you later try to pull + 'git://example.com/hello.git', `git-daemon` will interpret the path + as '/srv/git/hello.git'. + +--interpolated-path=pathtemplate:: + To support virtual hosting, an interpolated path template can be + used to dynamically construct alternate paths. The template + supports %H for the target hostname as supplied by the client but + converted to all lowercase, %CH for the canonical hostname, + %IP for the server's IP address, %P for the port number, + and %D for the absolute path of the named repository. + After interpolation, the path is validated against the directory + whitelist. + +--export-all:: + Allow pulling from all directories that look like GIT repositories + (have the 'objects' and 'refs' subdirectories), even if they + do not have the 'git-daemon-export-ok' file. + +--inetd:: + Have the server run as an inetd service. Implies --syslog. + Incompatible with --port, --listen, --user and --group options. + +--listen=host_or_ipaddr:: + Listen on an a specific IP address or hostname. IP addresses can + be either an IPv4 address or an IPV6 address if supported. If IPv6 + is not supported, then --listen=hostname is also not supported and + --listen must be given an IPv4 address. + Incompatible with '--inetd' option. + +--port=n:: + Listen on an alternative port. Incompatible with '--inetd' option. + +--init-timeout:: + Timeout between the moment the connection is established and the + client request is received (typically a rather low value, since + that should be basically immediate). + +--timeout:: + Timeout for specific client sub-requests. This includes the time + it takes for the server to process the sub-request and time spent + waiting for next client's request. + +--syslog:: + Log to syslog instead of stderr. Note that this option does not imply + --verbose, thus by default only error conditions will be logged. + +--user-path, --user-path=path:: + Allow ~user notation to be used in requests. When + specified with no parameter, requests to + git://host/~alice/foo is taken as a request to access + 'foo' repository in the home directory of user `alice`. + If `--user-path=path` is specified, the same request is + taken as a request to access `path/foo` repository in + the home directory of user `alice`. + +--verbose:: + Log details about the incoming connections and requested files. + +--reuseaddr:: + Use SO_REUSEADDR when binding the listening socket. + This allows the server to restart without waiting for + old connections to time out. + +--detach:: + Detach from the shell. Implies --syslog. + +--pid-file=file:: + Save the process id in 'file'. + +--user=user, --group=group:: + Change daemon's uid and gid before entering the service loop. + When only `--user` is given without `--group`, the + primary group ID for the user is used. The values of + the option are given to `getpwnam(3)` and `getgrnam(3)` + and numeric IDs are not supported. ++ +Giving these options is an error when used with `--inetd`; use +the facility of inet daemon to achieve the same before spawning +`git-daemon` if needed. + +--enable=service, --disable=service:: + Enable/disable the service site-wide per default. Note + that a service disabled site-wide can still be enabled + per repository if it is marked overridable and the + repository enables the service with an configuration + item. + +--allow-override=service, --forbid-override=service:: + Allow/forbid overriding the site-wide default with per + repository configuration. By default, all the services + are overridable. + +<directory>:: + A directory to add to the whitelist of allowed directories. Unless + --strict-paths is specified this will also include subdirectories + of each named directory. + +SERVICES +-------- + +upload-pack:: + This serves `git-fetch-pack` and `git-peek-remote` + clients. It is enabled by default, but a repository can + disable it by setting `daemon.uploadpack` configuration + item to `false`. + +upload-archive:: + This serves `git-archive --remote`. + +EXAMPLES +-------- +We assume the following in /etc/services:: ++ +------------ +$ grep 9418 /etc/services +git 9418/tcp # Git Version Control System +------------ + +git-daemon as inetd server:: + To set up `git-daemon` as an inetd service that handles any + repository under the whitelisted set of directories, /pub/foo + and /pub/bar, place an entry like the following into + /etc/inetd all on one line: ++ +------------------------------------------------ + git stream tcp nowait nobody /usr/bin/git-daemon + git-daemon --inetd --verbose --export-all + /pub/foo /pub/bar +------------------------------------------------ + + +git-daemon as inetd server for virtual hosts:: + To set up `git-daemon` as an inetd service that handles + repositories for different virtual hosts, `www.example.com` + and `www.example.org`, place an entry like the following into + `/etc/inetd` all on one line: ++ +------------------------------------------------ + git stream tcp nowait nobody /usr/bin/git-daemon + git-daemon --inetd --verbose --export-all + --interpolated-path=/pub/%H%D + /pub/www.example.org/software + /pub/www.example.com/software + /software +------------------------------------------------ ++ +In this example, the root-level directory `/pub` will contain +a subdirectory for each virtual host name supported. +Further, both hosts advertise repositories simply as +`git://www.example.com/software/repo.git`. For pre-1.4.0 +clients, a symlink from `/software` into the appropriate +default repository could be made as well. + + +git-daemon as regular daemon for virtual hosts:: + To set up `git-daemon` as a regular, non-inetd service that + handles repositories for multiple virtual hosts based on + their IP addresses, start the daemon like this: ++ +------------------------------------------------ + git-daemon --verbose --export-all + --interpolated-path=/pub/%IP/%D + /pub/192.168.1.200/software + /pub/10.10.220.23/software +------------------------------------------------ ++ +In this example, the root-level directory `/pub` will contain +a subdirectory for each virtual host IP address supported. +Repositories can still be accessed by hostname though, assuming +they correspond to these IP addresses. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org>, YOSHIFUJI Hideaki +<yoshfuji@linux-ipv6.org> and the git-list <git@vger.kernel.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-describe.txt b/Documentation/git-describe.txt new file mode 100644 index 0000000000..47a583d3a6 --- /dev/null +++ b/Documentation/git-describe.txt @@ -0,0 +1,122 @@ +git-describe(1) +=============== + +NAME +---- +git-describe - Show the most recent tag that is reachable from a commit + + +SYNOPSIS +-------- +'git-describe' [--all] [--tags] [--abbrev=<n>] <committish>... + +DESCRIPTION +----------- +The command finds the most recent tag that is reachable from a +commit, and if the commit itself is pointed at by the tag, shows +the tag. Otherwise, it suffixes the tag name with the number of +additional commits and the abbreviated object name of the commit. + + +OPTIONS +------- +<committish>:: + The object name of the committish. + +--all:: + Instead of using only the annotated tags, use any ref + found in `.git/refs/`. + +--tags:: + Instead of using only the annotated tags, use any tag + found in `.git/refs/tags`. + +--abbrev=<n>:: + Instead of using the default 8 hexadecimal digits as the + abbreviated object name, use <n> digits. + +--candidates=<n>:: + Instead of considering only the 10 most recent tags as + candidates to describe the input committish consider + up to <n> candidates. Increasing <n> above 10 will take + slightly longer but may produce a more accurate result. + +--debug:: + Verbosely display information about the searching strategy + being employed to standard error. The tag name will still + be printed to standard out. + +EXAMPLES +-------- + +With something like git.git current tree, I get: + + [torvalds@g5 git]$ git-describe parent + v1.0.4-14-g2414721 + +i.e. the current head of my "parent" branch is based on v1.0.4, +but since it has a handful commits on top of that, +describe has added the number of additional commits ("14") and +an abbreviated object name for the commit itself ("2414721") +at the end. + +The number of additional commits is the number +of commits which would be displayed by "git log v1.0.4..parent". +The hash suffix is "-g" + 7-char abbreviation for the tip commit +of parent (which was `2414721b194453f058079d897d13c4e377f92dc6`). + +Doing a "git-describe" on a tag-name will just show the tag name: + + [torvalds@g5 git]$ git-describe v1.0.4 + v1.0.4 + +With --all, the command can use branch heads as references, so +the output shows the reference path as well: + + [torvalds@g5 git]$ git describe --all --abbrev=4 v1.0.5^2 + tags/v1.0.0-21-g975b + + [torvalds@g5 git]$ git describe --all HEAD^ + heads/lt/describe-7-g975b + +With --abbrev set to 0, the command can be used to find the +closest tagname without any suffix: + + [torvalds@g5 git]$ git describe --abbrev=0 v1.0.5^2 + tags/v1.0.0 + +SEARCH STRATEGY +--------------- + +For each committish supplied "git describe" will first look for +a tag which tags exactly that commit. Annotated tags will always +be preferred over lightweight tags, and tags with newer dates will +always be preferred over tags with older dates. If an exact match +is found, its name will be output and searching will stop. + +If an exact match was not found "git describe" will walk back +through the commit history to locate an ancestor commit which +has been tagged. The ancestor's tag will be output along with an +abbreviation of the input committish's SHA1. + +If multiple tags were found during the walk then the tag which +has the fewest commits different from the input committish will be +selected and output. Here fewest commits different is defined as +the number of commits which would be shown by "git log tag..input" +will be the smallest number of commits possible. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org>, but somewhat +butchered by Junio C Hamano <junkio@cox.net>. Later significantly +updated by Shawn Pearce <spearce@spearce.org>. + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-diff-files.txt b/Documentation/git-diff-files.txt new file mode 100644 index 0000000000..7248b35d95 --- /dev/null +++ b/Documentation/git-diff-files.txt @@ -0,0 +1,58 @@ +git-diff-files(1) +================= + +NAME +---- +git-diff-files - Compares files in the working tree and the index + + +SYNOPSIS +-------- +'git-diff-files' [-q] [-0|-1|-2|-3|-c|--cc] [<common diff options>] [<path>...] + +DESCRIPTION +----------- +Compares the files in the working tree and the index. When paths +are specified, compares only those named paths. Otherwise all +entries in the index are compared. The output format is the +same as "git-diff-index" and "git-diff-tree". + +OPTIONS +------- +include::diff-options.txt[] + +-1 -2 -3 or --base --ours --theirs, and -0:: + Diff against the "base" version, "our branch" or "their + branch" respectively. With these options, diffs for + merged entries are not shown. ++ +The default is to diff against our branch (-2) and the +cleanly resolved paths. The option -0 can be given to +omit diff output for unmerged entries and just show "Unmerged". + +-c,--cc:: + This compares stage 2 (our branch), stage 3 (their + branch) and the working tree file and outputs a combined + diff, similar to the way 'diff-tree' shows a merge + commit with these flags. + +-q:: + Remain silent even on nonexistent files + +Output format +------------- +include::diff-format.txt[] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-diff-index.txt b/Documentation/git-diff-index.txt new file mode 100644 index 0000000000..2df581c2c9 --- /dev/null +++ b/Documentation/git-diff-index.txt @@ -0,0 +1,133 @@ +git-diff-index(1) +================= + +NAME +---- +git-diff-index - Compares content and mode of blobs between the index and repository + + +SYNOPSIS +-------- +'git-diff-index' [-m] [--cached] [<common diff options>] <tree-ish> [<path>...] + +DESCRIPTION +----------- +Compares the content and mode of the blobs found via a tree +object with the content of the current index and, optionally +ignoring the stat state of the file on disk. When paths are +specified, compares only those named paths. Otherwise all +entries in the index are compared. + +OPTIONS +------- +include::diff-options.txt[] + +<tree-ish>:: + The id of a tree object to diff against. + +--cached:: + do not consider the on-disk file at all + +-m:: + By default, files recorded in the index but not checked + out are reported as deleted. This flag makes + "git-diff-index" say that all non-checked-out files are up + to date. + +Output format +------------- +include::diff-format.txt[] + +Operating Modes +--------------- +You can choose whether you want to trust the index file entirely +(using the '--cached' flag) or ask the diff logic to show any files +that don't match the stat state as being "tentatively changed". Both +of these operations are very useful indeed. + +Cached Mode +----------- +If '--cached' is specified, it allows you to ask: + + show me the differences between HEAD and the current index + contents (the ones I'd write with a "git-write-tree") + +For example, let's say that you have worked on your working directory, updated +some files in the index and are ready to commit. You want to see exactly +*what* you are going to commit, without having to write a new tree +object and compare it that way, and to do that, you just do + + git-diff-index --cached HEAD + +Example: let's say I had renamed `commit.c` to `git-commit.c`, and I had +done an "git-update-index" to make that effective in the index file. +"git-diff-files" wouldn't show anything at all, since the index file +matches my working directory. But doing a "git-diff-index" does: + + torvalds@ppc970:~/git> git-diff-index --cached HEAD + -100644 blob 4161aecc6700a2eb579e842af0b7f22b98443f74 commit.c + +100644 blob 4161aecc6700a2eb579e842af0b7f22b98443f74 git-commit.c + +You can see easily that the above is a rename. + +In fact, "git-diff-index --cached" *should* always be entirely equivalent to +actually doing a "git-write-tree" and comparing that. Except this one is much +nicer for the case where you just want to check where you are. + +So doing a "git-diff-index --cached" is basically very useful when you are +asking yourself "what have I already marked for being committed, and +what's the difference to a previous tree". + +Non-cached Mode +--------------- +The "non-cached" mode takes a different approach, and is potentially +the more useful of the two in that what it does can't be emulated with +a "git-write-tree" + "git-diff-tree". Thus that's the default mode. +The non-cached version asks the question: + + show me the differences between HEAD and the currently checked out + tree - index contents _and_ files that aren't up-to-date + +which is obviously a very useful question too, since that tells you what +you *could* commit. Again, the output matches the "git-diff-tree -r" +output to a tee, but with a twist. + +The twist is that if some file doesn't match the index, we don't have +a backing store thing for it, and we use the magic "all-zero" sha1 to +show that. So let's say that you have edited `kernel/sched.c`, but +have not actually done a "git-update-index" on it yet - there is no +"object" associated with the new state, and you get: + + torvalds@ppc970:~/v2.6/linux> git-diff-index HEAD + *100644->100664 blob 7476bb......->000000...... kernel/sched.c + +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. + +NOTE: As with other commands of this type, "git-diff-index" does not +actually look at the contents of the file at all. So maybe +`kernel/sched.c` hasn't actually changed, and it's just that you +touched it. In either case, it's a note that you need to +"git-update-index" it to make the index be in sync. + +NOTE: You can have a mixture of files show up as "has been updated" +and "is still dirty in the working directory" together. You can always +tell which file is in which state, since the "has been updated" ones +show a valid sha1, and the "not in sync with the index" ones will +always have the special all-zero sha1. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-diff-stages.txt b/Documentation/git-diff-stages.txt new file mode 100644 index 0000000000..b8f45b8cdc --- /dev/null +++ b/Documentation/git-diff-stages.txt @@ -0,0 +1,42 @@ +git-diff-stages(1) +================== + +NAME +---- +git-diff-stages - Compares two merge stages in the index + + +SYNOPSIS +-------- +'git-diff-stages' [<common diff options>] <stage1> <stage2> [<path>...] + +DESCRIPTION +----------- +DEPRECATED and will be removed in 1.5.1. + +Compares the content and mode of the blobs in two stages in an +unmerged index file. + +OPTIONS +------- +include::diff-options.txt[] + +<stage1>,<stage2>:: + The stage number to be compared. + +Output format +------------- +include::diff-format.txt[] + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-diff-tree.txt b/Documentation/git-diff-tree.txt new file mode 100644 index 0000000000..5d6e9dc751 --- /dev/null +++ b/Documentation/git-diff-tree.txt @@ -0,0 +1,166 @@ +git-diff-tree(1) +================ + +NAME +---- +git-diff-tree - Compares the content and mode of blobs found via two tree objects + + +SYNOPSIS +-------- +[verse] +'git-diff-tree' [--stdin] [-m] [-s] [-v] [--no-commit-id] [--pretty] + [-t] [-r] [-c | --cc] [--root] [<common diff options>] + <tree-ish> [<tree-ish>] [<path>...] + +DESCRIPTION +----------- +Compares the content and mode of the blobs found via two tree objects. + +If there is only one <tree-ish> given, the commit is compared with its parents +(see --stdin below). + +Note that "git-diff-tree" can use the tree encapsulated in a commit object. + +OPTIONS +------- +include::diff-options.txt[] + +<tree-ish>:: + The id of a tree object. + +<path>...:: + If provided, the results are limited to a subset of files + matching one of these prefix strings. + i.e., file matches `/^<pattern1>|<pattern2>|.../` + Note that this parameter does not provide any wildcard or regexp + features. + +-r:: + recurse into sub-trees + +-t:: + show tree entry itself as well as subtrees. Implies -r. + +--root:: + When '--root' is specified the initial commit will be showed as a big + creation event. This is equivalent to a diff against the NULL tree. + +--stdin:: + When '--stdin' is specified, the command does not take + <tree-ish> arguments from the command line. Instead, it + reads either one <commit> or a pair of <tree-ish> + separated with a single space from its standard input. ++ +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 +behavior. This does not apply to the case where two <tree-ish> +separated with a single space are given. + +-m:: + By default, "git-diff-tree --stdin" does not show + differences for merge commits. With this flag, it shows + differences to that commit from all of its parents. See + also '-c'. + +-s:: + By default, "git-diff-tree --stdin" shows differences, + either in machine-readable form (without '-p') or in patch + form (with '-p'). This output can be suppressed. It is + only useful with '-v' flag. + +-v:: + This flag causes "git-diff-tree --stdin" to also show + the commit message before the differences. + +include::pretty-formats.txt[] + +--no-commit-id:: + git-diff-tree outputs a line with the commit ID when + applicable. This flag suppressed the commit ID output. + +-c:: + This flag changes the way a merge commit is displayed + (which means it is useful only when the command is given + one <tree-ish>, or '--stdin'). It shows the differences + from each of the parents to the merge result simultaneously + instead of showing pairwise diff between a parent and the + result one at a time (which is what the '-m' option does). + Furthermore, it lists only files which were modified + from all parents. + +--cc:: + This flag changes the way a merge commit patch is displayed, + in a similar way to the '-c' option. It implies the '-c' + and '-p' options and further compresses the patch output + by omitting hunks that show differences from only one + parent, or show the same change from all but one parent + for an Octopus merge. When this optimization makes all + hunks disappear, the commit itself and the commit log + message is not shown, just like in any other "empty diff" case. + +--always:: + Show the commit itself and the commit log message even + if the diff itself is empty. + + +Limiting Output +--------------- +If you're only interested in differences in a subset of files, for +example some architecture-specific files, you might do: + + git-diff-tree -r <tree-ish> <tree-ish> arch/ia64 include/asm-ia64 + +and it will only show you what changed in those two directories. + +Or if you are searching for what changed in just `kernel/sched.c`, just do + + git-diff-tree -r <tree-ish> <tree-ish> kernel/sched.c + +and it will ignore all differences to other files. + +The pattern is always the prefix, and is matched exactly. There are no +wildcards. Even stricter, it has to match a complete path component. +I.e. "foo" does not pick up `foobar.h`. "foo" does match `foo/bar.h` +so it can be used to name subdirectories. + +An example of normal usage is: + + torvalds@ppc970:~/git> git-diff-tree 5319e4...... + *100664->100664 blob ac348b.......->a01513....... git-fsck-objects.c + +which tells you that the last commit changed just one file (it's from +this one: + +----------------------------------------------------------------------------- +commit 3c6f7ca19ad4043e9e72fa94106f352897e651a8 +tree 5319e4d609cdd282069cc4dce33c1db559539b03 +parent b4e628ea30d5ab3606119d2ea5caeab141d38df7 +author Linus Torvalds <torvalds@ppc970.osdl.org> Sat Apr 9 12:02:30 2005 +committer Linus Torvalds <torvalds@ppc970.osdl.org> Sat Apr 9 12:02:30 2005 + +Make "git-fsck-objects" print out all the root commits it finds. + +Once I do the reference tracking, I'll also make it print out all the +HEAD commits it finds, which is even more interesting. +----------------------------------------------------------------------------- + +in case you care). + +Output format +------------- +include::diff-format.txt[] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-diff.txt b/Documentation/git-diff.txt new file mode 100644 index 0000000000..6a098df26b --- /dev/null +++ b/Documentation/git-diff.txt @@ -0,0 +1,137 @@ +git-diff(1) +=========== + +NAME +---- +git-diff - Show changes between commits, commit and working tree, etc + + +SYNOPSIS +-------- +'git-diff' [ --diff-options ] <commit>{0,2} [--] [<path>...] + +DESCRIPTION +----------- +Show changes between two trees, a tree and the working tree, a +tree and the index file, or the index file and the working tree. + +'git-diff' [--options] [--] [<path>...]:: + + This form is to view the changes you made relative to + the index (staging area for the next commit). In other + words, the differences are what you _could_ tell git to + further add to the index but you still haven't. You can + stage these changes by using gitlink:git-add[1]. + +'git-diff' [--options] --cached [<commit>] [--] [<path>...]:: + + This form is to view the changes you staged for the next + commit relative to the named <commit>. Typically you + would want comparison with the latest commit, so if you + do not give <commit>, it defaults to HEAD. + +'git-diff' [--options] <commit> [--] [<path>...]:: + + This form is to view the changes you have in your + working tree relative to the named <commit>. You can + use HEAD to compare it with the latest commit, or a + branch name to compare with the tip of a different + branch. + +'git-diff' [--options] <commit> <commit> [--] [<path>...]:: + + This form is to view the changes between two <commit>, + for example, tips of two branches. + +Just in case if you are doing something exotic, it should be +noted that all of the <commit> in the above description can be +any <tree-ish>. + +For a more complete list of ways to spell <commit>, see +"SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1]. + + +OPTIONS +------- +include::diff-options.txt[] + +<path>...:: + The <paths> parameters, when given, are used to limit + the diff to the named paths (you can give directory + names and get diff for all files under them). + + +EXAMPLES +-------- + +Various ways to check your working tree:: ++ +------------ +$ git diff <1> +$ git diff --cached <2> +$ git diff HEAD <3> +------------ ++ +<1> changes in the working tree not yet staged for the next commit. +<2> changes between the index and your last commit; what you +would be committing if you run "git commit" without "-a" option. +<3> changes in the working tree since your last commit; what you +would be committing if you run "git commit -a" + +Comparing with arbitrary commits:: ++ +------------ +$ git diff test <1> +$ git diff HEAD -- ./test <2> +$ git diff HEAD^ HEAD <3> +------------ ++ +<1> instead of using the tip of the current branch, compare with the +tip of "test" branch. +<2> instead of comparing with the tip of "test" branch, compare with +the tip of the current branch, but limit the comparison to the +file "test". +<3> compare the version before the last commit and the last commit. + + +Limiting the diff output:: ++ +------------ +$ git diff --diff-filter=MRC <1> +$ git diff --name-status -r <2> +$ git diff arch/i386 include/asm-i386 <3> +------------ ++ +<1> show only modification, rename and copy, but not addition +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 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. + +Munging the diff output:: ++ +------------ +$ git diff --find-copies-harder -B -C <1> +$ git diff -R <2> +------------ ++ +<1> spend extra cycles to find renames, copies and complete +rewrites (very expensive). +<2> output diff in reverse. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-fast-import.txt b/Documentation/git-fast-import.txt new file mode 100644 index 0000000000..939ec4652b --- /dev/null +++ b/Documentation/git-fast-import.txt @@ -0,0 +1,902 @@ +git-fast-import(1) +================== + +NAME +---- +git-fast-import - Backend for fast Git data importers. + + +SYNOPSIS +-------- +frontend | 'git-fast-import' [options] + +DESCRIPTION +----------- +This program is usually not what the end user wants to run directly. +Most end users want to use one of the existing frontend programs, +which parses a specific type of foreign source and feeds the contents +stored there to git-fast-import. + +fast-import reads a mixed command/data stream from standard input and +writes one or more packfiles directly into the current repository. +When EOF is received on standard input, fast import writes out +updated branch and tag refs, fully updating the current repository +with the newly imported data. + +The fast-import backend itself can import into an empty repository (one that +has already been initialized by gitlink:git-init[1]) or incrementally +update an existing populated repository. Whether or not incremental +imports are supported from a particular foreign source depends on +the frontend program in use. + + +OPTIONS +------- +--date-format=<fmt>:: + Specify the type of dates the frontend will supply to + fast-import within `author`, `committer` and `tagger` commands. + See ``Date Formats'' below for details about which formats + are supported, and their syntax. + +--force:: + Force updating modified existing branches, even if doing + so would cause commits to be lost (as the new commit does + not contain the old commit). + +--max-pack-size=<n>:: + Maximum size of each output packfile, expressed in MiB. + The default is 4096 (4 GiB) as that is the maximum allowed + packfile size (due to file format limitations). Some + importers may wish to lower this, such as to ensure the + resulting packfiles fit on CDs. + +--depth=<n>:: + Maximum delta depth, for blob and tree deltification. + Default is 10. + +--active-branches=<n>:: + Maximum number of branches to maintain active at once. + See ``Memory Utilization'' below for details. Default is 5. + +--export-marks=<file>:: + Dumps the internal marks table to <file> when complete. + Marks are written one per line as `:markid SHA-1`. + Frontends can use this file to validate imports after they + have been completed. + +--export-pack-edges=<file>:: + After creating a packfile, print a line of data to + <file> listing the filename of the packfile and the last + commit on each branch that was written to that packfile. + This information may be useful after importing projects + whose total object set exceeds the 4 GiB packfile limit, + as these commits can be used as edge points during calls + to gitlink:git-pack-objects[1]. + +--quiet:: + Disable all non-fatal output, making fast-import silent when it + is successful. This option disables the output shown by + \--stats. + +--stats:: + Display some basic statistics about the objects fast-import has + created, the packfiles they were stored into, and the + memory used by fast-import during this run. Showing this output + is currently the default, but can be disabled with \--quiet. + + +Performance +----------- +The design of fast-import allows it to import large projects in a minimum +amount of memory usage and processing time. Assuming the frontend +is able to keep up with fast-import and feed it a constant stream of data, +import times for projects holding 10+ years of history and containing +100,000+ individual commits are generally completed in just 1-2 +hours on quite modest (~$2,000 USD) hardware. + +Most bottlenecks appear to be in foreign source data access (the +source just cannot extract revisions fast enough) or disk IO (fast-import +writes as fast as the disk will take the data). Imports will run +faster if the source data is stored on a different drive than the +destination Git repository (due to less IO contention). + + +Development Cost +---------------- +A typical frontend for fast-import tends to weigh in at approximately 200 +lines of Perl/Python/Ruby code. Most developers have been able to +create working importers in just a couple of hours, even though it +is their first exposure to fast-import, and sometimes even to Git. This is +an ideal situation, given that most conversion tools are throw-away +(use once, and never look back). + + +Parallel Operation +------------------ +Like `git-push` or `git-fetch`, imports handled by fast-import are safe to +run alongside parallel `git repack -a -d` or `git gc` invocations, +or any other Git operation (including `git prune`, as loose objects +are never used by fast-import). + +fast-import does not lock the branch or tag refs it is actively importing. +After the import, during its ref update phase, fast-import tests each +existing branch ref to verify the update will be a fast-forward +update (the commit stored in the ref is contained in the new +history of the commit to be written). If the update is not a +fast-forward update, fast-import will skip updating that ref and instead +prints a warning message. fast-import will always attempt to update all +branch refs, and does not stop on the first failure. + +Branch updates can be forced with \--force, but its recommended that +this only be used on an otherwise quiet repository. Using \--force +is not necessary for an initial import into an empty repository. + + +Technical Discussion +-------------------- +fast-import tracks a set of branches in memory. Any branch can be created +or modified at any point during the import process by sending a +`commit` command on the input stream. This design allows a frontend +program to process an unlimited number of branches simultaneously, +generating commits in the order they are available from the source +data. It also simplifies the frontend programs considerably. + +fast-import does not use or alter the current working directory, or any +file within it. (It does however update the current Git repository, +as referenced by `GIT_DIR`.) Therefore an import frontend may use +the working directory for its own purposes, such as extracting file +revisions from the foreign source. This ignorance of the working +directory also allows fast-import to run very quickly, as it does not +need to perform any costly file update operations when switching +between branches. + +Input Format +------------ +With the exception of raw file data (which Git does not interpret) +the fast-import input format is text (ASCII) based. This text based +format simplifies development and debugging of frontend programs, +especially when a higher level language such as Perl, Python or +Ruby is being used. + +fast-import is very strict about its input. Where we say SP below we mean +*exactly* one space. Likewise LF means one (and only one) linefeed. +Supplying additional whitespace characters will cause unexpected +results, such as branch names or file names with leading or trailing +spaces in their name, or early termination of fast-import when it encounters +unexpected input. + +Date Formats +~~~~~~~~~~~~ +The following date formats are supported. A frontend should select +the format it will use for this import by passing the format name +in the \--date-format=<fmt> command line option. + +`raw`:: + This is the Git native format and is `<time> SP <offutc>`. + It is also fast-import's default format, if \--date-format was + not specified. ++ +The time of the event is specified by `<time>` as the number of +seconds since the UNIX epoch (midnight, Jan 1, 1970, UTC) and is +written as an ASCII decimal integer. ++ +The local offset is specified by `<offutc>` as a positive or negative +offset from UTC. For example EST (which is 5 hours behind UTC) +would be expressed in `<tz>` by ``-0500'' while UTC is ``+0000''. +The local offset does not affect `<time>`; it is used only as an +advisement to help formatting routines display the timestamp. ++ +If the local offset is not available in the source material, use +``+0000'', or the most common local offset. For example many +organizations have a CVS repository which has only ever been accessed +by users who are located in the same location and timezone. In this +case a reasonable offset from UTC could be assumed. ++ +Unlike the `rfc2822` format, this format is very strict. Any +variation in formatting will cause fast-import to reject the value. + +`rfc2822`:: + This is the standard email format as described by RFC 2822. ++ +An example value is ``Tue Feb 6 11:22:18 2007 -0500''. The Git +parser is accurate, but a little on the lenient side. It is the +same parser used by gitlink:git-am[1] when applying patches +received from email. ++ +Some malformed strings may be accepted as valid dates. In some of +these cases Git will still be able to obtain the correct date from +the malformed string. There are also some types of malformed +strings which Git will parse wrong, and yet consider valid. +Seriously malformed strings will be rejected. ++ +Unlike the `raw` format above, the timezone/UTC offset information +contained in an RFC 2822 date string is used to adjust the date +value to UTC prior to storage. Therefore it is important that +this information be as accurate as possible. ++ +If the source material uses RFC 2822 style dates, +the frontend should let fast-import handle the parsing and conversion +(rather than attempting to do it itself) as the Git parser has +been well tested in the wild. ++ +Frontends should prefer the `raw` format if the source material +already uses UNIX-epoch format, can be coaxed to give dates in that +format, or its format is easiliy convertible to it, as there is no +ambiguity in parsing. + +`now`:: + Always use the current time and timezone. The literal + `now` must always be supplied for `<when>`. ++ +This is a toy format. The current time and timezone of this system +is always copied into the identity string at the time it is being +created by fast-import. There is no way to specify a different time or +timezone. ++ +This particular format is supplied as its short to implement and +may be useful to a process that wants to create a new commit +right now, without needing to use a working directory or +gitlink:git-update-index[1]. ++ +If separate `author` and `committer` commands are used in a `commit` +the timestamps may not match, as the system clock will be polled +twice (once for each command). The only way to ensure that both +author and committer identity information has the same timestamp +is to omit `author` (thus copying from `committer`) or to use a +date format other than `now`. + +Commands +~~~~~~~~ +fast-import accepts several commands to update the current repository +and control the current import process. More detailed discussion +(with examples) of each command follows later. + +`commit`:: + Creates a new branch or updates an existing branch by + creating a new commit and updating the branch to point at + the newly created commit. + +`tag`:: + Creates an annotated tag object from an existing commit or + branch. Lightweight tags are not supported by this command, + as they are not recommended for recording meaningful points + in time. + +`reset`:: + Reset an existing branch (or a new branch) to a specific + revision. This command must be used to change a branch to + a specific revision without making a commit on it. + +`blob`:: + Convert raw file data into a blob, for future use in a + `commit` command. This command is optional and is not + needed to perform an import. + +`checkpoint`:: + Forces fast-import to close the current packfile, generate its + unique SHA-1 checksum and index, and start a new packfile. + This command is optional and is not needed to perform + an import. + +`commit` +~~~~~~~~ +Create or update a branch with a new commit, recording one logical +change to the project. + +.... + 'commit' SP <ref> LF + mark? + ('author' SP <name> SP LT <email> GT SP <when> LF)? + 'committer' SP <name> SP LT <email> GT SP <when> LF + data + ('from' SP <committish> LF)? + ('merge' SP <committish> LF)? + (filemodify | filedelete | filedeleteall)* + LF +.... + +where `<ref>` is the name of the branch to make the commit on. +Typically branch names are prefixed with `refs/heads/` in +Git, so importing the CVS branch symbol `RELENG-1_0` would use +`refs/heads/RELENG-1_0` for the value of `<ref>`. The value of +`<ref>` must be a valid refname in Git. As `LF` is not valid in +a Git refname, no quoting or escaping syntax is supported here. + +A `mark` command may optionally appear, requesting fast-import to save a +reference to the newly created commit for future use by the frontend +(see below for format). It is very common for frontends to mark +every commit they create, thereby allowing future branch creation +from any imported commit. + +The `data` command following `committer` must supply the commit +message (see below for `data` command syntax). To import an empty +commit message use a 0 length data. Commit messages are free-form +and are not interpreted by Git. Currently they must be encoded in +UTF-8, as fast-import does not permit other encodings to be specified. + +Zero or more `filemodify`, `filedelete` and `filedeleteall` commands +may be included to update the contents of the branch prior to +creating the commit. These commands may be supplied in any order. +However it is recommended that a `filedeleteall` command preceed +all `filemodify` commands in the same commit, as `filedeleteall` +wipes the branch clean (see below). + +`author` +^^^^^^^^ +An `author` command may optionally appear, if the author information +might differ from the committer information. If `author` is omitted +then fast-import will automatically use the committer's information for +the author portion of the commit. See below for a description of +the fields in `author`, as they are identical to `committer`. + +`committer` +^^^^^^^^^^^ +The `committer` command indicates who made this commit, and when +they made it. + +Here `<name>` is the person's display name (for example +``Com M Itter'') and `<email>` is the person's email address +(``cm@example.com''). `LT` and `GT` are the literal less-than (\x3c) +and greater-than (\x3e) symbols. These are required to delimit +the email address from the other fields in the line. Note that +`<name>` is free-form and may contain any sequence of bytes, except +`LT` and `LF`. It is typically UTF-8 encoded. + +The time of the change is specified by `<when>` using the date format +that was selected by the \--date-format=<fmt> command line option. +See ``Date Formats'' above for the set of supported formats, and +their syntax. + +`from` +^^^^^^ +Only valid for the first commit made on this branch by this +fast-import process. The `from` command is used to specify the commit +to initialize this branch from. This revision will be the first +ancestor of the new commit. + +Omitting the `from` command in the first commit of a new branch will +cause fast-import to create that commit with no ancestor. This tends to be +desired only for the initial commit of a project. Omitting the +`from` command on existing branches is required, as the current +commit on that branch is automatically assumed to be the first +ancestor of the new commit. + +As `LF` is not valid in a Git refname or SHA-1 expression, no +quoting or escaping syntax is supported within `<committish>`. + +Here `<committish>` is any of the following: + +* The name of an existing branch already in fast-import's internal branch + table. If fast-import doesn't know the name, its treated as a SHA-1 + expression. + +* A mark reference, `:<idnum>`, where `<idnum>` is the mark number. ++ +The reason fast-import uses `:` to denote a mark reference is this character +is not legal in a Git branch name. The leading `:` makes it easy +to distingush between the mark 42 (`:42`) and the branch 42 (`42` +or `refs/heads/42`), or an abbreviated SHA-1 which happened to +consist only of base-10 digits. ++ +Marks must be declared (via `mark`) before they can be used. + +* A complete 40 byte or abbreviated commit SHA-1 in hex. + +* Any valid Git SHA-1 expression that resolves to a commit. See + ``SPECIFYING REVISIONS'' in gitlink:git-rev-parse[1] for details. + +The special case of restarting an incremental import from the +current branch value should be written as: +---- + from refs/heads/branch^0 +---- +The `{caret}0` suffix is necessary as fast-import does not permit a branch to +start from itself, and the branch is created in memory before the +`from` command is even read from the input. Adding `{caret}0` will force +fast-import to resolve the commit through Git's revision parsing library, +rather than its internal branch table, thereby loading in the +existing value of the branch. + +`merge` +^^^^^^^ +Includes one additional ancestor commit, and makes the current +commit a merge commit. An unlimited number of `merge` commands per +commit are permitted by fast-import, thereby establishing an n-way merge. +However Git's other tools never create commits with more than 15 +additional ancestors (forming a 16-way merge). For this reason +it is suggested that frontends do not use more than 15 `merge` +commands per commit. + +Here `<committish>` is any of the commit specification expressions +also accepted by `from` (see above). + +`filemodify` +^^^^^^^^^^^^ +Included in a `commit` command to add a new file or change the +content of an existing file. This command has two different means +of specifying the content of the file. + +External data format:: + The data content for the file was already supplied by a prior + `blob` command. The frontend just needs to connect it. ++ +.... + 'M' SP <mode> SP <dataref> SP <path> LF +.... ++ +Here `<dataref>` can be either a mark reference (`:<idnum>`) +set by a prior `blob` command, or a full 40-byte SHA-1 of an +existing Git blob object. + +Inline data format:: + The data content for the file has not been supplied yet. + The frontend wants to supply it as part of this modify + command. ++ +.... + 'M' SP <mode> SP 'inline' SP <path> LF + data +.... ++ +See below for a detailed description of the `data` command. + +In both formats `<mode>` is the type of file entry, specified +in octal. Git only supports the following modes: + +* `100644` or `644`: A normal (not-executable) file. The majority + of files in most projects use this mode. If in doubt, this is + what you want. +* `100755` or `755`: A normal, but executable, file. +* `120000`: A symlink, the content of the file will be the link target. + +In both formats `<path>` is the complete path of the file to be added +(if not already existing) or modified (if already existing). + +A `<path>` string must use UNIX-style directory seperators (forward +slash `/`), may contain any byte other than `LF`, and must not +start with double quote (`"`). + +If an `LF` or double quote must be encoded into `<path>` shell-style +quoting should be used, e.g. `"path/with\n and \" in it"`. + +The value of `<path>` must be in canoncial form. That is it must not: + +* contain an empty directory component (e.g. `foo//bar` is invalid), +* end with a directory seperator (e.g. `foo/` is invalid), +* start with a directory seperator (e.g. `/foo` is invalid), +* contain the special component `.` or `..` (e.g. `foo/./bar` and + `foo/../bar` are invalid). + +It is recommended that `<path>` always be encoded using UTF-8. + +`filedelete` +^^^^^^^^^^^^ +Included in a `commit` command to remove a file from the branch. +If the file removal makes its directory empty, the directory will +be automatically removed too. This cascades up the tree until the +first non-empty directory or the root is reached. + +.... + 'D' SP <path> LF +.... + +here `<path>` is the complete path of the file to be removed. +See `filemodify` above for a detailed description of `<path>`. + +`filedeleteall` +^^^^^^^^^^^^^^^ +Included in a `commit` command to remove all files (and also all +directories) from the branch. This command resets the internal +branch structure to have no files in it, allowing the frontend +to subsequently add all interesting files from scratch. + +.... + 'deleteall' LF +.... + +This command is extremely useful if the frontend does not know +(or does not care to know) what files are currently on the branch, +and therefore cannot generate the proper `filedelete` commands to +update the content. + +Issuing a `filedeleteall` followed by the needed `filemodify` +commands to set the correct content will produce the same results +as sending only the needed `filemodify` and `filedelete` commands. +The `filedeleteall` approach may however require fast-import to use slightly +more memory per active branch (less than 1 MiB for even most large +projects); so frontends that can easily obtain only the affected +paths for a commit are encouraged to do so. + +`mark` +~~~~~~ +Arranges for fast-import to save a reference to the current object, allowing +the frontend to recall this object at a future point in time, without +knowing its SHA-1. Here the current object is the object creation +command the `mark` command appears within. This can be `commit`, +`tag`, and `blob`, but `commit` is the most common usage. + +.... + 'mark' SP ':' <idnum> LF +.... + +where `<idnum>` is the number assigned by the frontend to this mark. +The value of `<idnum>` is expressed as an ASCII decimal integer. +The value 0 is reserved and cannot be used as +a mark. Only values greater than or equal to 1 may be used as marks. + +New marks are created automatically. Existing marks can be moved +to another object simply by reusing the same `<idnum>` in another +`mark` command. + +`tag` +~~~~~ +Creates an annotated tag referring to a specific commit. To create +lightweight (non-annotated) tags see the `reset` command below. + +.... + 'tag' SP <name> LF + 'from' SP <committish> LF + 'tagger' SP <name> SP LT <email> GT SP <when> LF + data + LF +.... + +where `<name>` is the name of the tag to create. + +Tag names are automatically prefixed with `refs/tags/` when stored +in Git, so importing the CVS branch symbol `RELENG-1_0-FINAL` would +use just `RELENG-1_0-FINAL` for `<name>`, and fast-import will write the +corresponding ref as `refs/tags/RELENG-1_0-FINAL`. + +The value of `<name>` must be a valid refname in Git and therefore +may contain forward slashes. As `LF` is not valid in a Git refname, +no quoting or escaping syntax is supported here. + +The `from` command is the same as in the `commit` command; see +above for details. + +The `tagger` command uses the same format as `committer` within +`commit`; again see above for details. + +The `data` command following `tagger` must supply the annotated tag +message (see below for `data` command syntax). To import an empty +tag message use a 0 length data. Tag messages are free-form and are +not interpreted by Git. Currently they must be encoded in UTF-8, +as fast-import does not permit other encodings to be specified. + +Signing annotated tags during import from within fast-import is not +supported. Trying to include your own PGP/GPG signature is not +recommended, as the frontend does not (easily) have access to the +complete set of bytes which normally goes into such a signature. +If signing is required, create lightweight tags from within fast-import with +`reset`, then create the annotated versions of those tags offline +with the standard gitlink:git-tag[1] process. + +`reset` +~~~~~~~ +Creates (or recreates) the named branch, optionally starting from +a specific revision. The reset command allows a frontend to issue +a new `from` command for an existing branch, or to create a new +branch from an existing commit without creating a new commit. + +.... + 'reset' SP <ref> LF + ('from' SP <committish> LF)? + LF +.... + +For a detailed description of `<ref>` and `<committish>` see above +under `commit` and `from`. + +The `reset` command can also be used to create lightweight +(non-annotated) tags. For example: + +==== + reset refs/tags/938 + from :938 +==== + +would create the lightweight tag `refs/tags/938` referring to +whatever commit mark `:938` references. + +`blob` +~~~~~~ +Requests writing one file revision to the packfile. The revision +is not connected to any commit; this connection must be formed in +a subsequent `commit` command by referencing the blob through an +assigned mark. + +.... + 'blob' LF + mark? + data +.... + +The mark command is optional here as some frontends have chosen +to generate the Git SHA-1 for the blob on their own, and feed that +directly to `commit`. This is typically more work than its worth +however, as marks are inexpensive to store and easy to use. + +`data` +~~~~~~ +Supplies raw data (for use as blob/file content, commit messages, or +annotated tag messages) to fast-import. Data can be supplied using an exact +byte count or delimited with a terminating line. Real frontends +intended for production-quality conversions should always use the +exact byte count format, as it is more robust and performs better. +The delimited format is intended primarily for testing fast-import. + +Exact byte count format:: + The frontend must specify the number of bytes of data. ++ +.... + 'data' SP <count> LF + <raw> LF +.... ++ +where `<count>` is the exact number of bytes appearing within +`<raw>`. The value of `<count>` is expressed as an ASCII decimal +integer. The `LF` on either side of `<raw>` is not +included in `<count>` and will not be included in the imported data. + +Delimited format:: + A delimiter string is used to mark the end of the data. + fast-import will compute the length by searching for the delimiter. + This format is primarly useful for testing and is not + recommended for real data. ++ +.... + 'data' SP '<<' <delim> LF + <raw> LF + <delim> LF +.... ++ +where `<delim>` is the chosen delimiter string. The string `<delim>` +must not appear on a line by itself within `<raw>`, as otherwise +fast-import will think the data ends earlier than it really does. The `LF` +immediately trailing `<raw>` is part of `<raw>`. This is one of +the limitations of the delimited format, it is impossible to supply +a data chunk which does not have an LF as its last byte. + +`checkpoint` +~~~~~~~~~~~~ +Forces fast-import to close the current packfile, start a new one, and to +save out all current branch refs, tags and marks. + +.... + 'checkpoint' LF + LF +.... + +Note that fast-import automatically switches packfiles when the current +packfile reaches \--max-pack-size, or 4 GiB, whichever limit is +smaller. During an automatic packfile switch fast-import does not update +the branch refs, tags or marks. + +As a `checkpoint` can require a significant amount of CPU time and +disk IO (to compute the overall pack SHA-1 checksum, generate the +corresponding index file, and update the refs) it can easily take +several minutes for a single `checkpoint` command to complete. + +Frontends may choose to issue checkpoints during extremely large +and long running imports, or when they need to allow another Git +process access to a branch. However given that a 30 GiB Subversion +repository can be loaded into Git through fast-import in about 3 hours, +explicit checkpointing may not be necessary. + + +Tips and Tricks +--------------- +The following tips and tricks have been collected from various +users of fast-import, and are offered here as suggestions. + +Use One Mark Per Commit +~~~~~~~~~~~~~~~~~~~~~~~ +When doing a repository conversion, use a unique mark per commit +(`mark :<n>`) and supply the \--export-marks option on the command +line. fast-import will dump a file which lists every mark and the Git +object SHA-1 that corresponds to it. If the frontend can tie +the marks back to the source repository, it is easy to verify the +accuracy and completeness of the import by comparing each Git +commit to the corresponding source revision. + +Coming from a system such as Perforce or Subversion this should be +quite simple, as the fast-import mark can also be the Perforce changeset +number or the Subversion revision number. + +Freely Skip Around Branches +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Don't bother trying to optimize the frontend to stick to one branch +at a time during an import. Although doing so might be slightly +faster for fast-import, it tends to increase the complexity of the frontend +code considerably. + +The branch LRU builtin to fast-import tends to behave very well, and the +cost of activating an inactive branch is so low that bouncing around +between branches has virtually no impact on import performance. + +Handling Renames +~~~~~~~~~~~~~~~~ +When importing a renamed file or directory, simply delete the old +name(s) and modify the new name(s) during the corresponding commit. +Git performs rename detection after-the-fact, rather than explicitly +during a commit. + +Use Tag Fixup Branches +~~~~~~~~~~~~~~~~~~~~~~ +Some other SCM systems let the user create a tag from multiple +files which are not from the same commit/changeset. Or to create +tags which are a subset of the files available in the repository. + +Importing these tags as-is in Git is impossible without making at +least one commit which ``fixes up'' the files to match the content +of the tag. Use fast-import's `reset` command to reset a dummy branch +outside of your normal branch space to the base commit for the tag, +then commit one or more file fixup commits, and finally tag the +dummy branch. + +For example since all normal branches are stored under `refs/heads/` +name the tag fixup branch `TAG_FIXUP`. This way it is impossible for +the fixup branch used by the importer to have namespace conflicts +with real branches imported from the source (the name `TAG_FIXUP` +is not `refs/heads/TAG_FIXUP`). + +When committing fixups, consider using `merge` to connect the +commit(s) which are supplying file revisions to the fixup branch. +Doing so will allow tools such as gitlink:git-blame[1] to track +through the real commit history and properly annotate the source +files. + +After fast-import terminates the frontend will need to do `rm .git/TAG_FIXUP` +to remove the dummy branch. + +Import Now, Repack Later +~~~~~~~~~~~~~~~~~~~~~~~~ +As soon as fast-import completes the Git repository is completely valid +and ready for use. Typicallly this takes only a very short time, +even for considerably large projects (100,000+ commits). + +However repacking the repository is necessary to improve data +locality and access performance. It can also take hours on extremely +large projects (especially if -f and a large \--window parameter is +used). Since repacking is safe to run alongside readers and writers, +run the repack in the background and let it finish when it finishes. +There is no reason to wait to explore your new Git project! + +If you choose to wait for the repack, don't try to run benchmarks +or performance tests until repacking is completed. fast-import outputs +suboptimal packfiles that are simply never seen in real use +situations. + +Repacking Historical Data +~~~~~~~~~~~~~~~~~~~~~~~~~ +If you are repacking very old imported data (e.g. older than the +last year), consider expending some extra CPU time and supplying +\--window=50 (or higher) when you run gitlink:git-repack[1]. +This will take longer, but will also produce a smaller packfile. +You only need to expend the effort once, and everyone using your +project will benefit from the smaller repository. + + +Packfile Optimization +--------------------- +When packing a blob fast-import always attempts to deltify against the last +blob written. Unless specifically arranged for by the frontend, +this will probably not be a prior version of the same file, so the +generated delta will not be the smallest possible. The resulting +packfile will be compressed, but will not be optimal. + +Frontends which have efficient access to all revisions of a +single file (for example reading an RCS/CVS ,v file) can choose +to supply all revisions of that file as a sequence of consecutive +`blob` commands. This allows fast-import to deltify the different file +revisions against each other, saving space in the final packfile. +Marks can be used to later identify individual file revisions during +a sequence of `commit` commands. + +The packfile(s) created by fast-import do not encourage good disk access +patterns. This is caused by fast-import writing the data in the order +it is received on standard input, while Git typically organizes +data within packfiles to make the most recent (current tip) data +appear before historical data. Git also clusters commits together, +speeding up revision traversal through better cache locality. + +For this reason it is strongly recommended that users repack the +repository with `git repack -a -d` after fast-import completes, allowing +Git to reorganize the packfiles for faster data access. If blob +deltas are suboptimal (see above) then also adding the `-f` option +to force recomputation of all deltas can significantly reduce the +final packfile size (30-50% smaller can be quite typical). + + +Memory Utilization +------------------ +There are a number of factors which affect how much memory fast-import +requires to perform an import. Like critical sections of core +Git, fast-import uses its own memory allocators to ammortize any overheads +associated with malloc. In practice fast-import tends to ammoritize any +malloc overheads to 0, due to its use of large block allocations. + +per object +~~~~~~~~~~ +fast-import maintains an in-memory structure for every object written in +this execution. On a 32 bit system the structure is 32 bytes, +on a 64 bit system the structure is 40 bytes (due to the larger +pointer sizes). Objects in the table are not deallocated until +fast-import terminates. Importing 2 million objects on a 32 bit system +will require approximately 64 MiB of memory. + +The object table is actually a hashtable keyed on the object name +(the unique SHA-1). This storage configuration allows fast-import to reuse +an existing or already written object and avoid writing duplicates +to the output packfile. Duplicate blobs are surprisingly common +in an import, typically due to branch merges in the source. + +per mark +~~~~~~~~ +Marks are stored in a sparse array, using 1 pointer (4 bytes or 8 +bytes, depending on pointer size) per mark. Although the array +is sparse, frontends are still strongly encouraged to use marks +between 1 and n, where n is the total number of marks required for +this import. + +per branch +~~~~~~~~~~ +Branches are classified as active and inactive. The memory usage +of the two classes is significantly different. + +Inactive branches are stored in a structure which uses 96 or 120 +bytes (32 bit or 64 bit systems, respectively), plus the length of +the branch name (typically under 200 bytes), per branch. fast-import will +easily handle as many as 10,000 inactive branches in under 2 MiB +of memory. + +Active branches have the same overhead as inactive branches, but +also contain copies of every tree that has been recently modified on +that branch. If subtree `include` has not been modified since the +branch became active, its contents will not be loaded into memory, +but if subtree `src` has been modified by a commit since the branch +became active, then its contents will be loaded in memory. + +As active branches store metadata about the files contained on that +branch, their in-memory storage size can grow to a considerable size +(see below). + +fast-import automatically moves active branches to inactive status based on +a simple least-recently-used algorithm. The LRU chain is updated on +each `commit` command. The maximum number of active branches can be +increased or decreased on the command line with \--active-branches=. + +per active tree +~~~~~~~~~~~~~~~ +Trees (aka directories) use just 12 bytes of memory on top of the +memory required for their entries (see ``per active file'' below). +The cost of a tree is virtually 0, as its overhead ammortizes out +over the individual file entries. + +per active file entry +~~~~~~~~~~~~~~~~~~~~~ +Files (and pointers to subtrees) within active trees require 52 or 64 +bytes (32/64 bit platforms) per entry. To conserve space, file and +tree names are pooled in a common string table, allowing the filename +``Makefile'' to use just 16 bytes (after including the string header +overhead) no matter how many times it occurs within the project. + +The active branch LRU, when coupled with the filename string pool +and lazy loading of subtrees, allows fast-import to efficiently import +projects with 2,000+ branches and 45,114+ files in a very limited +memory footprint (less than 2.7 MiB per active branch). + + +Author +------ +Written by Shawn O. Pearce <spearce@spearce.org>. + +Documentation +-------------- +Documentation by Shawn O. Pearce <spearce@spearce.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-fetch-pack.txt b/Documentation/git-fetch-pack.txt new file mode 100644 index 0000000000..105d76b0ba --- /dev/null +++ b/Documentation/git-fetch-pack.txt @@ -0,0 +1,93 @@ +git-fetch-pack(1) +================= + +NAME +---- +git-fetch-pack - Receive missing objects from another repository + + +SYNOPSIS +-------- +'git-fetch-pack' [--all] [--quiet|-q] [--keep|-k] [--thin] [--upload-pack=<git-upload-pack>] [--depth=<n>] [-v] [<host>:]<directory> [<refs>...] + +DESCRIPTION +----------- +Usually you would want to use gitlink:git-fetch[1] which is a +higher level wrapper of this command instead. + +Invokes 'git-upload-pack' on a potentially remote repository, +and asks it to send objects missing from this repository, to +update the named heads. The list of commits available locally +is found out by scanning local $GIT_DIR/refs/ and sent to +'git-upload-pack' running on the other end. + +This command degenerates to download everything to complete the +asked refs from the remote side when the local side does not +have a common ancestor commit. + + +OPTIONS +------- +\--all:: + Fetch all remote refs. + +\--quiet, \-q:: + Pass '-q' flag to 'git-unpack-objects'; this makes the + cloning process less verbose. + +\--keep, \-k:: + Do not invoke 'git-unpack-objects' on received data, but + create a single packfile out of it instead, and store it + in the object database. If provided twice then the pack is + locked against repacking. + +\--thin:: + Spend extra cycles to minimize the number of objects to be sent. + Use it on slower connection. + +\--upload-pack=<git-upload-pack>:: + Use this to specify the path to 'git-upload-pack' on the + remote side, if is not found on your $PATH. + Installations of sshd ignores the user's environment + setup scripts for login shells (e.g. .bash_profile) and + your privately installed git may not be found on the system + default $PATH. Another workaround suggested is to set + up your $PATH in ".bashrc", but this flag is for people + who do not want to pay the overhead for non-interactive + shells by having a lean .bashrc file (they set most of + the things up in .bash_profile). + +\--exec=<git-upload-pack>:: + Same as \--upload-pack=<git-upload-pack>. + +\--depth=<n>:: + Limit fetching to ancestor-chains not longer than n. + +\-v:: + Run verbosely. + +<host>:: + A remote host that houses the repository. When this + part is specified, 'git-upload-pack' is invoked via + ssh. + +<directory>:: + The repository to sync from. + +<refs>...:: + The remote heads to update from. This is relative to + $GIT_DIR (e.g. "HEAD", "refs/heads/master"). When + unspecified, update from all heads the remote side has. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-fetch.txt b/Documentation/git-fetch.txt new file mode 100644 index 0000000000..5fbeab76b7 --- /dev/null +++ b/Documentation/git-fetch.txt @@ -0,0 +1,56 @@ +git-fetch(1) +============ + +NAME +---- +git-fetch - Download objects and refs from another repository + + +SYNOPSIS +-------- +'git-fetch' <options> <repository> <refspec>... + + +DESCRIPTION +----------- +Fetches named heads or tags from another repository, along with +the objects necessary to complete them. + +The ref names and their object names of fetched refs are stored +in `.git/FETCH_HEAD`. This information is left for a later merge +operation done by "git merge". + +When <refspec> stores the fetched result in tracking branches, +the tags that point at these branches are automatically +followed. This is done by first fetching from the remote using +the given <refspec>s, and if the repository has objects that are +pointed by remote tags that it does not yet have, then fetch +those missing tags. If the other end has tags that point at +branches you are not interested in, you will not get them. + + +OPTIONS +------- +include::fetch-options.txt[] + +include::pull-fetch-param.txt[] + +include::urls.txt[] + +SEE ALSO +-------- +gitlink:git-pull[1] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and +Junio C Hamano <junkio@cox.net> + +Documentation +------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-fmt-merge-msg.txt b/Documentation/git-fmt-merge-msg.txt new file mode 100644 index 0000000000..a70eb3994a --- /dev/null +++ b/Documentation/git-fmt-merge-msg.txt @@ -0,0 +1,39 @@ +git-fmt-merge-msg(1) +==================== + +NAME +---- +git-fmt-merge-msg - Produce a merge commit message + + +SYNOPSIS +-------- +'git-fmt-merge-msg' <$GIT_DIR/FETCH_HEAD + +DESCRIPTION +----------- +Takes the list of merged objects on stdin and produces a suitable +commit message to be used for the merge commit, usually to be +passed as the '<merge-message>' argument of `git-merge`. + +This script is intended mostly for internal use by scripts +automatically invoking `git-merge`. + + +SEE ALSO +-------- +gitlink:git-merge[1] + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Petr Baudis, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt new file mode 100644 index 0000000000..f49b0d944c --- /dev/null +++ b/Documentation/git-for-each-ref.txt @@ -0,0 +1,185 @@ +git-for-each-ref(1) +=================== + +NAME +---- +git-for-each-ref - Output information on each ref + +SYNOPSIS +-------- +'git-for-each-ref' [--count=<count>]\* [--shell|--perl|--python|--tcl] [--sort=<key>]\* [--format=<format>] [<pattern>] + +DESCRIPTION +----------- + +Iterate over all refs that match `<pattern>` and show them +according to the given `<format>`, after sorting them according +to the given set of `<key>`. If `<max>` is given, stop after +showing that many refs. The interpolated values in `<format>` +can optionally be quoted as string literals in the specified +host language allowing their direct evaluation in that language. + +OPTIONS +------- +<count>:: + By default the command shows all refs that match + `<pattern>`. This option makes it stop after showing + that many refs. + +<key>:: + A field name to sort on. Prefix `-` to sort in + descending order of the value. When unspecified, + `refname` is used. More than one sort keys can be + given. + +<format>:: + A string that interpolates `%(fieldname)` from the + object pointed at by a ref being shown. If `fieldname` + is prefixed with an asterisk (`*`) and the ref points + at a tag object, the value for the field in the object + tag refers is used. When unspecified, defaults to + `%(objectname) SPC %(objecttype) TAB %(refname)`. + It also interpolates `%%` to `%`, and `%xx` where `xx` + are hex digits interpolates to character with hex code + `xx`; for example `%00` interpolates to `\0` (NUL), + `%09` to `\t` (TAB) and `%0a` to `\n` (LF). + +<pattern>:: + If given, the name of the ref is matched against this + using fnmatch(3). Refs that do not match the pattern + are not shown. + +--shell, --perl, --python, --tcl:: + If given, strings that substitute `%(fieldname)` + placeholders are quoted as string literals suitable for + the specified host language. This is meant to produce + a scriptlet that can directly be `eval`ed. + + +FIELD NAMES +----------- + +Various values from structured fields in referenced objects can +be used to interpolate into the resulting output, or as sort +keys. + +For all objects, the following names can be used: + +refname:: + The name of the ref (the part after $GIT_DIR/). + +objecttype:: + The type of the object (`blob`, `tree`, `commit`, `tag`). + +objectsize:: + The size of the object (the same as `git-cat-file -s` reports). + +objectname:: + The object name (aka SHA-1). + +In addition to the above, for commit and tag objects, the header +field names (`tree`, `parent`, `object`, `type`, and `tag`) can +be used to specify the value in the header field. + +Fields that have name-email-date tuple as its value (`author`, +`committer`, and `tagger`) can be suffixed with `name`, `email`, +and `date` to extract the named component. + +The first line of the message in a commit and tag object is +`subject`, the remaining lines are `body`. The whole message +is `contents`. + +For sorting purposes, fields with numeric values sort in numeric +order (`objectsize`, `authordate`, `committerdate`, `taggerdate`). +All other fields are used to sort in their byte-value order. + +In any case, a field name that refers to a field inapplicable to +the object referred by the ref does not cause an error. It +returns an empty string instead. + + +EXAMPLES +-------- + +An example directly producing formatted text. Show the most recent +3 tagged commits:: + +------------ +#!/bin/sh + +git-for-each-ref --count=3 --sort='-*authordate' \ +--format='From: %(*authorname) %(*authoremail) +Subject: %(*subject) +Date: %(*authordate) +Ref: %(*refname) + +%(*body) +' 'refs/tags' +------------ + + +A simple example showing the use of shell eval on the output, +demonstrating the use of --shell. List the prefixes of all heads:: +------------ +#!/bin/sh + +git-for-each-ref --shell --format="ref=%(refname)" refs/heads | \ +while read entry +do + eval "$entry" + echo `dirname $ref` +done +------------ + + +A bit more elaborate report on tags, demonstrating that the format +may be an entire script:: +------------ +#!/bin/sh + +fmt=' + r=%(refname) + t=%(*objecttype) + T=${r#refs/tags/} + + o=%(*objectname) + n=%(*authorname) + e=%(*authoremail) + s=%(*subject) + d=%(*authordate) + b=%(*body) + + kind=Tag + if test "z$t" = z + then + # could be a lightweight tag + t=%(objecttype) + kind="Lightweight tag" + o=%(objectname) + n=%(authorname) + e=%(authoremail) + s=%(subject) + d=%(authordate) + b=%(body) + fi + echo "$kind $T points at a $t object $o" + if test "z$t" = zcommit + then + echo "The commit was authored by $n $e +at $d, and titled + + $s + +Its message reads as: +" + echo "$b" | sed -e "s/^/ /" + echo + fi +' + +eval=`git-for-each-ref --shell --format="$fmt" \ + --sort='*objecttype' \ + --sort=-taggerdate \ + refs/tags` +eval "$eval" +------------ diff --git a/Documentation/git-format-patch.txt b/Documentation/git-format-patch.txt new file mode 100644 index 0000000000..59f34b9f0d --- /dev/null +++ b/Documentation/git-format-patch.txt @@ -0,0 +1,156 @@ +git-format-patch(1) +=================== + +NAME +---- +git-format-patch - Prepare patches for e-mail submission + + +SYNOPSIS +-------- +[verse] +'git-format-patch' [-n | -k] [-o <dir> | --stdout] [--attach] [--thread] + [-s | --signoff] [--diff-options] [--start-number <n>] + [--in-reply-to=Message-Id] [--suffix=.<sfx>] + [--ignore-if-in-upstream] + <since>[..<until>] + +DESCRIPTION +----------- + +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. For a more complete list of ways to spell +<since> and <until>, see "SPECIFYING REVISIONS" section in +gitlink:git-rev-parse[1]. + +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. The names of the output files are printed to standard +output, unless the --stdout option is specified. + +If -o is specified, output files are created in <dir>. Otherwise +they are created in the current working directory. + +If -n is specified, instead of "[PATCH] Subject", the first line +is formatted as "[PATCH n/m] Subject". + +If given --thread, git-format-patch will generate In-Reply-To and +References headers to make the second and subsequent patch mails appear +as replies to the first mail; this also generates a Message-Id header to +reference. + +OPTIONS +------- +-o|--output-directory <dir>:: + Use <dir> to store the resulting files, instead of the + current working directory. + +-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. + +-s|--signoff:: + Add `Signed-off-by:` line to the commit message, using + the committer identity of yourself. + +--stdout:: + 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. + +--thread:: + Add In-Reply-To and References headers to make the second and + subsequent mails appear as replies to the first. Also generates + the Message-Id header to reference. + +--in-reply-to=Message-Id:: + Make the first mail (or all the mails with --no-thread) appear as a + reply to the given Message-Id, which avoids breaking threads to + provide a new patch series. + +--ignore-if-in-upstream:: + Do not include a patch that matches a commit in + <until>..<since>. This will examine all patches reachable + from <since> but not from <until> and compare them with the + patches being generated, and any patch that matches is + ignored. + +--suffix=.<sfx>:: + Instead of using `.patch` as the suffix for generated + filenames, use specifed suffix. A common alternative is + `--suffix=.txt`. ++ +Note that you would need to include the leading dot `.` if you +want a filename like `0001-description-of-my-change.patch`, and +the first letter does not have to be a dot. Leaving it empty would +not add any suffix. + +CONFIGURATION +------------- +You can specify extra mail header lines to be added to each +message in the repository configuration. Also you can specify +the default suffix different from the built-in one: + +------------ +[format] + headers = "Organization: git-foo\n" + suffix = .txt +------------ + + +EXAMPLES +-------- + +git-format-patch -k --stdout R1..R2 | git-am -3 -k:: + Extract commits between revisions R1 and R2, and apply + them on top of the current branch using `git-am` to + cherry-pick them. + +git-format-patch origin:: + 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. 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. + +git-format-patch -3:: + Extract three topmost commits from the current branch + and format them as e-mailable patches. + +See Also +-------- +gitlink:git-am[1], gitlink:git-send-email[1] + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-fsck-objects.txt b/Documentation/git-fsck-objects.txt new file mode 100644 index 0000000000..f21061ecfe --- /dev/null +++ b/Documentation/git-fsck-objects.txt @@ -0,0 +1,17 @@ +git-fsck-objects(1) +=================== + +NAME +---- +git-fsck-objects - Verifies the connectivity and validity of the objects in the database + + +SYNOPSIS +-------- +'git-fsck-objects' ... + +DESCRIPTION +----------- + +This is a synonym for gitlink:git-fsck[1]. Please refer to the +documentation of that command. diff --git a/Documentation/git-fsck.txt b/Documentation/git-fsck.txt new file mode 100644 index 0000000000..058009d2fa --- /dev/null +++ b/Documentation/git-fsck.txt @@ -0,0 +1,139 @@ +git-fsck(1) +=========== + +NAME +---- +git-fsck - Verifies the connectivity and validity of the objects in the database + + +SYNOPSIS +-------- +[verse] +'git-fsck' [--tags] [--root] [--unreachable] [--cache] + [--full] [--strict] [<object>*] + +DESCRIPTION +----------- +Verifies the connectivity and validity of the objects in the database. + +OPTIONS +------- +<object>:: + An object to treat as the head of an unreachability trace. ++ +If no objects are given, git-fsck defaults to using the +index file and all SHA1 references in .git/refs/* as heads. + +--unreachable:: + Print out objects that exist but that aren't readable from any + of the reference nodes. + +--root:: + Report root nodes. + +--tags:: + Report tags. + +--cache:: + Consider any object recorded in the index also as a head node for + an unreachability trace. + +--full:: + Check not just objects in GIT_OBJECT_DIRECTORY + ($GIT_DIR/objects), but also the ones found in alternate + object pools listed in GIT_ALTERNATE_OBJECT_DIRECTORIES + or $GIT_DIR/objects/info/alternates, + and in packed git archives found in $GIT_DIR/objects/pack + and corresponding pack subdirectories in alternate + object pools. + +--strict:: + Enable more strict checking, namely to catch a file mode + recorded with g+w bit set, which was created by older + versions of git. Existing repositories, including the + Linux kernel, git itself, and sparse repository have old + objects that triggers this check, but it is recommended + to check new projects with this flag. + +It tests SHA1 and general object sanity, and it does full tracking of +the resulting reachability and everything else. It prints out any +corruption it finds (missing or bad objects), and if you use the +'--unreachable' flag it will also print out objects that exist but +that aren't readable from any of the specified head nodes. + +So for example + + git-fsck --unreachable HEAD $(cat .git/refs/heads/*) + +will do quite a _lot_ of verification on the tree. There are a few +extra validity tests to be added (make sure that tree objects are +sorted properly etc), but on the whole if "git-fsck" is happy, you +do have a valid tree. + +Any corrupt objects you will have to find in backups or other archives +(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 +evil person, and the end result might be crap. git is a revision +tracking system, not a quality assurance system ;) + +Extracted Diagnostics +--------------------- + +expect dangling commits - potential heads - due to lack of head information:: + You haven't specified any nodes as heads so it won't be + possible to differentiate between un-parented commits and + root nodes. + +missing sha1 directory '<dir>':: + The directory holding the sha1 objects is missing. + +unreachable <type> <object>:: + The <type> object <object>, isn't actually referred to directly + or indirectly in any of the trees or commits seen. This can + mean that there's another root node that you're not specifying + or that the tree is corrupt. If you haven't missed a root node + then you might as well delete unreachable nodes since they + can't be used. + +missing <type> <object>:: + The <type> object <object>, is referred to but isn't present in + the database. + +dangling <type> <object>:: + The <type> object <object>, is present in the database but never + 'directly' used. A dangling commit could be a root node. + +warning: git-fsck: tree <tree> has full pathnames in it:: + And it shouldn't... + +sha1 mismatch <object>:: + The database has an object who's sha1 doesn't match the + database value. + This indicates a serious data integrity problem. + +Environment Variables +--------------------- + +GIT_OBJECT_DIRECTORY:: + used to specify the object database root (usually $GIT_DIR/objects) + +GIT_INDEX_FILE:: + used to specify the index file of the index + +GIT_ALTERNATE_OBJECT_DIRECTORIES:: + used to specify additional object database roots (usually unset) + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-gc.txt b/Documentation/git-gc.txt new file mode 100644 index 0000000000..e37758ad15 --- /dev/null +++ b/Documentation/git-gc.txt @@ -0,0 +1,79 @@ +git-gc(1) +========= + +NAME +---- +git-gc - Cleanup unnecessary files and optimize the local repository + + +SYNOPSIS +-------- +'git-gc' [--prune] + +DESCRIPTION +----------- +Runs a number of housekeeping tasks within the current repository, +such as compressing file revisions (to reduce disk space and increase +performance) and removing unreachable objects which may have been +created from prior invocations of gitlink:git-add[1]. + +Users are encouraged to run this task on a regular basis within +each repository to maintain good disk space utilization and good +operating performance. + +OPTIONS +------- + +--prune:: + Usually `git-gc` packs refs, expires old reflog entries, + packs loose objects, + and removes old 'rerere' records. Removal + of unreferenced loose objects is an unsafe operation + while other git operations are in progress, so it is not + done by default. Pass this option if you want it, and only + when you know nobody else is creating new objects in the + repository at the same time (e.g. never use this option + in a cron script). + + +Configuration +------------- + +The optional configuration variable 'gc.reflogExpire' can be +set to indicate how long historical entries within each branch's +reflog should remain available in this repository. The setting is +expressed as a length of time, for example '90 days' or '3 months'. +It defaults to '90 days'. + +The optional configuration variable 'gc.reflogExpireUnreachable' +can be set to indicate how long historical reflog entries which +are not part of the current branch should remain available in +this repository. These types of entries are generally created as +a result of using `git commit \--amend` or `git rebase` and are the +commits prior to the amend or rebase occurring. Since these changes +are not part of the current project most users will want to expire +them sooner. This option defaults to '30 days'. + +The optional configuration variable 'gc.rerereresolved' indicates +how long records of conflicted merge you resolved earlier are +kept. This defaults to 60 days. + +The optional configuration variable 'gc.rerereunresolved' indicates +how long records of conflicted merge you have not resolved are +kept. This defaults to 15 days. + + +See Also +-------- +gitlink:git-prune[1] +gitlink:git-reflog[1] +gitlink:git-repack[1] +gitlink:git-rerere[1] + +Author +------ +Written by Shawn O. Pearce <spearce@spearce.org> + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-get-tar-commit-id.txt b/Documentation/git-get-tar-commit-id.txt new file mode 100644 index 0000000000..48805b651c --- /dev/null +++ b/Documentation/git-get-tar-commit-id.txt @@ -0,0 +1,37 @@ +git-get-tar-commit-id(1) +======================== + +NAME +---- +git-get-tar-commit-id - Extract commit ID from an archive created using git-tar-tree + + +SYNOPSIS +-------- +'git-get-tar-commit-id' < <tarfile> + + +DESCRIPTION +----------- +Acts as a filter, extracting the commit ID stored in archives created by +git-tar-tree. It reads only the first 1024 bytes of input, thus its +runtime is not influenced by the size of <tarfile> very much. + +If no commit ID is found, git-get-tar-commit-id quietly exists with a +return code of 1. This can happen if <tarfile> had not been created +using git-tar-tree or if the first parameter of git-tar-tree had been +a tree ID instead of a commit ID or tag. + + +Author +------ +Written by Rene Scharfe <rene.scharfe@lsrfire.ath.cx> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-grep.txt b/Documentation/git-grep.txt new file mode 100644 index 0000000000..0140c8e358 --- /dev/null +++ b/Documentation/git-grep.txt @@ -0,0 +1,136 @@ +git-grep(1) +=========== + +NAME +---- +git-grep - Print lines matching a pattern + + +SYNOPSIS +-------- +[verse] +'git-grep' [--cached] + [-a | --text] [-I] [-i | --ignore-case] [-w | --word-regexp] + [-v | --invert-match] [-h|-H] [--full-name] + [-E | --extended-regexp] [-G | --basic-regexp] [-F | --fixed-strings] + [-n] [-l | --files-with-matches] [-L | --files-without-match] + [-c | --count] [--all-match] + [-A <post-context>] [-B <pre-context>] [-C <context>] + [-f <file>] [-e] <pattern> [--and|--or|--not|(|)|-e <pattern>...] + [<tree>...] + [--] [<path>...] + +DESCRIPTION +----------- +Look for specified patterns in the working tree files, blobs +registered in the index file, or given tree objects. + + +OPTIONS +------- +--cached:: + Instead of searching in the working tree files, check + the blobs registered in the index file. + +-a | --text:: + Process binary files as if they were text. + +-i | --ignore-case:: + Ignore case differences between the patterns and the + files. + +-w | --word-regexp:: + Match the pattern only at word boundary (either begin at the + beginning of a line, or preceded by a non-word character; end at + the end of a line or followed by a non-word character). + +-v | --invert-match:: + Select non-matching lines. + +-h | -H:: + By default, the command shows the filename for each + match. `-h` option is used to suppress this output. + `-H` is there for completeness and does not do anything + except it overrides `-h` given earlier on the command + line. + +--full-name:: + When run from a subdirectory, the command usually + outputs paths relative to the current directory. This + option forces paths to be output relative to the project + top directory. + +-E | --extended-regexp | -G | --basic-regexp:: + Use POSIX extended/basic regexp for patterns. Default + is to use basic regexp. + +-n:: + Prefix the line number to matching lines. + +-l | --files-with-matches | -L | --files-without-match:: + Instead of showing every matched line, show only the + names of files that contain (or do not contain) matches. + +-c | --count:: + Instead of showing every matched line, show the number of + lines that match. + +-[ABC] <context>:: + Show `context` trailing (`A` -- after), or leading (`B` + -- before), or both (`C` -- context) lines, and place a + line containing `--` between contiguous groups of + matches. + +-f <file>:: + Read patterns from <file>, one per line. + +-e:: + The next parameter is the pattern. This option has to be + used for patterns starting with - and should be used in + scripts passing user input to grep. Multiple patterns are + combined by 'or'. + +--and | --or | --not | ( | ):: + Specify how multiple patterns are combined using Boolean + expressions. `--or` is the default operator. `--and` has + higher precedence than `--or`. `-e` has to be used for all + patterns. + +--all-match:: + When giving multiple pattern expressions combined with `--or`, + this flag is specified to limit the match to files that + have lines to match all of them. + +`<tree>...`:: + Search blobs in the trees for specified patterns. + +\--:: + Signals the end of options; the rest of the parameters + are <path> limiters. + + +Example +------- + +git grep -e \'#define\' --and \( -e MAX_PATH -e PATH_MAX \):: + Looks for a line that has `#define` and either `MAX_PATH` or + `PATH_MAX`. + +git grep --all-match -e NODE -e Unexpected:: + Looks for a line that has `NODE` or `Unexpected` in + files that have lines that match both. + +Author +------ +Originally written by Linus Torvalds <torvalds@osdl.org>, later +revamped by Junio C Hamano. + + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-hash-object.txt b/Documentation/git-hash-object.txt new file mode 100644 index 0000000000..5edc36f060 --- /dev/null +++ b/Documentation/git-hash-object.txt @@ -0,0 +1,46 @@ +git-hash-object(1) +================== + +NAME +---- +git-hash-object - Compute object ID and optionally creates a blob from a file + + +SYNOPSIS +-------- +'git-hash-object' [-t <type>] [-w] [--stdin] [--] <file>... + +DESCRIPTION +----------- +Computes the object ID value for an object with specified type +with the contents of the named file (which can be outside of the +work tree), and optionally writes the resulting object into the +object database. Reports its object ID to its standard output. +This is used by "git-cvsimport" to update the index +without modifying files in the work tree. When <type> is not +specified, it defaults to "blob". + +OPTIONS +------- + +-t <type>:: + Specify the type (default: "blob"). + +-w:: + Actually write the object into the object database. + +--stdin:: + Read the object from standard input instead of from a file. + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-http-fetch.txt b/Documentation/git-http-fetch.txt new file mode 100644 index 0000000000..7dc2df3044 --- /dev/null +++ b/Documentation/git-http-fetch.txt @@ -0,0 +1,53 @@ +git-http-fetch(1) +================= + +NAME +---- +git-http-fetch - Download from a remote git repository via HTTP + + +SYNOPSIS +-------- +'git-http-fetch' [-c] [-t] [-a] [-d] [-v] [-w filename] [--recover] [--stdin] <commit> <url> + +DESCRIPTION +----------- +Downloads a remote git repository via HTTP. + +OPTIONS +------- +commit-id:: + Either the hash or the filename under [URL]/refs/ to + pull. + +-c:: + Get the commit objects. +-t:: + Get trees associated with the commit objects. +-a:: + Get all the objects. +-v:: + Report what is downloaded. + +-w <filename>:: + Writes the commit-id into the filename under $GIT_DIR/refs/<filename> on + the local end after the transfer is complete. + +--stdin:: + Instead of a commit id on the commandline (which is not expected in this + case), 'git-http-fetch' expects lines on stdin in the format + + <commit-id>['\t'<filename-as-in--w>] + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-http-push.txt b/Documentation/git-http-push.txt new file mode 100644 index 0000000000..4b4a46169c --- /dev/null +++ b/Documentation/git-http-push.txt @@ -0,0 +1,89 @@ +git-http-push(1) +================ + +NAME +---- +git-http-push - Push objects over HTTP/DAV to another repository + + +SYNOPSIS +-------- +'git-http-push' [--complete] [--force] [--verbose] <url> <ref> [<ref>...] + +DESCRIPTION +----------- +Sends missing objects to remote repository, and updates the +remote branch. + + +OPTIONS +------- +--complete:: + Do not assume that the remote repository is complete in its + current state, and verify all objects in the entire local + ref's history exist in the remote repository. + +--force:: + Usually, the command refuses to update a remote ref that + is not an ancestor of the local ref used to overwrite it. + This flag disables the check. What this means is that + the remote repository can lose commits; use it with + care. + +--verbose:: + Report the list of objects being walked locally and the + list of objects successfully sent to the remote repository. + +<ref>...:: + The remote refs to update. + + +Specifying the Refs +------------------- + +A '<ref>' specification can be either a single pattern, or a pair +of such patterns separated by a colon ":" (this means that a ref name +cannot have a colon in it). A single pattern '<name>' is just a +shorthand for '<name>:<name>'. + +Each pattern pair consists of the source side (before the colon) +and the destination side (after the colon). The ref to be +pushed is determined by finding a match that matches the source +side, and where it is pushed is determined by using the +destination side. + + - It is an error if <src> does not match exactly one of the + local refs. + + - If <dst> does not match any remote ref, either + + * it has to start with "refs/"; <dst> is used as the + destination literally in this case. + + * <src> == <dst> and the ref that matched the <src> must not + exist in the set of remote refs; the ref matched <src> + locally is used as the name of the destination. + +Without '--force', the <src> ref is stored at the remote only if +<dst> does not exist, or <dst> is a proper subset (i.e. an +ancestor) of <src>. This check, known as "fast forward check", +is performed in order to avoid accidentally overwriting the +remote ref and lose other peoples' commits from there. + +With '--force', the fast forward check is disabled for all refs. + +Optionally, a <ref> parameter can be prefixed with a plus '+' sign +to disable the fast-forward check only on that ref. + + +Author +------ +Written by Nick Hengeveld <nickh@reactrix.com> + +Documentation +-------------- +Documentation by Nick Hengeveld + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-imap-send.txt b/Documentation/git-imap-send.txt new file mode 100644 index 0000000000..eca9e9ccef --- /dev/null +++ b/Documentation/git-imap-send.txt @@ -0,0 +1,62 @@ +git-imap-send(1) +================ + +NAME +---- +git-imap-send - Dump a mailbox from stdin into an imap folder + + +SYNOPSIS +-------- +'git-imap-send' + + +DESCRIPTION +----------- +This command uploads a mailbox generated with git-format-patch +into an imap drafts folder. This allows patches to be sent as +other email is sent with mail clients that cannot read mailbox +files directly. + +Typical usage is something like: + +git-format-patch --signoff --stdout --attach origin | git-imap-send + + +CONFIGURATION +------------- + +git-imap-send requires the following values in the repository +configuration file (shown with examples): + +.......................... +[imap] + Folder = "INBOX.Drafts" + +[imap] + Tunnel = "ssh -q user@server.com /usr/bin/imapd ./Maildir 2> /dev/null" + +[imap] + Host = imap.server.com + User = bob + Pass = pwd + Port = 143 +.......................... + + +BUGS +---- +Doesn't handle lines starting with "From " in the message body. + + +Author +------ +Derived from isync 1.0.1 by Mike McCormack. + +Documentation +-------------- +Documentation by Mike McCormack + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-index-pack.txt b/Documentation/git-index-pack.txt new file mode 100644 index 0000000000..2229ee86b7 --- /dev/null +++ b/Documentation/git-index-pack.txt @@ -0,0 +1,94 @@ +git-index-pack(1) +================= + +NAME +---- +git-index-pack - Build pack index file for an existing packed archive + + +SYNOPSIS +-------- +'git-index-pack' [-v] [-o <index-file>] <pack-file> +'git-index-pack' --stdin [--fix-thin] [--keep] [-v] [-o <index-file>] [<pack-file>] + + +DESCRIPTION +----------- +Reads a packed archive (.pack) from the specified file, and +builds a pack index file (.idx) for it. The packed archive +together with the pack index can then be placed in the +objects/pack/ directory of a git repository. + + +OPTIONS +------- +-v:: + Be verbose about what is going on, including progress status. + +-o <index-file>:: + Write the generated pack index into the specified + file. Without this option the name of pack index + file is constructed from the name of packed archive + file by replacing .pack with .idx (and the program + fails if the name of packed archive does not end + with .pack). + +--stdin:: + When this flag is provided, the pack is read from stdin + instead and a copy is then written to <pack-file>. If + <pack-file> is not specified, the pack is written to + objects/pack/ directory of the current git repository with + a default name determined from the pack content. If + <pack-file> is not specified consider using --keep to + prevent a race condition between this process and + gitlink::git-repack[1] . + +--fix-thin:: + It is possible for gitlink:git-pack-objects[1] to build + "thin" pack, which records objects in deltified form based on + objects not included in the pack to reduce network traffic. + Those objects are expected to be present on the receiving end + and they must be included in the pack for that pack to be self + contained and indexable. Without this option any attempt to + index a thin pack will fail. This option only makes sense in + conjunction with --stdin. + +--keep:: + Before moving the index into its final destination + create an empty .keep file for the associated pack file. + This option is usually necessary with --stdin to prevent a + simultaneous gitlink:git-repack[1] process from deleting + the newly constructed pack and index before refs can be + updated to use objects contained in the pack. + +--keep='why':: + Like --keep create a .keep file before moving the index into + its final destination, but rather than creating an empty file + place 'why' followed by an LF into the .keep file. The 'why' + message can later be searched for within all .keep files to + locate any which have outlived their usefulness. + + +Note +---- + +Once the index has been created, the list of object names is sorted +and the SHA1 hash of that list is printed to stdout. If --stdin was +also used then this is prefixed by either "pack\t", or "keep\t" if a +new .keep file was successfully created. This is useful to remove a +.keep file used as a lock to prevent the race with gitlink:git-repack[1] +mentioned above. + + +Author +------ +Written by Sergey Vlasov <vsu@altlinux.ru> + +Documentation +------------- +Documentation by Sergey Vlasov + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-init-db.txt b/Documentation/git-init-db.txt new file mode 100644 index 0000000000..5412135d76 --- /dev/null +++ b/Documentation/git-init-db.txt @@ -0,0 +1,19 @@ +git-init-db(1) +============== + +NAME +---- +git-init-db - Creates an empty git repository + + +SYNOPSIS +-------- +'git-init-db' [--template=<template_directory>] [--shared[=<permissions>]] + + +DESCRIPTION +----------- + +This is a synonym for gitlink:git-init[1]. Please refer to the +documentation of that command. + diff --git a/Documentation/git-init.txt b/Documentation/git-init.txt new file mode 100644 index 0000000000..1b64d3ab03 --- /dev/null +++ b/Documentation/git-init.txt @@ -0,0 +1,111 @@ +git-init(1) +=========== + +NAME +---- +git-init - Create an empty git repository or reinitialize an existing one + + +SYNOPSIS +-------- +'git-init' [--template=<template_directory>] [--shared[=<permissions>]] + + +OPTIONS +------- + +-- + +--template=<template_directory>:: + +Provide the directory from which templates will be used. The default template +directory is `/usr/share/git-core/templates`. + +When specified, `<template_directory>` is used as the source of the template +files rather than the default. The template files include some directory +structure, some suggested "exclude patterns", and copies of non-executing +"hook" files. The suggested patterns and hook files are all modifiable and +extensible. + +--shared[={false|true|umask|group|all|world|everybody}]:: + +Specify that the git repository is to be shared amongst several users. This +allows users belonging to the same group to push into that +repository. When specified, the config variable "core.sharedRepository" is +set so that files and directories under `$GIT_DIR` are created with the +requested permissions. When not specified, git will use permissions reported +by umask(2). + +The option can have the following values, defaulting to 'group' if no value +is given: + + - 'umask' (or 'false'): Use permissions reported by umask(2). The default, + when `--shared` is not specified. + + - 'group' (or 'true'): Make the repository group-writable, (and g+sx, since + the git group may be not the primary group of all users). + + - 'all' (or 'world' or 'everybody'): Same as 'group', but make the repository + readable by all users. + +By default, the configuration flag receive.denyNonFastforward is enabled +in shared repositories, so that you cannot force a non fast-forwarding push +into it. + +-- + + +DESCRIPTION +----------- +This command creates an empty git repository - basically a `.git` directory +with subdirectories for `objects`, `refs/heads`, `refs/tags`, and +template files. +An initial `HEAD` file that references the HEAD of the master branch +is also created. + +If the `$GIT_DIR` environment variable is set then it specifies a path +to use instead of `./.git` for the base of the repository. + +If the object storage directory is specified via the `$GIT_OBJECT_DIRECTORY` +environment variable then the sha1 directories are created underneath - +otherwise the default `$GIT_DIR/objects` directory is used. + +Running `git-init` in an existing repository is safe. It will not overwrite +things that are already there. The primary reason for rerunning `git-init` +is to pick up newly added templates. + +Note that `git-init` is the same as `git-init-db`. The command +was primarily meant to initialize the object database, but over +time it has become responsible for setting up the other aspects +of the repository, such as installing the default hooks and +setting the configuration variables. The old name is retained +for backward compatibility reasons. + + +EXAMPLES +-------- + +Start a new git repository for an existing code base:: ++ +---------------- +$ cd /path/to/my/codebase +$ git-init <1> +$ git-add . <2> +---------------- ++ +<1> prepare /path/to/my/codebase/.git directory +<2> add all existing file to the index + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-instaweb.txt b/Documentation/git-instaweb.txt new file mode 100644 index 0000000000..52a6aa6e82 --- /dev/null +++ b/Documentation/git-instaweb.txt @@ -0,0 +1,84 @@ +git-instaweb(1) +=============== + +NAME +---- +git-instaweb - Instantly browse your working repository in gitweb + +SYNOPSIS +-------- +'git-instaweb' [--local] [--httpd=<httpd>] [--port=<port>] [--browser=<browser>] + +'git-instaweb' [--start] [--stop] [--restart] + +DESCRIPTION +----------- +A simple script to setup gitweb and a web server for browsing the local +repository. + +OPTIONS +------- + +-l|--local:: + Only bind the web server to the local IP (127.0.0.1). + +-d|--httpd:: + The HTTP daemon command-line that will be executed. + Command-line options may be specified here, and the + configuration file will be added at the end of the command-line. + Currently, lighttpd and apache2 are the only supported servers. + (Default: lighttpd) + +-m|--module-path:: + The module path (only needed if httpd is Apache). + (Default: /usr/lib/apache2/modules) + +-p|--port:: + The port number to bind the httpd to. (Default: 1234) + +-b|--browser:: + + The web browser command-line to execute to view the gitweb page. + If blank, the URL of the gitweb instance will be printed to + stdout. (Default: 'firefox') + +--start:: + Start the httpd instance and exit. This does not generate + any of the configuration files for spawning a new instance. + +--stop:: + Stop the httpd instance and exit. This does not generate + any of the configuration files for spawning a new instance, + nor does it close the browser. + +--restart:: + Restart the httpd instance and exit. This does not generate + any of the configuration files for spawning a new instance. + +CONFIGURATION +------------- + +You may specify configuration in your .git/config + +----------------------------------------------------------------------- +[instaweb] + local = true + httpd = apache2 -f + port = 4321 + browser = konqueror + modulepath = /usr/lib/apache2/modules + +----------------------------------------------------------------------- + +Author +------ +Written by Eric Wong <normalperson@yhbt.net> + +Documentation +-------------- +Documentation by Eric Wong <normalperson@yhbt.net>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-local-fetch.txt b/Documentation/git-local-fetch.txt new file mode 100644 index 0000000000..22048d82bd --- /dev/null +++ b/Documentation/git-local-fetch.txt @@ -0,0 +1,49 @@ +git-local-fetch(1) +================== + +NAME +---- +git-local-fetch - Duplicate another git repository on a local system + + +SYNOPSIS +-------- +'git-local-fetch' [-c] [-t] [-a] [-d] [-v] [-w filename] [--recover] [-l] [-s] [-n] commit-id path + +DESCRIPTION +----------- +Duplicates another git repository on a local system. + +OPTIONS +------- +-c:: + Get the commit objects. +-t:: + Get trees associated with the commit objects. +-a:: + Get all the objects. +-v:: + Report what is downloaded. + +-w <filename>:: + Writes the commit-id into the filename under $GIT_DIR/refs/<filename> on + the local end after the transfer is complete. + +--stdin:: + Instead of a commit id on the commandline (which is not expected in this + case), 'git-local-fetch' expects lines on stdin in the format + + <commit-id>['\t'<filename-as-in--w>] + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-log.txt b/Documentation/git-log.txt new file mode 100644 index 0000000000..361eaec700 --- /dev/null +++ b/Documentation/git-log.txt @@ -0,0 +1,88 @@ +git-log(1) +========== + +NAME +---- +git-log - Show commit logs + + +SYNOPSIS +-------- +'git-log' <option>... + +DESCRIPTION +----------- +Shows the commit logs. + +The command takes options applicable to the gitlink:git-rev-list[1] +command to control what is shown and how, and options applicable to +the gitlink:git-diff-tree[1] commands to control how the changes +each commit introduces are shown. + +This manual page describes only the most frequently used options. + + +OPTIONS +------- + +include::pretty-formats.txt[] + +-<n>:: + Limits the number of commits to show. + +<since>..<until>:: + Show only commits between the named two commits. When + either <since> or <until> is omitted, it defaults to + `HEAD`, i.e. the tip of the current branch. + For a more complete list of ways to spell <since> + and <until>, see "SPECIFYING REVISIONS" section in + gitlink:git-rev-parse[1]. + +-p:: + Show the change the commit introduces in a patch form. + +<paths>...:: + Show only commits that affect the specified paths. + + +Examples +-------- +git log --no-merges:: + + Show the whole commit history, but skip any merges + +git log v2.6.12.. include/scsi drivers/scsi:: + + Show all commits since version 'v2.6.12' that changed any file + in the include/scsi or drivers/scsi subdirectories + +git log --since="2 weeks ago" \-- gitk:: + + Show the changes during the last two weeks to the file 'gitk'. + The "--" is necessary to avoid confusion with the *branch* named + 'gitk' + +git log -r --name-status release..test:: + + Show the commits that are in the "test" branch but not yet + in the "release" branch, along with the list of paths + each commit modifies. + +Discussion +---------- + +include::i18n.txt[] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-lost-found.txt b/Documentation/git-lost-found.txt new file mode 100644 index 0000000000..f52a9d7f68 --- /dev/null +++ b/Documentation/git-lost-found.txt @@ -0,0 +1,78 @@ +git-lost-found(1) +================= + +NAME +---- +git-lost-found - Recover lost refs that luckily have not yet been pruned + +SYNOPSIS +-------- +'git-lost-found' + +DESCRIPTION +----------- +Finds dangling commits and tags from the object database, and +creates refs to them in .git/lost-found/ directory. Commits and +tags that dereference to commits go to .git/lost-found/commit +and others are stored in .git/lost-found/other directory. + + +OUTPUT +------ +One line description from the commit and tag found along with +their object name are printed on the standard output. + + +EXAMPLE +------- + +Suppose you run 'git tag -f' and mistyped the tag to overwrite. +The ref to your tag is overwritten, but until you run 'git +prune', it is still there. + +------------ +$ git lost-found +[1ef2b196d909eed523d4f3c9bf54b78cdd6843c6] GIT 0.99.9c +... +------------ + +Also you can use gitk to browse how they relate to each other +and existing (probably old) tags. + +------------ +$ gitk $(cd .git/lost-found/commit && echo ??*) +------------ + +After making sure that it is the object you are looking for, you +can reconnect it to your regular .git/refs hierarchy. + +------------ +$ git cat-file -t 1ef2b196 +tag +$ git cat-file tag 1ef2b196 +object fa41bbce8e38c67a218415de6cfa510c7e50032a +type commit +tag v0.99.9c +tagger Junio C Hamano <junkio@cox.net> 1131059594 -0800 + +GIT 0.99.9c + +This contains the following changes from the "master" branch, since +... +$ git update-ref refs/tags/not-lost-anymore 1ef2b196 +$ git rev-parse not-lost-anymore +1ef2b196d909eed523d4f3c9bf54b78cdd6843c6 +------------ + +Author +------ +Written by Junio C Hamano 濱野 ç´” <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt new file mode 100644 index 0000000000..79e0b7b71a --- /dev/null +++ b/Documentation/git-ls-files.txt @@ -0,0 +1,254 @@ +git-ls-files(1) +=============== + +NAME +---- +git-ls-files - Show information about files in the index and the working tree + + +SYNOPSIS +-------- +[verse] +'git-ls-files' [-z] [-t] [-v] + (--[cached|deleted|others|ignored|stage|unmerged|killed|modified])\* + (-[c|d|o|i|s|u|k|m])\* + [-x <pattern>|--exclude=<pattern>] + [-X <file>|--exclude-from=<file>] + [--exclude-per-directory=<file>] + [--error-unmatch] + [--full-name] [--abbrev] [--] [<file>]\* + +DESCRIPTION +----------- +This merges the file listing in the directory cache index with the +actual working directory list, and shows different combinations of the +two. + +One or more of the options below may be used to determine the files +shown: + +OPTIONS +------- +-c|--cached:: + Show cached files in the output (default) + +-d|--deleted:: + Show deleted files in the output + +-m|--modified:: + Show modified files in the output + +-o|--others:: + Show other files in the output + +-i|--ignored:: + Show ignored files in the output + Note the this also reverses any exclude list present. + +-s|--stage:: + Show stage files in the output + +--directory:: + If a whole directory is classified as "other", show just its + name (with a trailing slash) and not its whole contents. + +--no-empty-directory:: + Do not list empty directories. Has no effect without --directory. + +-u|--unmerged:: + Show unmerged files in the output (forces --stage) + +-k|--killed:: + Show files on the filesystem that need to be removed due + to file/directory conflicts for checkout-index to + succeed. + +-z:: + \0 line termination on output. + +-x|--exclude=<pattern>:: + Skips files matching pattern. + Note that pattern is a shell wildcard pattern. + +-X|--exclude-from=<file>:: + exclude patterns are read from <file>; 1 per line. + +--exclude-per-directory=<file>:: + read additional exclude patterns that apply only to the + directory and its subdirectories in <file>. + +--error-unmatch:: + If any <file> does not appear in the index, treat this as an + error (return 1). + +-t:: + Identify the file status with the following tags (followed by + a space) at the start of each line: + H:: cached + M:: unmerged + R:: removed/deleted + C:: modified/changed + K:: to be killed + ?:: other + +-v:: + Similar to `-t`, but use lowercase letters for files + that are marked as 'always matching index'. + +--full-name:: + When run from a subdirectory, the command usually + outputs paths relative to the current directory. This + option forces paths to be output relative to the project + top directory. + +--abbrev[=<n>]:: + Instead of showing the full 40-byte hexadecimal object + lines, show only handful hexdigits prefix. + Non default number of digits can be specified with --abbrev=<n>. + +\--:: + Do not interpret any more arguments as options. + +<file>:: + Files to show. If no files are given all files which match the other + specified criteria are shown. + +Output +------ +show files just outputs the filename unless '--stage' is specified in +which case it outputs: + + [<tag> ]<mode> <object> <stage> <file> + +"git-ls-files --unmerged" and "git-ls-files --stage" can be used to examine +detailed information on unmerged paths. + +For an unmerged path, instead of recording a single mode/SHA1 pair, +the dircache records up to three such pairs; one from tree O in stage +1, A in stage 2, and B in stage 3. This information can be used by +the user (or the porcelain) to see what should eventually be recorded at the +path. (see git-read-tree for more information on state) + +When `-z` option is not used, TAB, LF, and backslash characters +in pathnames are represented as `\t`, `\n`, and `\\`, +respectively. + + +Exclude Patterns +---------------- + +'git-ls-files' can use a list of "exclude patterns" when +traversing the directory tree and finding files to show when the +flags --others or --ignored are specified. + +These exclude patterns come from these places: + + 1. command line flag --exclude=<pattern> specifies a single + pattern. + + 2. command line flag --exclude-from=<file> specifies a list of + patterns stored in a file. + + 3. command line flag --exclude-per-directory=<name> specifies + a name of the file in each directory 'git-ls-files' + examines, and if exists, its contents are used as an + additional list of patterns. + +An exclude pattern file used by (2) and (3) contains one pattern +per line. A line that starts with a '#' can be used as comment +for readability. + +There are three lists of patterns that are in effect at a given +time. They are built and ordered in the following way: + + * --exclude=<pattern> from the command line; patterns are + ordered in the same order as they appear on the command line. + + * lines read from --exclude-from=<file>; patterns are ordered + in the same order as they appear in the file. + + * When --exclude-per-directory=<name> is specified, upon + entering a directory that has such a file, its contents are + appended at the end of the current "list of patterns". They + are popped off when leaving the directory. + +Each pattern in the pattern list specifies "a match pattern" and +optionally the fate; either a file that matches the pattern is +considered excluded or included. A filename is matched against +the patterns in the three lists; the --exclude-from list is +checked first, then the --exclude-per-directory list, and then +finally the --exclude list. The last match determines its fate. +If there is no match in the three lists, the fate is "included". + +A pattern specified on the command line with --exclude or read +from the file specified with --exclude-from is relative to the +top of the directory tree. A pattern read from a file specified +by --exclude-per-directory is relative to the directory that the +pattern file appears in. + +An exclude pattern is of the following format: + + - an optional prefix '!' which means that the fate this pattern + specifies is "include", not the usual "exclude"; the + remainder of the pattern string is interpreted according to + the following rules. + + - if it does not contain a slash '/', it is a shell glob + pattern and used to match against the filename without + leading directories. + + - otherwise, it is a shell glob pattern, suitable for + consumption by fnmatch(3) with FNM_PATHNAME flag. I.e. a + slash in the pattern must match a slash in the pathname. + "Documentation/\*.html" matches "Documentation/git.html" but + not "ppc/ppc.html". As a natural exception, "/*.c" matches + "cat-file.c" but not "mozilla-sha1/sha1.c". + +An example: + +-------------------------------------------------------------- + $ cat .git/info/exclude + # ignore objects and archives, anywhere in the tree. + *.[oa] + $ cat Documentation/.gitignore + # ignore generated html files, + *.html + # except foo.html which is maintained by hand + !foo.html + $ git-ls-files --ignored \ + --exclude='Documentation/*.[0-9]' \ + --exclude-from=.git/info/exclude \ + --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 +-------- +gitlink:git-read-tree[1] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-ls-remote.txt b/Documentation/git-ls-remote.txt new file mode 100644 index 0000000000..c254005ca3 --- /dev/null +++ b/Documentation/git-ls-remote.txt @@ -0,0 +1,73 @@ +git-ls-remote(1) +================ + +NAME +---- +git-ls-remote - List references in a remote repository + + +SYNOPSIS +-------- +[verse] +'git-ls-remote' [--heads] [--tags] [-u <exec> | --upload-pack <exec>] + <repository> <refs>... + +DESCRIPTION +----------- +Displays references available in a remote repository along with the associated +commit IDs. + + +OPTIONS +------- +-h|--heads, -t|--tags:: + Limit to only refs/heads and refs/tags, respectively. + These options are _not_ mutually exclusive; when given + both, references stored in refs/heads and refs/tags are + displayed. + +-u <exec>, --upload-pack=<exec>:: + Specify the full path of gitlink:git-upload-pack[1] on the remote + host. This allows listing references from repositories accessed via + SSH and where the SSH daemon does not use the PATH configured by the + user. Also see the '--exec' option for gitlink:git-peek-remote[1]. + +<repository>:: + Location of the repository. The shorthand defined in + $GIT_DIR/branches/ can be used. Use "." (dot) to list references in + the local repository. + +<refs>...:: + When unspecified, all references, after filtering done + with --heads and --tags, are shown. When <refs>... are + specified, only references matching the given patterns + are displayed. + +EXAMPLES +-------- + + $ git ls-remote --tags ./. + d6602ec5194c87b0fc87103ca4d67251c76f233a refs/tags/v0.99 + f25a265a342aed6041ab0cc484224d9ca54b6f41 refs/tags/v0.99.1 + 7ceca275d047c90c0c7d5afb13ab97efdf51bd6e refs/tags/v0.99.3 + c5db5456ae3b0873fc659c19fafdde22313cc441 refs/tags/v0.99.2 + 0918385dbd9656cab0d1d81ba7453d49bbc16250 refs/tags/junio-gpg-pub + $ git ls-remote http://www.kernel.org/pub/scm/git/git.git master pu rc + 5fe978a5381f1fbad26a80e682ddd2a401966740 refs/heads/master + c781a84b5204fb294c9ccc79f8b3baceeb32c061 refs/heads/pu + b1d096f2926c4e37c9c0b6a7bf2119bedaa277cb refs/heads/rc + $ echo http://www.kernel.org/pub/scm/git/git.git >.git/branches/public + $ git ls-remote --tags public v\* + d6602ec5194c87b0fc87103ca4d67251c76f233a refs/tags/v0.99 + f25a265a342aed6041ab0cc484224d9ca54b6f41 refs/tags/v0.99.1 + c5db5456ae3b0873fc659c19fafdde22313cc441 refs/tags/v0.99.2 + 7ceca275d047c90c0c7d5afb13ab97efdf51bd6e refs/tags/v0.99.3 + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-ls-tree.txt b/Documentation/git-ls-tree.txt new file mode 100644 index 0000000000..7899394081 --- /dev/null +++ b/Documentation/git-ls-tree.txt @@ -0,0 +1,83 @@ +git-ls-tree(1) +============== + +NAME +---- +git-ls-tree - List the contents of a tree object + + +SYNOPSIS +-------- +[verse] +'git-ls-tree' [-d] [-r] [-t] [-z] + [--name-only] [--name-status] [--full-name] [--abbrev=[<n>]] + <tree-ish> [paths...] + +DESCRIPTION +----------- +Lists the contents of a given tree object, like what "/bin/ls -a" does +in the current working directory. Note that the usage is subtly different, +though - 'paths' denote just a list of patterns to match, e.g. so specifying +directory name (without '-r') will behave differently, and order of the +arguments does not matter. + +OPTIONS +------- +<tree-ish>:: + Id of a tree-ish. + +-d:: + Show only the named tree entry itself, not its children. + +-r:: + Recurse into sub-trees. + +-t:: + Show tree entries even when going to recurse them. Has no effect + if '-r' was not passed. '-d' implies '-t'. + +-z:: + \0 line termination on output. + +--name-only:: +--name-status:: + List only filenames (instead of the "long" output), one per line. + +--abbrev[=<n>]:: + Instead of showing the full 40-byte hexadecimal object + 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 + implicitly uses the root level of the tree as the sole path argument. + + +Output Format +------------- + <mode> SP <type> SP <object> TAB <file> + +When the `-z` option is not used, TAB, LF, and backslash characters +in pathnames are represented as `\t`, `\n`, and `\\`, respectively. + + +Author +------ +Written by Petr Baudis <pasky@suse.cz> +Completely rewritten from scratch by Junio C Hamano <junkio@cox.net>, +another major rewrite by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list +<git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-mailinfo.txt b/Documentation/git-mailinfo.txt new file mode 100644 index 0000000000..ba18133ead --- /dev/null +++ b/Documentation/git-mailinfo.txt @@ -0,0 +1,70 @@ +git-mailinfo(1) +=============== + +NAME +---- +git-mailinfo - Extracts patch and authorship from a single e-mail message + + +SYNOPSIS +-------- +'git-mailinfo' [-k] [-u | --encoding=<encoding>] <msg> <patch> + + +DESCRIPTION +----------- +Reading a single e-mail message from the standard input, and +writes the commit log message in <msg> file, and the patches in +<patch> file. The author name, e-mail and e-mail subject are +written out to the standard output to be used by git-applypatch +to create a commit. It is usually not necessary to use this +command directly. See gitlink:git-am[1] instead. + + +OPTIONS +------- +-k:: + Usually the program 'cleans up' the Subject: header line + to extract the title line for the commit log message, + among which (1) remove 'Re:' or 're:', (2) leading + whitespaces, (3) '[' up to ']', typically '[PATCH]', and + then prepends "[PATCH] ". This flag forbids this + munging, and is most useful when used to read back 'git + format-patch --mbox' output. + +-u:: + The commit log message, author name and author email are + taken from the e-mail, and after minimally decoding MIME + transfer encoding, re-coded in UTF-8 by transliterating + them. This used to be optional but now it is the default. ++ +Note that the patch is always used as-is without charset +conversion, even with this flag. + +--encoding=<encoding>:: + Similar to -u but if the local convention is different + from what is specified by i18n.commitencoding, this flag + can be used to override it. + +<msg>:: + The commit log message extracted from e-mail, usually + except the title line which comes from e-mail Subject. + +<patch>:: + The patch extracted from e-mail. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and +Junio C Hamano <junkio@cox.net> + + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-mailsplit.txt b/Documentation/git-mailsplit.txt new file mode 100644 index 0000000000..c11d6a530f --- /dev/null +++ b/Documentation/git-mailsplit.txt @@ -0,0 +1,52 @@ +git-mailsplit(1) +================ + +NAME +---- +git-mailsplit - Simple UNIX mbox splitter program + +SYNOPSIS +-------- +'git-mailsplit' [-b] [-f<nn>] [-d<prec>] -o<directory> [--] [<mbox>...] + +DESCRIPTION +----------- +Splits a mbox file into a list of files: "0001" "0002" .. in the specified +directory so you can process them further from there. + +OPTIONS +------- +<mbox>:: + Mbox file to split. If not given, the mbox is read from + the standard input. + +<directory>:: + Directory in which to place the individual messages. + +-b:: + If any file doesn't begin with a From line, assume it is a + single mail message instead of signaling error. + +-d<prec>:: + Instead of the default 4 digits with leading zeros, + different precision can be specified for the generated + filenames. + +-f<nn>:: + Skip the first <nn> numbers, for example if -f3 is specified, + start the numbering with 0004. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> +and Junio C Hamano <junkio@cox.net> + + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-merge-base.txt b/Documentation/git-merge-base.txt new file mode 100644 index 0000000000..3190aed108 --- /dev/null +++ b/Documentation/git-merge-base.txt @@ -0,0 +1,43 @@ +git-merge-base(1) +================= + +NAME +---- +git-merge-base - Find as good common ancestors as possible for a merge + + +SYNOPSIS +-------- +'git-merge-base' [--all] <commit> <commit> + +DESCRIPTION +----------- + +"git-merge-base" finds as good a common ancestor as possible between +the two commits. That is, given two commits A and B 'git-merge-base A +B' will output a commit which is reachable from both A and B through +the parent relationship. + +Given a selection of equally good common ancestors it should not be +relied on to decide in any particular way. + +The "git-merge-base" algorithm is still in flux - use the source... + +OPTIONS +------- +--all:: + Output all common ancestors for the two commits instead of + just one. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-merge-file.txt b/Documentation/git-merge-file.txt new file mode 100644 index 0000000000..31882abb87 --- /dev/null +++ b/Documentation/git-merge-file.txt @@ -0,0 +1,92 @@ +git-merge-file(1) +================= + +NAME +---- +git-merge-file - Run a three-way file merge + + +SYNOPSIS +-------- +[verse] +'git-merge-file' [-L <current-name> [-L <base-name> [-L <other-name>]]] + [-p|--stdout] [-q|--quiet] <current-file> <base-file> <other-file> + + +DESCRIPTION +----------- +git-file-merge incorporates all changes that lead from the `<base-file>` +to `<other-file>` into `<current-file>`. The result ordinarily goes into +`<current-file>`. git-merge-file is useful for combining separate changes +to an original. Suppose `<base-file>` is the original, and both +`<current-file>` and `<other-file>` are modifications of `<base-file>`. +Then git-merge-file combines both changes. + +A conflict occurs if both `<current-file>` and `<other-file>` have changes +in a common segment of lines. If a conflict is found, git-merge-file +normally outputs a warning and brackets the conflict with <<<<<<< and +>>>>>>> lines. A typical conflict will look like this: + + <<<<<<< A + lines in file A + ======= + lines in file B + >>>>>>> B + +If there are conflicts, the user should edit the result and delete one of +the alternatives. + +The exit value of this program is negative on error, and the number of +conflicts otherwise. If the merge was clean, the exit value is 0. + +git-merge-file is designed to be a minimal clone of RCS merge, that is, it +implements all of RCS merge's functionality which is needed by +gitlink:git[1]. + + +OPTIONS +------- + +-L <label>:: + This option may be given up to three times, and + specifies labels to be used in place of the + corresponding file names in conflict reports. That is, + `git-merge-file -L x -L y -L z a b c` generates output that + looks like it came from files x, y and z instead of + from files a, b and c. + +-p:: + Send results to standard output instead of overwriting + `<current-file>`. + +-q:: + Quiet; do not warn about conflicts. + + +EXAMPLES +-------- + +git merge-file README.my README README.upstream:: + + combines the changes of README.my and README.upstream since README, + tries to merge them and writes the result into README.my. + +git merge-file -L a -L b -L c tmp/a123 tmp/b234 tmp/c345:: + + merges tmp/a123 and tmp/c345 with the base tmp/b234, but uses labels + `a` and `c` instead of `tmp/a123` and `tmp/c345`. + + +Author +------ +Written by Johannes Schindelin <johannes.schindelin@gmx.de> + + +Documentation +-------------- +Documentation by Johannes Schindelin and the git-list <git@vger.kernel.org>, +with parts copied from the original documentation of RCS merge. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-merge-index.txt b/Documentation/git-merge-index.txt new file mode 100644 index 0000000000..b8ee1ff2b0 --- /dev/null +++ b/Documentation/git-merge-index.txt @@ -0,0 +1,88 @@ +git-merge-index(1) +================== + +NAME +---- +git-merge-index - Run a merge for files needing merging + + +SYNOPSIS +-------- +'git-merge-index' [-o] [-q] <merge-program> (-a | \-- | <file>\*) + +DESCRIPTION +----------- +This looks up the <file>(s) in the index and, if there are any merge +entries, passes the SHA1 hash for those files as arguments 1, 2, 3 (empty +argument if no file), and <file> as argument 4. File modes for the three +files are passed as arguments 5, 6 and 7. + +OPTIONS +------- +\--:: + Do not interpret any more arguments as options. + +-a:: + Run merge against all files in the index that need merging. + +-o:: + Instead of stopping at the first failed merge, do all of them + in one shot - continue with merging even when previous merges + returned errors, and only return the error code after all the + merges are over. + +-q:: + Do not complain about failed merge program (the merge program + failure usually indicates conflicts during merge). This is for + porcelains which might want to emit custom messages. + +If "git-merge-index" is called with multiple <file>s (or -a) then it +processes them in turn only stopping if merge returns a non-zero exit +code. + +Typically this is run with the a script calling git's imitation of +the merge command from the RCS package. + +A sample script called "git-merge-one-file" is included in the +distribution. + +ALERT ALERT ALERT! The git "merge object order" is different from the +RCS "merge" program merge object order. In the above ordering, the +original is first. But the argument order to the 3-way merge program +"merge" is to have the original in the middle. Don't ask me why. + +Examples: + + torvalds@ppc970:~/merge-test> git-merge-index cat MM + This is MM from the original tree. # original + This is modified MM in the branch A. # merge1 + This is modified MM in the branch B. # merge2 + This is modified MM in the branch B. # current contents + +or + + torvalds@ppc970:~/merge-test> git-merge-index cat AA MM + cat: : No such file or directory + This is added AA in the branch A. + This is added AA in the branch B. + This is added AA in the branch B. + fatal: merge program failed + +where the latter example shows how "git-merge-index" will stop trying to +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). + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> +One-shot merge by Petr Baudis <pasky@ucw.cz> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-merge-one-file.txt b/Documentation/git-merge-one-file.txt new file mode 100644 index 0000000000..f80ab3b8c4 --- /dev/null +++ b/Documentation/git-merge-one-file.txt @@ -0,0 +1,30 @@ +git-merge-one-file(1) +===================== + +NAME +---- +git-merge-one-file - The standard helper program to use with git-merge-index + + +SYNOPSIS +-------- +'git-merge-one-file' + +DESCRIPTION +----------- +This is the standard helper program to use with "git-merge-index" +to resolve a merge after the trivial merge done with "git-read-tree -m". + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org>, +Junio C Hamano <junkio@cox.net> and Petr Baudis <pasky@suse.cz>. + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-merge-tree.txt b/Documentation/git-merge-tree.txt new file mode 100644 index 0000000000..35fb4fb713 --- /dev/null +++ b/Documentation/git-merge-tree.txt @@ -0,0 +1,37 @@ +git-merge-tree(1) +================= + +NAME +---- +git-merge-tree - Show three-way merge without touching index + + +SYNOPSIS +-------- +'git-merge-tree' <base-tree> <branch1> <branch2> + +DESCRIPTION +----------- +Reads three treeish, and output trivial merge results and +conflicting stages to the standard output. This is similar to +what three-way read-tree -m does, but instead of storing the +results in the index, the command outputs the entries to the +standard output. + +This is meant to be used by higher level scripts to compute +merge results outside index, and stuff the results back into the +index. For this reason, the output from the command omits +entries that match <branch1> tree. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt new file mode 100644 index 0000000000..3c08a6f7db --- /dev/null +++ b/Documentation/git-merge.txt @@ -0,0 +1,160 @@ +git-merge(1) +============ + +NAME +---- +git-merge - Join two or more development histories together + + +SYNOPSIS +-------- +[verse] +'git-merge' [-n] [--no-commit] [--squash] [-s <strategy>]... + -m=<msg> <remote> <remote>... + +DESCRIPTION +----------- +This is the top-level interface to the merge machinery +which drives multiple merge strategy scripts. + + +OPTIONS +------- +include::merge-options.txt[] + +<msg>:: + The commit message to be used for the merge commit (in case + it is created). The `git-fmt-merge-msg` script can be used + to give a good default for automated `git-merge` invocations. + +<head>:: + Our branch head commit. This has to be `HEAD`, so new + syntax does not require it + +<remote>:: + Other branch head merged into our branch. You need at + least one <remote>. Specifying more than one <remote> + obviously means you are trying an Octopus. + +include::merge-strategies.txt[] + + +If you tried a merge which resulted in a complex conflicts and +would want to start over, you can recover with +gitlink:git-reset[1]. + + +HOW MERGE WORKS +--------------- + +A merge is always between the current `HEAD` and one or more +remote branch heads, and the index file must exactly match the +tree of `HEAD` commit (i.e. the contents of the last commit) when +it happens. In other words, `git-diff --cached HEAD` must +report no changes. + +[NOTE] +This is a bit of lie. In certain special cases, your index are +allowed to be different from the tree of `HEAD` commit. The most +notable case is when your `HEAD` commit is already ahead of what +is being merged, in which case your index can have arbitrary +difference from your `HEAD` commit. Otherwise, your index entries +are allowed have differences from your `HEAD` commit that match +the result of trivial merge (e.g. you received the same patch +from external source to produce the same result as what you are +merging). For example, if a path did not exist in the common +ancestor and your head commit but exists in the tree you are +merging into your repository, and if you already happen to have +that path exactly in your index, the merge does not have to +fail. + +Otherwise, merge will refuse to do any harm to your repository +(that is, it may fetch the objects from remote, and it may even +update the local branch used to keep track of the remote branch +with `git pull remote rbranch:lbranch`, but your working tree, +`.git/HEAD` pointer and index file are left intact). + +You may have local modifications in the working tree files. In +other words, `git-diff` is allowed to report changes. +However, the merge uses your working tree as the working area, +and in order to prevent the merge operation from losing such +changes, it makes sure that they do not interfere with the +merge. Those complex tables in read-tree documentation define +what it means for a path to "interfere with the merge". And if +your local modifications interfere with the merge, again, it +stops before touching anything. + +So in the above two "failed merge" case, you do not have to +worry about loss of data --- you simply were not ready to do +a merge, so no merge happened at all. You may want to finish +whatever you were in the middle of doing, and retry the same +pull after you are done and ready. + +When things cleanly merge, these things happen: + +1. the results are updated both in the index file and in your + working tree, +2. index file is written out as a tree, +3. the tree gets committed, and +4. the `HEAD` pointer gets advanced. + +Because of 2., we require that the original state of the index +file to match exactly the current `HEAD` commit; otherwise we +will write out your local changes already registered in your +index file along with the merge result, which is not good. +Because 1. involves only the paths different between your +branch and the remote branch you are pulling from during the +merge (which is typically a fraction of the whole tree), you can +have local modifications in your working tree as long as they do +not overlap with what the merge updates. + +When there are conflicts, these things happen: + +1. `HEAD` stays the same. + +2. Cleanly merged paths are updated both in the index file and + in your working tree. + +3. For conflicting paths, the index file records up to three + versions; stage1 stores the version from the common ancestor, + stage2 from `HEAD`, and stage3 from the remote branch (you + can inspect the stages with `git-ls-files -u`). The working + tree files have the result of "merge" program; i.e. 3-way + merge result with familiar conflict markers `<<< === >>>`. + +4. No other changes are done. In particular, the local + modifications you had before you started merge will stay the + same and the index entries for them stay as they were, + i.e. matching `HEAD`. + +After seeing a conflict, you can do two things: + + * Decide not to merge. The only clean-up you need are to reset + the index file to the `HEAD` commit to reverse 2. and to clean + up working tree changes made by 2. and 3.; `git-reset` can + be used for this. + + * Resolve the conflicts. `git-diff` would report only the + conflicting paths because of the above 2. and 3.. Edit the + working tree files into a desirable shape, `git-update-index` + them, to make the index file contain what the merge result + should be, and run `git-commit` to commit the result. + + +SEE ALSO +-------- +gitlink:git-fmt-merge-msg[1], gitlink:git-pull[1] + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-mktag.txt b/Documentation/git-mktag.txt new file mode 100644 index 0000000000..2860a3d1ba --- /dev/null +++ b/Documentation/git-mktag.txt @@ -0,0 +1,47 @@ +git-mktag(1) +============ + +NAME +---- +git-mktag - Creates a tag object + + +SYNOPSIS +-------- +'git-mktag' < signature_file + +DESCRIPTION +----------- +Reads a tag contents on standard input and creates a tag object +that can also be used to sign other objects. + +The output is the new tag's <object> identifier. + +Tag Format +---------- +A tag signature file has a very simple fixed format: three lines of + + object <sha1> + type <typename> + tag <tagname> + +followed by some 'optional' free-form signature that git itself +doesn't care about, but that can be verified with gpg or similar. + +The size of the full object is artificially limited to 8kB. (Just +because I'm a lazy bastard, and if you can't fit a signature in that +size, you're doing something wrong) + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-mktree.txt b/Documentation/git-mktree.txt new file mode 100644 index 0000000000..5f9ee603b7 --- /dev/null +++ b/Documentation/git-mktree.txt @@ -0,0 +1,35 @@ +git-mktree(1) +============= + +NAME +---- +git-mktree - Build a tree-object from ls-tree formatted text + + +SYNOPSIS +-------- +'git-mktree' [-z] + +DESCRIPTION +----------- +Reads standard input in non-recursive `ls-tree` output format, +and creates a tree object. The object name of the tree object +built is written to the standard output. + +OPTIONS +------- +-z:: + Read the NUL-terminated `ls-tree -z` output instead. + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-mv.txt b/Documentation/git-mv.txt new file mode 100644 index 0000000000..6756b76bb1 --- /dev/null +++ b/Documentation/git-mv.txt @@ -0,0 +1,54 @@ +git-mv(1) +========= + +NAME +---- +git-mv - Move or rename a file, a directory, or a symlink + + +SYNOPSIS +-------- +'git-mv' <options>... <args>... + +DESCRIPTION +----------- +This script is used to move or rename a file, directory or symlink. + + git-mv [-f] [-n] <source> <destination> + git-mv [-f] [-n] [-k] <source> ... <destination directory> + +In the first form, it renames <source>, which must exist and be either +a file, symlink or directory, to <destination>. +In the second form, the last argument has to be an existing +directory; the given sources will be moved into this directory. + +The index is updated after successful completion, but the change must still be +committed. + +OPTIONS +------- +-f:: + Force renaming or moving of a file even if the target exists +-k:: + Skip move or rename actions which would lead to an error + condition. An error happens when a source is neither existing nor + controlled by GIT, or when it would overwrite an existing + file unless '-f' is given. +-n:: + Do nothing; only show what would happen + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> +Rewritten by Ryan Anderson <ryan@michonline.com> +Move functionality added by Josef Weidendorfer <Josef.Weidendorfer@gmx.de> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-name-rev.txt b/Documentation/git-name-rev.txt new file mode 100644 index 0000000000..37fbf66efb --- /dev/null +++ b/Documentation/git-name-rev.txt @@ -0,0 +1,67 @@ +git-name-rev(1) +=============== + +NAME +---- +git-name-rev - Find symbolic names for given revs + + +SYNOPSIS +-------- +'git-name-rev' [--tags] ( --all | --stdin | <committish>... ) + +DESCRIPTION +----------- +Finds symbolic names suitable for human digestion for revisions given in any +format parsable by git-rev-parse. + + +OPTIONS +------- + +--tags:: + Do not use branch names, but only tags to name the commits + +--all:: + List all commits reachable from all refs + +--stdin:: + Read from stdin, append "(<rev_name>)" to all sha1's of nameable + commits, and pass to stdout + +EXAMPLE +------- + +Given a commit, find out where it is relative to the local refs. Say somebody +wrote you about that fantastic commit 33db5f4d9027a10e477ccf054b2c1ab94f74c85a. +Of course, you look into the commit, but that only tells you what happened, but +not the context. + +Enter git-name-rev: + +------------ +% git name-rev 33db5f4d9027a10e477ccf054b2c1ab94f74c85a +33db5f4d9027a10e477ccf054b2c1ab94f74c85a tags/v0.99^0~940 +------------ + +Now you are wiser, because you know that it happened 940 revisions before v0.99. + +Another nice thing you can do is: + +------------ +% git log | git name-rev --stdin +------------ + + +Author +------ +Written by Johannes Schindelin <Johannes.Schindelin@gmx.de> + +Documentation +-------------- +Documentation by Johannes Schindelin. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-p4import.txt b/Documentation/git-p4import.txt new file mode 100644 index 0000000000..6edb9f12b8 --- /dev/null +++ b/Documentation/git-p4import.txt @@ -0,0 +1,168 @@ +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 +$ 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, e.g. 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 (e.g. 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 + diff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt new file mode 100644 index 0000000000..fdc6f97289 --- /dev/null +++ b/Documentation/git-pack-objects.txt @@ -0,0 +1,159 @@ +git-pack-objects(1) +=================== + +NAME +---- +git-pack-objects - Create a packed archive of objects + + +SYNOPSIS +-------- +[verse] +'git-pack-objects' [-q] [--no-reuse-delta] [--delta-base-offset] [--non-empty] + [--local] [--incremental] [--window=N] [--depth=N] [--all-progress] + [--revs [--unpacked | --all]*] [--stdout | base-name] < object-list + + +DESCRIPTION +----------- +Reads list of objects from the standard input, and writes a packed +archive with specified base-name, or to the standard output. + +A packed archive is an efficient way to transfer set of objects +between two repositories, and also is an archival format which +is efficient to access. The packed archive format (.pack) is +designed to be unpackable without having anything else, but for +random access, accompanied with the pack index file (.idx). + +'git-unpack-objects' command can read the packed archive and +expand the objects contained in the pack into "one-file +one-object" format; this is typically done by the smart-pull +commands when a pack is created on-the-fly for efficient network +transport by their peers. + +Placing both in the pack/ subdirectory of $GIT_OBJECT_DIRECTORY (or +any of the directories on $GIT_ALTERNATE_OBJECT_DIRECTORIES) +enables git to read from such an archive. + +In a packed archive, an object is either stored as a compressed +whole, or as a difference from some other object. The latter is +often called a delta. + + +OPTIONS +------- +base-name:: + Write into a pair of files (.pack and .idx), using + <base-name> to determine the name of the created file. + When this option is used, the two files are written in + <base-name>-<SHA1>.{pack,idx} files. <SHA1> is a hash + of the sorted object names to make the resulting filename + based on the pack content, and written to the standard + output of the command. + +--stdout:: + Write the pack contents (what would have been written to + .pack file) out to the standard output. + +--revs:: + Read the revision arguments from the standard input, instead of + individual object names. The revision arguments are processed + the same way as gitlink:git-rev-list[1] with `--objects` flag + uses its `commit` arguments to build the list of objects it + outputs. The objects on the resulting list are packed. + +--unpacked:: + This implies `--revs`. When processing the list of + revision arguments read from the standard input, limit + the objects packed to those that are not already packed. + +--all:: + This implies `--revs`. In addition to the list of + revision arguments read from the standard input, pretend + as if all refs under `$GIT_DIR/refs` are specified to be + included. + +--window=[N], --depth=[N]:: + These two options affect how the objects contained in + the pack are stored using delta compression. The + objects are first internally sorted by type, size and + optionally names and compared against the other objects + within --window to see if using delta compression saves + space. --depth limits the maximum delta depth; making + it too deep affects the performance on the unpacker + side, because delta data needs to be applied that many + times to get to the necessary object. + The default value for both --window and --depth is 10. + +--incremental:: + This flag causes an object already in a pack ignored + even if it appears in the standard input. + +--local:: + This flag is similar to `--incremental`; instead of + ignoring all packed objects, it only ignores objects + that are packed and not in the local object store + (i.e. borrowed from an alternate). + +--non-empty:: + Only create a packed archive if it would contain at + least one object. + +--progress:: + Progress status is reported on the standard error stream + by default when it is attached to a terminal, unless -q + is specified. This flag forces progress status even if + the standard error stream is not directed to a terminal. + +--all-progress:: + When --stdout is specified then progress report is + displayed during the object count and deltification phases + but inhibited during the write-out phase. The reason is + that in some cases the output stream is directly linked + to another command which may wish to display progress + status of its own as it processes incoming pack data. + This flag is like --progress except that it forces progress + report for the write-out phase as well even if --stdout is + used. + +-q:: + This flag makes the command not to report its progress + on the standard error stream. + +--no-reuse-delta:: + When creating a packed archive in a repository that + has existing packs, the command reuses existing deltas. + This sometimes results in a slightly suboptimal pack. + This flag tells the command not to reuse existing deltas + but compute them from scratch. + +--delta-base-offset:: + A packed archive can express base object of a delta as + either 20-byte object name or as an offset in the + stream, but older version of git does not understand the + latter. By default, git-pack-objects only uses the + former format for better compatibility. This option + allows the command to use the latter format for + compactness. Depending on the average delta chain + length, this option typically shrinks the resulting + packfile by 3-5 per-cent. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +------------- +Documentation by Junio C Hamano + +See Also +-------- +gitlink:git-rev-list[1] +gitlink:git-repack[1] +gitlink:git-prune-packed[1] + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-pack-redundant.txt b/Documentation/git-pack-redundant.txt new file mode 100644 index 0000000000..94bbea0db2 --- /dev/null +++ b/Documentation/git-pack-redundant.txt @@ -0,0 +1,58 @@ +git-pack-redundant(1) +===================== + +NAME +---- +git-pack-redundant - Find redundant pack files + + +SYNOPSIS +-------- +'git-pack-redundant' [ --verbose ] [ --alt-odb ] < --all | .pack filename ... > + +DESCRIPTION +----------- +This program computes which packs in your repository +are redundant. The output is suitable for piping to +'xargs rm' if you are in the root of the repository. + +git-pack-redundant accepts a list of objects on standard input. Any objects +given will be ignored when checking which packs are required. This makes the +following command useful when wanting to remove packs which contain unreachable +objects. + +git-fsck --full --unreachable | cut -d ' ' -f3 | \ +git-pack-redundant --all | xargs rm + +OPTIONS +------- + + +--all:: + Processes all packs. Any filenames on the command line are ignored. + +--alt-odb:: + Don't require objects present in packs from alternate object + directories to be present in local packs. + +--verbose:: + Outputs some statistics to stderr. Has a small performance penalty. + +Author +------ +Written by Lukas Sandström <lukass@etek.chalmers.se> + +Documentation +-------------- +Documentation by Lukas Sandström <lukass@etek.chalmers.se> + +See Also +-------- +gitlink:git-pack-objects[1] +gitlink:git-repack[1] +gitlink:git-prune-packed[1] + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-pack-refs.txt b/Documentation/git-pack-refs.txt new file mode 100644 index 0000000000..a20fc7de40 --- /dev/null +++ b/Documentation/git-pack-refs.txt @@ -0,0 +1,66 @@ +git-pack-refs(1) +================ + +NAME +---- +git-pack-refs - Pack heads and tags for efficient repository access + +SYNOPSIS +-------- +'git-pack-refs' [--all] [--no-prune] + +DESCRIPTION +----------- + +Traditionally, tips of branches and tags (collectively known as +'refs') were stored one file per ref under `$GIT_DIR/refs` +directory. While many branch tips tend to be updated often, +most tags and some branch tips are never updated. When a +repository has hundreds or thousands of tags, this +one-file-per-ref format both wastes storage and hurts +performance. + +This command is used to solve the storage and performance +problem by stashing the refs in a single file, +`$GIT_DIR/packed-refs`. When a ref is missing from the +traditional `$GIT_DIR/refs` hierarchy, it is looked up in this +file and used if found. + +Subsequent updates to branches always creates new file under +`$GIT_DIR/refs` hierarchy. + +A recommended practice to deal with a repository with too many +refs is to pack its refs with `--all --prune` once, and +occasionally run `git-pack-refs \--prune`. Tags are by +definition stationary and are not expected to change. Branch +heads will be packed with the initial `pack-refs --all`, but +only the currently active branch heads will become unpacked, +and next `pack-refs` (without `--all`) will leave them +unpacked. + + +OPTIONS +------- + +\--all:: + +The command by default packs all tags and refs that are already +packed, and leaves other refs +alone. This is because branches are expected to be actively +developed and packing their tips does not help performance. +This option causes branch tips to be packed as well. Useful for +a repository with many branches of historical interests. + +\--no-prune:: + +The command usually removes loose refs under `$GIT_DIR/refs` +hierarchy after packing them. This option tells it not to. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-parse-remote.txt b/Documentation/git-parse-remote.txt new file mode 100644 index 0000000000..11b1f4d2e2 --- /dev/null +++ b/Documentation/git-parse-remote.txt @@ -0,0 +1,50 @@ +git-parse-remote(1) +=================== + +NAME +---- +git-parse-remote - Routines to help parsing remote repository access parameters + + +SYNOPSIS +-------- +'. git-parse-remote' + +DESCRIPTION +----------- +This script is included in various scripts to supply +routines to parse files under $GIT_DIR/remotes/ and +$GIT_DIR/branches/ and configuration variables that are related +to fetching, pulling and pushing. + +The primary entry points are: + +get_remote_refs_for_fetch:: + Given the list of user-supplied `<repo> <refspec>...`, + return the list of refs to fetch after canonicalizing + them into `$GIT_DIR` relative paths + (e.g. `refs/heads/foo`). When `<refspec>...` is empty + the returned list of refs consists of the defaults + for the given `<repo>`, if specified in + `$GIT_DIR/remotes/`, `$GIT_DIR/branches/`, or `remote.*.fetch` + configuration. + +get_remote_refs_for_push:: + Given the list of user-supplied `<repo> <refspec>...`, + return the list of refs to push in a form suitable to be + fed to the `git-send-pack` command. When `<refspec>...` + is empty the returned list of refs consists of the + defaults for the given `<repo>`, if specified in + `$GIT_DIR/remotes/`. + +Author +------ +Written by Junio C Hamano. + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-patch-id.txt b/Documentation/git-patch-id.txt new file mode 100644 index 0000000000..a7e9fd021a --- /dev/null +++ b/Documentation/git-patch-id.txt @@ -0,0 +1,43 @@ +git-patch-id(1) +=============== + +NAME +---- +git-patch-id - Compute unique ID for a patch + +SYNOPSIS +-------- +'git-patch-id' < <patch> + +DESCRIPTION +----------- +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, 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. + +When dealing with git-diff-tree output, it takes advantage of +the fact that the patch is prefixed with the object name of the +commit, and outputs two 40-byte hexadecimal string. The first +string is the patch ID, and the second string is the commit ID. +This can be used to make a mapping from patch ID to commit ID. + +OPTIONS +------- +<patch>:: + The diff to create the ID of. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-peek-remote.txt b/Documentation/git-peek-remote.txt new file mode 100644 index 0000000000..74f37bd904 --- /dev/null +++ b/Documentation/git-peek-remote.txt @@ -0,0 +1,55 @@ +git-peek-remote(1) +================== + +NAME +---- +git-peek-remote - List the references in a remote repository + + +SYNOPSIS +-------- +'git-peek-remote' [--upload-pack=<git-upload-pack>] [<host>:]<directory> + +DESCRIPTION +----------- +Lists the references the remote repository has, and optionally +stores them in the local repository under the same name. + +OPTIONS +------- +\--upload-pack=<git-upload-pack>:: + Use this to specify the path to 'git-upload-pack' on the + remote side, if it is not found on your $PATH. Some + installations of sshd ignores the user's environment + setup scripts for login shells (e.g. .bash_profile) and + your privately installed git may not be found on the system + default $PATH. Another workaround suggested is to set + up your $PATH in ".bashrc", but this flag is for people + who do not want to pay the overhead for non-interactive + shells, but prefer having a lean .bashrc file (they set most of + the things up in .bash_profile). + +\--exec=<git-upload-pack>:: + Same \--upload-pack=<git-upload-pack>. + +<host>:: + A remote host that houses the repository. When this + part is specified, 'git-upload-pack' is invoked via + ssh. + +<directory>:: + The repository to sync from. + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-prune-packed.txt b/Documentation/git-prune-packed.txt new file mode 100644 index 0000000000..310033e460 --- /dev/null +++ b/Documentation/git-prune-packed.txt @@ -0,0 +1,53 @@ +git-prune-packed(1) +===================== + +NAME +---- +git-prune-packed - Remove extra objects that are already in pack files + + +SYNOPSIS +-------- +'git-prune-packed' [-n] [-q] + + +DESCRIPTION +----------- +This program search the `$GIT_OBJECT_DIR` for all objects that currently +exist in a pack file as well as the independent object directories. + +All such extra objects are removed. + +A pack is a collection of objects, individually compressed, with delta +compression applied, stored in a single file, with an associated index file. + +Packs are used to reduce the load on mirror systems, backup engines, +disk storage, etc. + + +OPTIONS +------- +-n:: + Don't actually remove any objects, only show those that would have been + removed. + +-q:: + Squelch the progress indicator. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Ryan Anderson <ryan@michonline.com> + +See Also +-------- +gitlink:git-pack-objects[1] +gitlink:git-repack[1] + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt new file mode 100644 index 0000000000..0b44f3015d --- /dev/null +++ b/Documentation/git-prune.txt @@ -0,0 +1,61 @@ +git-prune(1) +============ + +NAME +---- +git-prune - Prunes all unreachable objects from the object database + + +SYNOPSIS +-------- +'git-prune' [-n] [--] [<head>...] + +DESCRIPTION +----------- + +This runs `git-fsck --unreachable` using all the refs +available in `$GIT_DIR/refs`, optionally with additional set of +objects specified on the command line, and prunes all +objects unreachable from any of these head objects from the object database. +In addition, it +prunes the unpacked objects that are also found in packs by +running `git prune-packed`. + +OPTIONS +------- + +-n:: + Do not remove anything; just report what it would + remove. + +\--:: + Do not interpret any more arguments as options. + +<head>...:: + In addition to objects + reachable from any of our references, keep objects + reachable from listed <head>s. + +EXAMPLE +------- + +To prune objects not used by your repository nor another that +borrows from your repository via its +`.git/objects/info/alternates`: + +------------ +$ git prune $(cd ../another && $(git-rev-parse --all)) +------------ + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt new file mode 100644 index 0000000000..94478ed94d --- /dev/null +++ b/Documentation/git-pull.txt @@ -0,0 +1,168 @@ +git-pull(1) +=========== + +NAME +---- +git-pull - Fetch from and merge with another repository or a local branch + + +SYNOPSIS +-------- +'git-pull' <options> <repository> <refspec>... + + +DESCRIPTION +----------- +Runs `git-fetch` with the given parameters, and calls `git-merge` +to merge the retrieved head(s) into the current branch. + +Note that you can use `.` (current directory) as the +<repository> to pull from the local repository -- this is useful +when merging local branches into the current branch. + + +OPTIONS +------- +include::merge-options.txt[] + +include::fetch-options.txt[] + +include::pull-fetch-param.txt[] + +include::urls.txt[] + +include::merge-strategies.txt[] + +DEFAULT BEHAVIOUR +----------------- + +Often people use `git pull` without giving any parameter. +Traditionally, this has been equivalent to saying `git pull +origin`. However, when configuration `branch.<name>.remote` is +present while on branch `<name>`, that value is used instead of +`origin`. + +In order to determine what URL to use to fetch from, the value +of the configuration `remote.<origin>.url` is consulted +and if there is not any such variable, the value on `URL: ` line +in `$GIT_DIR/remotes/<origin>` file is used. + +In order to determine what remote branches to fetch (and +optionally store in the tracking branches) when the command is +run without any refspec parameters on the command line, values +of the configuration variable `remote.<origin>.fetch` are +consulted, and if there aren't any, `$GIT_DIR/remotes/<origin>` +file is consulted and its `Pull: ` lines are used. +In addition to the refspec formats described in the OPTIONS +section, you can have a globbing refspec that looks like this: + +------------ +refs/heads/*:refs/remotes/origin/* +------------ + +A globbing refspec must have a non-empty RHS (i.e. must store +what were fetched in tracking branches), and its LHS and RHS +must end with `/*`. The above specifies that all remote +branches are tracked using tracking branches in +`refs/remotes/origin/` hierarchy under the same name. + +The rule to determine which remote branch to merge after +fetching is a bit involved, in order not to break backward +compatibility. + +If explicit refspecs were given on the command +line of `git pull`, they are all merged. + +When no refspec was given on the command line, then `git pull` +uses the refspec from the configuration or +`$GIT_DIR/remotes/<origin>`. In such cases, the following +rules apply: + +. If `branch.<name>.merge` configuration for the current + branch `<name>` exists, that is the name of the branch at the + remote site that is merged. + +. If the refspec is a globbing one, nothing is merged. + +. Otherwise the remote branch of the first refspec is merged. + + +EXAMPLES +-------- + +git pull, git pull origin:: + Update the remote-tracking branches for the repository + you cloned from, then merge one of them into your + current branch. Normally the branch merged in is + the HEAD of the remote repository, but the choice is + determined by the branch.<name>.remote and + branch.<name>.merge options; see gitlink:git-config[1] + for details. + +git pull origin next:: + Merge into the current branch the remote branch `next`; + leaves a copy of `next` temporarily in FETCH_HEAD, but + does not update any remote-tracking branches. + +git pull . fixes enhancements:: + Bundle local branch `fixes` and `enhancements` on top of + the current branch, making an Octopus merge. This `git pull .` + syntax is equivalent to `git merge`. + +git pull -s ours . obsolete:: + Merge local branch `obsolete` into the current branch, + using `ours` merge strategy. + +git pull --no-commit . maint:: + Merge local branch `maint` into the current branch, but + do not make a commit automatically. This can be used + when you want to include further changes to the merge, + or want to write your own merge commit message. ++ +You should refrain from abusing this option to sneak substantial +changes into a merge commit. Small fixups like bumping +release/version name would be acceptable. + +Command line pull of multiple branches from one repository:: ++ +------------------------------------------------ +$ git checkout master +$ git fetch origin +pu:pu maint:tmp +$ git pull . tmp +------------------------------------------------ ++ +This updates (or creates, as necessary) branches `pu` and `tmp` +in the local repository by fetching from the branches +(respectively) `pu` and `maint` from the remote repository. ++ +The `pu` branch will be updated even if it is does not +fast-forward; the others will not be. ++ +The final command then merges the newly fetched `tmp` into master. + + +If you tried a pull which resulted in a complex conflicts and +would want to start over, you can recover with +gitlink:git-reset[1]. + + +SEE ALSO +-------- +gitlink:git-fetch[1], gitlink:git-merge[1], gitlink:git-config[1] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> +and Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Jon Loeliger, +David Greaves, +Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-push.txt b/Documentation/git-push.txt new file mode 100644 index 0000000000..f8cc2b5432 --- /dev/null +++ b/Documentation/git-push.txt @@ -0,0 +1,111 @@ +git-push(1) +=========== + +NAME +---- +git-push - Update remote refs along with associated objects + + +SYNOPSIS +-------- +'git-push' [--all] [--tags] [--receive-pack=<git-receive-pack>] [--repo=all] [-f | --force] [-v] [<repository> <refspec>...] + +DESCRIPTION +----------- + +Updates remote refs using local refs, while sending objects +necessary to complete the given refs. + +You can make interesting things happen to a repository +every time you push into it, by setting up 'hooks' there. See +documentation for gitlink:git-receive-pack[1]. + + +OPTIONS +------- +<repository>:: + The "remote" repository that is destination of a push + operation. See the section <<URLS,GIT URLS>> below. + +<refspec>:: + The canonical format of a <refspec> parameter is + `+?<src>:<dst>`; that is, an optional plus `+`, followed + by the source ref, followed by a colon `:`, followed by + the destination ref. ++ +The <src> side can be an +arbitrary "SHA1 expression" that can be used as an +argument to `git-cat-file -t`. E.g. `master~4` (push +four parents before the current master head). ++ +The local ref that matches <src> is used +to fast forward the remote ref that matches <dst>. If +the optional plus `+` is used, the remote ref is updated +even if it does not result in a fast forward update. ++ +Note: If no explicit refspec is found, (that is neither +on the command line nor in any Push line of the +corresponding remotes file---see below), then all the +refs that exist both on the local side and on the remote +side are updated. ++ +`tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`. ++ +A parameter <ref> without a colon is equivalent to +<ref>`:`<ref>, hence updates <ref> in the destination from <ref> +in the source. ++ +Pushing an empty <src> allows you to delete the <dst> ref from +the remote repository. + +\--all:: + Instead of naming each ref to push, specifies that all + refs be pushed. + +\--tags:: + All refs under `$GIT_DIR/refs/tags` are pushed, in + addition to refspecs explicitly listed on the command + line. + +\--receive-pack=<git-receive-pack>:: + Path to the 'git-receive-pack' program on the remote + end. Sometimes useful when pushing to a remote + repository over ssh, and you do not have the program in + a directory on the default $PATH. + +\--exec=<git-receive-pack>:: + Same as \--receive-pack=<git-receive-pack>. + +-f, \--force:: + Usually, the command refuses to update a remote ref that is + not a descendant of the local ref used to overwrite it. + This flag disables the check. This can cause the + remote repository to lose commits; use it with care. + +\--repo=<repo>:: + When no repository is specified the command defaults to + "origin"; this overrides it. + +\--thin, \--no-thin:: + These options are passed to `git-send-pack`. Thin + transfer spends extra cycles to minimize the number of + objects to be sent and meant to be used on slower connection. + +-v:: + Run verbosely. + +include::urls.txt[] + +Author +------ +Written by Junio C Hamano <junkio@cox.net>, later rewritten in C +by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-quiltimport.txt b/Documentation/git-quiltimport.txt new file mode 100644 index 0000000000..6e9a8c369a --- /dev/null +++ b/Documentation/git-quiltimport.txt @@ -0,0 +1,61 @@ +git-quiltimport(1) +================ + +NAME +---- +git-quiltimport - Applies a quilt patchset onto the current branch + + +SYNOPSIS +-------- +[verse] +'git-quiltimport' [--dry-run] [--author <author>] [--patches <dir>] + + +DESCRIPTION +----------- +Applies a quilt patchset onto the current git branch, preserving +the patch boundaries, patch order, and patch descriptions present +in the quilt patchset. + +For each patch the code attempts to extract the author from the +patch description. If that fails it falls back to the author +specified with --author. If the --author flag was not given +the patch description is displayed and the user is asked to +interactively enter the author of the patch. + +If a subject is not found in the patch description the patch name is +preserved as the 1 line subject in the git description. + +OPTIONS +------- +--dry-run:: + Walk through the patches in the series and warn + if we cannot find all of the necessary information to commit + a patch. At the time of this writing only missing author + information is warned about. + +--author Author Name <Author Email>:: + The author name and email address to use when no author + information can be found in the patch description. + +--patches <dir>:: + The directory to find the quilt patches and the + quilt series file. + + The default for the patch directory is patches + or the value of the $QUILT_PATCHES environment + variable. + +Author +------ +Written by Eric Biederman <ebiederm@lnxi.com> + +Documentation +-------------- +Documentation by Eric Biederman <ebiederm@lnxi.com> + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-read-tree.txt b/Documentation/git-read-tree.txt new file mode 100644 index 0000000000..0ff2890c7f --- /dev/null +++ b/Documentation/git-read-tree.txt @@ -0,0 +1,346 @@ +git-read-tree(1) +================ + +NAME +---- +git-read-tree - Reads tree information into the index + + +SYNOPSIS +-------- +'git-read-tree' (<tree-ish> | [[-m [--aggressive] | --reset | --prefix=<prefix>] [-u | -i]] [--exclude-per-directory=<gitignore>] <tree-ish1> [<tree-ish2> [<tree-ish3>]]) + + +DESCRIPTION +----------- +Reads the tree information given by <tree-ish> into the index, +but does not actually *update* any of the files it "caches". (see: +gitlink:git-checkout-index[1]) + +Optionally, it can merge a tree into the index, perform a +fast-forward (i.e. 2-way) merge, or a 3-way merge, with the `-m` +flag. When used with `-m`, the `-u` flag causes it to also update +the files in the work tree with the result of the merge. + +Trivial merges are done by `git-read-tree` itself. Only conflicting paths +will be in unmerged state when `git-read-tree` returns. + +OPTIONS +------- +-m:: + Perform a merge, not just a read. The command will + refuse to run if your index file has unmerged entries, + indicating that you have not finished previous merge you + started. + +--reset:: + Same as -m, except that unmerged entries are discarded + instead of failing. + +-u:: + After a successful merge, update the files in the work + tree with the result of the merge. + +-i:: + Usually a merge requires the index file as well as the + files in the working tree are up to date with the + current head commit, in order not to lose local + changes. This flag disables the check with the working + tree and is meant to be used when creating a merge of + trees that are not directly related to the current + working tree status into a temporary index file. + +--aggressive:: + Usually a three-way merge by `git-read-tree` resolves + the merge for really trivial cases and leaves other + cases unresolved in the index, so that Porcelains can + implement different merge policies. This flag makes the + command to resolve a few more cases internally: ++ +* when one side removes a path and the other side leaves the path + unmodified. The resolution is to remove that path. +* when both sides remove a path. The resolution is to remove that path. +* 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. + +--exclude-per-directory=<gitignore>:: + When running the command with `-u` and `-m` options, the + merge result may need to overwrite paths that are not + tracked in the current branch. The command usually + refuses to proceed with the merge to avoid losing such a + path. However this safety valve sometimes gets in the + way. For example, it often happens that the other + branch added a file that used to be a generated file in + your branch, and the safety valve triggers when you try + to switch to that branch after you ran `make` but before + running `make clean` to remove the generated file. This + option tells the command to read per-directory exclude + file (usually '.gitignore') and allows such an untracked + but explicitly ignored file to be overwritten. + +<tree-ish#>:: + The id of the tree object(s) to be read/merged. + + +Merging +------- +If `-m` is specified, `git-read-tree` can perform 3 kinds of +merge, a single tree merge if only 1 tree is given, a +fast-forward merge with 2 trees, or a 3-way merge if 3 trees are +provided. + + +Single Tree Merge +~~~~~~~~~~~~~~~~~ +If only 1 tree is specified, git-read-tree operates as if the user did not +specify `-m`, except that if the original index has an entry for a +given pathname, and the contents of the path matches with the tree +being read, the stat info from the index is used. (In other words, the +index's stat()s take precedence over the merged tree's). + +That means that if you do a `git-read-tree -m <newtree>` followed by a +`git-checkout-index -f -u -a`, the `git-checkout-index` only checks out +the stuff that really changed. + +This is used to avoid unnecessary false hits when `git-diff-files` is +run after `git-read-tree`. + + +Two Tree Merge +~~~~~~~~~~~~~~ + +Typically, this is invoked as `git-read-tree -m $H $M`, where $H +is the head commit of the current repository, and $M is the head +of a foreign tree, which is simply ahead of $H (i.e. we are in a +fast forward situation). + +When two trees are specified, the user is telling git-read-tree +the following: + + 1. The current index and work tree is derived from $H, but + the user may have local changes in them since $H; + + 2. The user wants to fast-forward to $M. + +In this case, the `git-read-tree -m $H $M` command makes sure +that no local change is lost as the result of this "merge". +Here are the "carry forward" rules: + + I (index) H M Result + ------------------------------------------------------- + 0 nothing nothing nothing (does not happen) + 1 nothing nothing exists use M + 2 nothing exists nothing remove path from index + 3 nothing exists exists use M + + clean I==H I==M + ------------------ + 4 yes N/A N/A nothing nothing keep index + 5 no N/A N/A nothing nothing keep index + + 6 yes N/A yes nothing exists keep index + 7 no N/A yes nothing exists keep index + 8 yes N/A no nothing exists fail + 9 no N/A no nothing exists fail + + 10 yes yes N/A exists nothing remove path from index + 11 no yes N/A exists nothing fail + 12 yes no N/A exists nothing fail + 13 no no N/A exists nothing fail + + clean (H=M) + ------ + 14 yes exists exists keep index + 15 no exists exists keep index + + clean I==H I==M (H!=M) + ------------------ + 16 yes no no exists exists fail + 17 no no no exists exists fail + 18 yes no yes exists exists keep index + 19 no no yes exists exists keep index + 20 yes yes no exists exists use M + 21 no yes no exists exists fail + +In all "keep index" cases, the index entry stays as in the +original index file. If the entry were not up to date, +git-read-tree keeps the copy in the work tree intact when +operating under the -u flag. + +When this form of git-read-tree returns successfully, you can +see what "local changes" you made are carried forward by running +`git-diff-index --cached $M`. Note that this does not +necessarily match `git-diff-index --cached $H` would have +produced before such a two tree merge. This is because of cases +18 and 19 --- if you already had the changes in $M (e.g. maybe +you picked it up via e-mail in a patch form), `git-diff-index +--cached $H` would have told you about the change before this +merge, but it would not show in `git-diff-index --cached $M` +output after two-tree merge. + + +3-Way Merge +~~~~~~~~~~~ +Each "index" entry has two bits worth of "stage" state. stage 0 is the +normal one, and is the only one you'd see in any kind of normal use. + +However, when you do `git-read-tree` with three trees, the "stage" +starts out at 1. + +This means that you can do + +---------------- +$ git-read-tree -m <tree1> <tree2> <tree3> +---------------- + +and you will end up with an index with all of the <tree1> entries in +"stage1", all of the <tree2> entries in "stage2" and all of the +<tree3> entries in "stage3". When performing a merge of another +branch into the current branch, we use the common ancestor tree +as <tree1>, the current branch head as <tree2>, and the other +branch head as <tree3>. + +Furthermore, `git-read-tree` has special-case logic that says: if you see +a file that matches in all respects in the following states, it +"collapses" back to "stage0": + + - stage 2 and 3 are the same; take one or the other (it makes no + difference - the same work has been done on our branch in + stage 2 and their branch in stage 3) + + - stage 1 and stage 2 are the same and stage 3 is different; take + stage 3 (our branch in stage 2 did not do anything since the + ancestor in stage 1 while their branch in stage 3 worked on + it) + + - stage 1 and stage 3 are the same and stage 2 is different take + stage 2 (we did something while they did nothing) + +The `git-write-tree` command refuses to write a nonsensical tree, and it +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, +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 +you are trying to merge (stage 2 and 3 respectively). + +The order of stages 1, 2 and 3 (hence the order of three +<tree-ish> command line arguments) are significant when you +start a 3-way merge with an index file that is already +populated. Here is an outline of how the algorithm works: + +- if a file exists in identical format in all three trees, it will + automatically collapse to "merged" state by git-read-tree. + +- a file that has _any_ difference what-so-ever in the three trees + will stay as separate entries in the index. It's up to "porcelain + policy" to determine how to remove the non-0 stages, and insert a + merged version. + +- 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 (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, + since they've already been done. + + * if you find a "stage1", but no matching "stage2" or "stage3", you + know it's been removed from both trees (it only existed in the + original tree), and you remove that entry. + + * if you find a matching "stage2" and "stage3" tree, you remove one + of them, and turn the other into a "stage0" entry. Remove any + matching "stage1" entry if it exists too. .. all the normal + trivial rules .. + +You would normally use `git-merge-index` with supplied +`git-merge-one-file` to do this last step. The script updates +the files in the working tree as it merges each path and at the +end of a successful merge. + +When you start a 3-way merge with an index file that is already +populated, it is assumed that it represents the state of the +files in your work tree, and you can even have files with +changes unrecorded in the index file. It is further assumed +that this state is "derived" from the stage 2 tree. The 3-way +merge refuses to run if it finds an entry in the original index +file that does not match stage 2. + +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 +committed last to your repository: + +---------------- +$ JC=`git-rev-parse --verify "HEAD^0"` +$ git-checkout-index -f -u -a $JC +---------------- + +You do random edits, without running git-update-index. And then +you notice that the tip of your "upstream" tree has advanced +since you pulled from him: + +---------------- +$ git-fetch git://.... linus +$ LT=`cat .git/FETCH_HEAD` +---------------- + +Your work tree is still based on your HEAD ($JC), but you have +some edits since. Three-way merge makes sure that you have not +added or modified index entries since $JC, and if you haven't, +then does the right thing. So with the following sequence: + +---------------- +$ git-read-tree -m -u `git-merge-base $JC $LT` $JC $LT +$ git-merge-index git-merge-one-file -a +$ echo "Merge with Linus" | \ + git-commit-tree `git-write-tree` -p $JC -p $LT +---------------- + +what you would commit is a pure merge between $JC and $LT without +your work-in-progress changes, and your work tree would be +updated to the result of the merge. + +However, if you have local changes in the working tree that +would be overwritten by this merge,`git-read-tree` will refuse +to run to prevent your changes from being lost. + +In other words, there is no need to worry about what exists only +in the working tree. When you have local changes in a part of +the project that is not involved in the merge, your changes do +not interfere with the merge, and are kept intact. When they +*do* interfere, the merge does not even start (`git-read-tree` +complains loudly and fails without modifying anything). In such +a case, you can simply continue doing what you were in the +middle of doing, and when your working tree is ready (i.e. you +have finished your work-in-progress), attempt the merge again. + + +See Also +-------- +gitlink:git-write-tree[1]; gitlink:git-ls-files[1] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt new file mode 100644 index 0000000000..f2ef1f7dc0 --- /dev/null +++ b/Documentation/git-rebase.txt @@ -0,0 +1,234 @@ +git-rebase(1) +============= + +NAME +---- +git-rebase - Forward-port local commits to the updated upstream head + +SYNOPSIS +-------- +'git-rebase' [-v] [--merge] [-C<n>] [--onto <newbase>] <upstream> [<branch>] + +'git-rebase' --continue | --skip | --abort + +DESCRIPTION +----------- +git-rebase replaces <branch> with a new branch of the same name. When +the --onto option is provided the new branch starts out with a HEAD equal +to <newbase>, otherwise it is equal to <upstream>. It then attempts to +create a new commit for each commit from the original <branch> that does +not exist in the <upstream> branch. + +It is possible that a merge failure will prevent this process from being +completely automatic. You will have to resolve any such merge failure +and run `git rebase --continue`. Another option is to bypass the commit +that caused the merge failure with `git rebase --skip`. To restore the +original <branch> and remove the .dotest working files, use the command +`git rebase --abort` instead. + +Note that if <branch> is not specified on the command line, the currently +checked out branch is used. + +Assume the following history exists and the current branch is "topic": + +------------ + A---B---C topic + / + D---E---F---G master +------------ + +From this point, the result of either of the following commands: + + + git-rebase master + git-rebase master topic + +would be: + +------------ + A'--B'--C' topic + / + D---E---F---G master +------------ + +The latter form is just a short-hand of `git checkout topic` +followed by `git rebase master`. + +Here is how you would transplant a topic branch based on one +branch to another, to pretend that you forked the topic branch +from the latter branch, using `rebase --onto`. + +First let's assume your 'topic' is based on branch 'next'. +For example feature developed in 'topic' depends on some +functionality which is found in 'next'. + +------------ + o---o---o---o---o master + \ + o---o---o---o---o next + \ + o---o---o topic +------------ + +We would want to make 'topic' forked from branch 'master', +for example because the functionality 'topic' branch depend on +got merged into more stable 'master' branch, like this: + +------------ + o---o---o---o---o master + | \ + | o'--o'--o' topic + \ + o---o---o---o---o next +------------ + +We can get this using the following command: + + git-rebase --onto master next topic + + +Another example of --onto option is to rebase part of a +branch. If we have the following situation: + +------------ + H---I---J topicB + / + E---F---G topicA + / + A---B---C---D master +------------ + +then the command + + git-rebase --onto master topicA topicB + +would result in: + +------------ + H'--I'--J' topicB + / + | E---F---G topicA + |/ + A---B---C---D master +------------ + +This is useful when topicB does not depend on topicA. + +A range of commits could also be removed with rebase. If we have +the following situation: + +------------ + E---F---G---H---I---J topicA +------------ + +then the command + + git-rebase --onto topicA~5 topicA~2 topicA + +would result in the removal of commits F and G: + +------------ + E---H'---I'---J' topicA +------------ + +This is useful if F and G were flawed in some way, or should not be +part of topicA. Note that the argument to --onto and the <upstream> +parameter can be any valid commit-ish. + +In case of conflict, git-rebase will stop at the first problematic commit +and leave conflict markers in the tree. You can use git diff to locate +the markers (<<<<<<) and make edits to resolve the conflict. For each +file you edit, you need to tell git that the conflict has been resolved, +typically this would be done with + + + git update-index <filename> + + +After resolving the conflict manually and updating the index with the +desired resolution, you can continue the rebasing process with + + + git rebase --continue + + +Alternatively, you can undo the git-rebase with + + + git rebase --abort + +OPTIONS +------- +<newbase>:: + Starting point at which to create the new commits. If the + --onto option is not specified, the starting point is + <upstream>. May be any valid commit, and not just an + existing branch name. + +<upstream>:: + Upstream branch to compare against. May be any valid commit, + not just an existing branch name. + +<branch>:: + Working branch; defaults to HEAD. + +--continue:: + Restart the rebasing process after having resolved a merge conflict. + +--abort:: + Restore the original branch and abort the rebase operation. + +--skip:: + Restart the rebasing process by skipping the current patch. + +--merge:: + Use merging strategies to rebase. When the recursive (default) merge + strategy is used, this allows rebase to be aware of renames on the + upstream side. + +-s <strategy>, \--strategy=<strategy>:: + Use the given merge strategy; can be supplied more than + once to specify them in the order they should be tried. + If there is no `-s` option, a built-in list of strategies + is used instead (`git-merge-recursive` when merging a single + head, `git-merge-octopus` otherwise). This implies --merge. + +-v, \--verbose:: + Display a diffstat of what changed upstream since the last rebase. + +-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 must match. By default no context is + ever ignored. + +include::merge-strategies.txt[] + +NOTES +----- +When you rebase a branch, you are changing its history in a way that +will cause problems for anyone who already has a copy of the branch +in their repository and tries to pull updates from you. You should +understand the implications of using 'git rebase' on a repository that +you share. + +When the git rebase command is run, it will first execute a "pre-rebase" +hook if one exists. You can use this hook to do sanity checks and +reject the rebase if it isn't appropriate. Please see the template +pre-rebase hook script for an example. + +You must be in the top directory of your project to start (or continue) +a rebase. Upon completion, <branch> will be the current branch. + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt new file mode 100644 index 0000000000..10e8c46c4c --- /dev/null +++ b/Documentation/git-receive-pack.txt @@ -0,0 +1,100 @@ +git-receive-pack(1) +=================== + +NAME +---- +git-receive-pack - Receive what is pushed into the repository + + +SYNOPSIS +-------- +'git-receive-pack' <directory> + +DESCRIPTION +----------- +Invoked by 'git-send-pack' and updates the repository with the +information fed from the remote end. + +This command is usually not invoked directly by the end user. +The UI for the protocol is on the 'git-send-pack' side, and the +program pair is meant to be used to push updates to remote +repository. For pull operations, see 'git-fetch-pack'. + +The command allows for creation and fast forwarding of sha1 refs +(heads/tags) on the remote end (strictly speaking, it is the +local end receive-pack runs, but to the user who is sitting at +the send-pack end, it is updating the remote. Confused?) + +Before each ref is updated, if $GIT_DIR/hooks/update file exists +and executable, it is called with three parameters: + + $GIT_DIR/hooks/update refname sha1-old sha1-new + +The refname parameter is relative to $GIT_DIR; e.g. for the +master head this is "refs/heads/master". Two sha1 are the +object names for the refname before and after the update. Note +that the hook is called before the refname is updated, so either +sha1-old is 0{40} (meaning there is no such ref yet), or it +should match what is recorded in refname. + +The hook should exit with non-zero status if it wants to +disallow updating the named ref. Otherwise it should exit with +zero. + +Using this hook, it is easy to generate mails on updates to +the local repository. This example script sends a mail with +the commits pushed to the repository: + + #!/bin/sh + # mail out commit update information. + if expr "$2" : '0*$' >/dev/null + then + echo "Created a new ref, with the following commits:" + git-rev-list --pretty "$2" + else + echo "New commits:" + git-rev-list --pretty "$3" "^$2" + fi | + mail -s "Changes to ref $1" commit-list@mydomain + exit 0 + +Another hook $GIT_DIR/hooks/post-update, if exists and +executable, is called with the list of refs that have been +updated. This can be used to implement repository wide cleanup +task if needed. The exit code from this hook invocation is +ignored; the only thing left for git-receive-pack to do at that +point is to exit itself anyway. This hook can be used, for +example, to run "git-update-server-info" if the repository is +packed and is served via a dumb transport. + + #!/bin/sh + exec git-update-server-info + +There are other real-world examples of using update and +post-update hooks found in the Documentation/howto directory. + +git-receive-pack honours the receive.denyNonFastforwards flag, which +tells it if updates to a ref should be denied if they are not fast-forwards. + +OPTIONS +------- +<directory>:: + The repository to sync into. + + +SEE ALSO +-------- +gitlink:git-send-pack[1] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-reflog.txt b/Documentation/git-reflog.txt new file mode 100644 index 0000000000..1e343bcdcd --- /dev/null +++ b/Documentation/git-reflog.txt @@ -0,0 +1,68 @@ +git-reflog(1) +============= + +NAME +---- +git-reflog - Manage reflog information + + +SYNOPSIS +-------- +'git reflog' <subcommand> <options> + +DESCRIPTION +----------- +The command takes various subcommands, and different options +depending on the subcommand: + +[verse] +git reflog expire [--dry-run] [--stale-fix] + [--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>... + +git reflog [show] [log-options] + +Reflog is a mechanism to record when the tip of branches are +updated. This command is to manage the information recorded in it. + +The subcommand "expire" is used to prune older reflog entries. +Entries older than `expire` time, or entries older than +`expire-unreachable` time and are not reachable from the current +tip, are removed from the reflog. This is typically not used +directly by the end users -- instead, see gitlink:git-gc[1]. + +The subcommand "show" (which is also the default, in the absense of any +subcommands) will take all the normal log options, and show the log of +the current branch. It is basically an alias for 'git log -g --abbrev-commit +--pretty=oneline', see gitlink:git-log[1]. + + +OPTIONS +------- + +--expire=<time>:: + Entries older than this time are pruned. Without the + option it is taken from configuration `gc.reflogExpire`, + which in turn defaults to 90 days. + +--expire-unreachable=<time>:: + Entries older than this time and are not reachable from + the current tip of the branch are pruned. Without the + option it is taken from configuration + `gc.reflogExpireUnreachable`, which in turn defaults to + 30 days. + +--all:: + Instead of listing <refs> explicitly, prune all refs. + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-relink.txt b/Documentation/git-relink.txt new file mode 100644 index 0000000000..aca60120c8 --- /dev/null +++ b/Documentation/git-relink.txt @@ -0,0 +1,37 @@ +git-relink(1) +============= + +NAME +---- +git-relink - Hardlink common objects in local repositories + +SYNOPSIS +-------- +'git-relink' [--safe] <dir> <dir> [<dir>]\* + +DESCRIPTION +----------- +This will scan 2 or more object repositories and look for common objects, check +if they are hardlinked, and replace one with a hardlink to the other if not. + +OPTIONS +------- +--safe:: + Stops if two objects with the same hash exist but have different sizes. + Default is to warn and continue. + +<dir>:: + Directories containing a .git/objects/ subdirectory. + +Author +------ +Written by Ryan Anderson <ryan@michonline.com> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-remote.txt b/Documentation/git-remote.txt new file mode 100644 index 0000000000..a60c31a315 --- /dev/null +++ b/Documentation/git-remote.txt @@ -0,0 +1,96 @@ +git-remote(1) +============ + +NAME +---- +git-remote - manage set of tracked repositories + + +SYNOPSIS +-------- +[verse] +'git-remote' +'git-remote' add <name> <url> +'git-remote' show <name> +'git-remote' prune <name> + +DESCRIPTION +----------- + +Manage the set of repositories ("remotes") whose branches you track. + + +COMMANDS +-------- + +With no arguments, shows a list of existing remotes. Several +subcommands are available to perform operations on the remotes. + +'add':: + +Adds a remote named <name> for the repository at +<url>. The command `git fetch <name>` can then be used to create and +update remote-tracking branches <name>/<branch>. + +'show':: + +Gives some information about the remote <name>. + +'prune':: + +Deletes all stale tracking branches under <name>. +These stale branches have already been removed from the remote repository +referenced by <name>, but are still locally available in "remotes/<name>". + + +DISCUSSION +---------- + +The remote configuration is achieved using the `remote.origin.url` and +`remote.origin.fetch` configuration variables. (See +gitlink:git-config[1]). + +Examples +-------- + +Add a new remote, fetch, and check out a branch from it: + +------------ +$ git remote +origin +$ git branch -r +origin/master +$ git remote add linux-nfs git://linux-nfs.org/pub/nfs-2.6.git +$ git remote +linux-nfs +origin +$ git fetch +* refs/remotes/linux-nfs/master: storing branch 'master' ... + commit: bf81b46 +$ git branch -r +origin/master +linux-nfs/master +$ git checkout -b nfs linux-nfs/master +... +------------ + +See Also +-------- +gitlink:git-fetch[1] +gitlink:git-branch[1] +gitlink:git-config[1] + +Author +------ +Written by Junio Hamano + + +Documentation +-------------- +Documentation by J. Bruce Fields and the git-list <git@vger.kernel.org>. + + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-repack.txt b/Documentation/git-repack.txt new file mode 100644 index 0000000000..d39abc126d --- /dev/null +++ b/Documentation/git-repack.txt @@ -0,0 +1,99 @@ +git-repack(1) +============= + +NAME +---- +git-repack - Pack unpacked objects in a repository + + +SYNOPSIS +-------- +'git-repack' [-a] [-d] [-f] [-l] [-n] [-q] [--window=N] [--depth=N] + +DESCRIPTION +----------- + +This script is used to combine all objects that do not currently +reside in a "pack", into a pack. + +A pack is a collection of objects, individually compressed, with +delta compression applied, stored in a single file, with an +associated index file. + +Packs are used to reduce the load on mirror systems, backup +engines, disk storage, etc. + +OPTIONS +------- + +-a:: + Instead of incrementally packing the unpacked objects, + pack everything available into a single pack. + Especially useful when packing a repository that is used + for private development and there is no need to worry + about people fetching via dumb file transfer protocols + from it. Use with '-d'. + +-d:: + After packing, if the newly created packs make some + existing packs redundant, remove the redundant packs. + Also runs gitlink:git-prune-packed[1]. + +-l:: + Pass the `--local` option to `git pack-objects`, see + gitlink:git-pack-objects[1]. + +-f:: + Pass the `--no-reuse-delta` option to `git pack-objects`, see + gitlink:git-pack-objects[1]. + +-q:: + Pass the `-q` option to `git pack-objects`, see + gitlink:git-pack-objects[1]. + +-n:: + Do not update the server information with + `git update-server-info`. + +--window=[N], --depth=[N]:: + These two options affect how the objects contained in the pack are + stored using delta compression. The objects are first internally + sorted by type, size and optionally names and compared against the + other objects within `--window` to see if using delta compression saves + space. `--depth` limits the maximum delta depth; making it too deep + affects the performance on the unpacker side, because delta data needs + to be applied that many times to get to the necessary object. + The default value for both --window and --depth is 10. + + +Configuration +------------- + +When configuration variable `repack.UseDeltaBaseOffset` is set +for the repository, the command passes `--delta-base-offset` +option to `git-pack-objects`; this typically results in slightly +smaller packs, but the generated packs are incompatible with +versions of git older than (and including) v1.4.3; do not set +the variable in a repository that older version of git needs to +be able to read (this includes repositories from which packs can +be copied out over http or rsync, and people who obtained packs +that way can try to use older git with it). + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Ryan Anderson <ryan@michonline.com> + +See Also +-------- +gitlink:git-pack-objects[1] +gitlink:git-prune-packed[1] + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-repo-config.txt b/Documentation/git-repo-config.txt new file mode 100644 index 0000000000..2deba31763 --- /dev/null +++ b/Documentation/git-repo-config.txt @@ -0,0 +1,18 @@ +git-repo-config(1) +================== + +NAME +---- +git-repo-config - Get and set repository or global options + + +SYNOPSIS +-------- +'git-repo-config' ... + + +DESCRIPTION +----------- + +This is a synonym for gitlink:git-config[1]. Please refer to the +documentation of that command. diff --git a/Documentation/git-request-pull.txt b/Documentation/git-request-pull.txt new file mode 100644 index 0000000000..478a5fd6b7 --- /dev/null +++ b/Documentation/git-request-pull.txt @@ -0,0 +1,40 @@ +git-request-pull(1) +=================== + +NAME +---- +git-request-pull - Generates a summary of pending changes + +SYNOPSIS +-------- +'git-request-pull' <start> <url> [<end>] + +DESCRIPTION +----------- + +Summarizes the changes between two commits to the standard output, and includes +the given URL in the generated summary. + +OPTIONS +------- +<start>:: + Commit to start at. + +<url>:: + URL to include in the summary. + +<end>:: + Commit to send at; defaults to HEAD. + +Author +------ +Written by Ryan Anderson <ryan@michonline.com> and Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-rerere.txt b/Documentation/git-rerere.txt new file mode 100644 index 0000000000..139b6eb773 --- /dev/null +++ b/Documentation/git-rerere.txt @@ -0,0 +1,212 @@ +git-rerere(1) +============= + +NAME +---- +git-rerere - Reuse recorded resolution of conflicted merges + +SYNOPSIS +-------- +'git-rerere' [clear|diff|status|gc] + +DESCRIPTION +----------- + +In a workflow that employs relatively long lived topic branches, +the developer sometimes needs to resolve the same conflict over +and over again until the topic branches are done (either merged +to the "release" branch, or sent out and accepted upstream). + +This command helps this process by recording conflicted +automerge results and corresponding hand-resolve results on the +initial manual merge, and later by noticing the same automerge +results and applying the previously recorded hand resolution. + +[NOTE] +You need to create `$GIT_DIR/rr-cache` directory to enable this +command. + + +COMMANDS +-------- + +Normally, git-rerere is run without arguments or user-intervention. +However, it has several commands that allow it to interact with +its working state. + +'clear':: + +This resets the metadata used by rerere if a merge resolution is to be +is aborted. Calling gitlink:git-am[1] --skip or gitlink:git-rebase[1] +[--skip|--abort] will automatically invoke this command. + +'diff':: + +This displays diffs for the current state of the resolution. It is +useful for tracking what has changed while the user is resolving +conflicts. Additional arguments are passed directly to the system +diff(1) command installed in PATH. + +'status':: + +Like diff, but this only prints the filenames that will be tracked +for resolutions. + +'gc':: + +This command is used to prune records of conflicted merge that +occurred long time ago. By default, conflicts older than 15 +days that you have not recorded their resolution, and conflicts +older than 60 days, are pruned. These are controlled with +`gc.rerereunresolved` and `gc.rerereresolved` configuration +variables. + + +DISCUSSION +---------- + +When your topic branch modifies overlapping area that your +master branch (or upstream) touched since your topic branch +forked from it, you may want to test it with the latest master, +even before your topic branch is ready to be pushed upstream: + +------------ + o---*---o topic + / + o---o---o---*---o---o master +------------ + +For such a test, you need to merge master and topic somehow. +One way to do it is to pull master into the topic branch: + +------------ + $ git checkout topic + $ git merge master + + o---*---o---+ topic + / / + o---o---o---*---o---o master +------------ + +The commits marked with `*` touch the same area in the same +file; you need to resolve the conflicts when creating the commit +marked with `+`. Then you can test the result to make sure your +work-in-progress still works with what is in the latest master. + +After this test merge, there are two ways to continue your work +on the topic. The easiest is to build on top of the test merge +commit `+`, and when your work in the topic branch is finally +ready, pull the topic branch into master, and/or ask the +upstream to pull from you. By that time, however, the master or +the upstream might have been advanced since the test merge `+`, +in which case the final commit graph would look like this: + +------------ + $ git checkout topic + $ git merge master + $ ... work on both topic and master branches + $ git checkout master + $ git merge topic + + o---*---o---+---o---o topic + / / \ + o---o---o---*---o---o---o---o---+ master +------------ + +When your topic branch is long-lived, however, your topic branch +would end up having many such "Merge from master" commits on it, +which would unnecessarily clutter the development history. +Readers of the Linux kernel mailing list may remember that Linus +complained about such too frequent test merges when a subsystem +maintainer asked to pull from a branch full of "useless merges". + +As an alternative, to keep the topic branch clean of test +merges, you could blow away the test merge, and keep building on +top of the tip before the test merge: + +------------ + $ git checkout topic + $ git merge master + $ git reset --hard HEAD^ ;# rewind the test merge + $ ... work on both topic and master branches + $ git checkout master + $ git merge topic + + o---*---o-------o---o topic + / \ + o---o---o---*---o---o---o---o---+ master +------------ + +This would leave only one merge commit when your topic branch is +finally ready and merged into the master branch. This merge +would require you to resolve the conflict, introduced by the +commits marked with `*`. However, often this conflict is the +same conflict you resolved when you created the test merge you +blew away. `git-rerere` command helps you to resolve this final +conflicted merge using the information from your earlier hand +resolve. + +Running `git-rerere` command immediately after a conflicted +automerge records the conflicted working tree files, with the +usual conflict markers `<<<<<<<`, `=======`, and `>>>>>>>` in +them. Later, after you are done resolving the conflicts, +running `git-rerere` again records the resolved state of these +files. Suppose you did this when you created the test merge of +master into the topic branch. + +Next time, running `git-rerere` after seeing a conflicted +automerge, if the conflict is the same as the earlier one +recorded, it is noticed and a three-way merge between the +earlier conflicted automerge, the earlier manual resolution, and +the current conflicted automerge is performed by the command. +If this three-way merge resolves cleanly, the result is written +out to your working tree file, so you would not have to manually +resolve it. Note that `git-rerere` leaves the index file alone, +so you still need to do the final sanity checks with `git diff` +(or `git diff -c`) and `git update-index` when you are +satisfied. + +As a convenience measure, `git-merge` automatically invokes +`git-rerere` when it exits with a failed automerge, which +records it if it is a new conflict, or reuses the earlier hand +resolve when it is not. `git-commit` also invokes `git-rerere` +when recording a merge result. What this means is that you do +not have to do anything special yourself (Note: you still have +to create `$GIT_DIR/rr-cache` directory to enable this command). + +In our example, when you did the test merge, the manual +resolution is recorded, and it will be reused when you do the +actual merge later with updated master and topic branch, as long +as the earlier resolution is still applicable. + +The information `git-rerere` records is also used when running +`git-rebase`. After blowing away the test merge and continuing +development on the topic branch: + +------------ + o---*---o-------o---o topic + / + o---o---o---*---o---o---o---o master + + $ git rebase master topic + + o---*---o-------o---o topic + / + o---o---o---*---o---o---o---o master +------------ + +you could run `git rebase master topic`, to keep yourself +up-to-date even before your topic is ready to be sent upstream. +This would result in falling back to three-way merge, and it +would conflict the same way the test merge you resolved earlier. +`git-rerere` is run by `git rebase` to help you resolve this +conflict. + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-reset.txt b/Documentation/git-reset.txt new file mode 100644 index 0000000000..04475a9216 --- /dev/null +++ b/Documentation/git-reset.txt @@ -0,0 +1,184 @@ +git-reset(1) +============ + +NAME +---- +git-reset - Reset current HEAD to the specified state + +SYNOPSIS +-------- +[verse] +'git-reset' [--mixed | --soft | --hard] [<commit>] +'git-reset' [--mixed] <commit> [--] <paths>... + +DESCRIPTION +----------- +Sets the current head to the specified commit and optionally resets the +index and working tree to match. + +This command is useful if you notice some small error in a recent +commit (or set of commits) and want to redo that part without showing +the undo in the history. + +If you want to undo a commit other than the latest on a branch, +gitlink:git-revert[1] is your friend. + +The second form with 'paths' is used to revert selected paths in +the index from a given commit, without moving HEAD. + + +OPTIONS +------- +--mixed:: + 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. + +--soft:: + Does not touch the index file nor the working tree at all, but + requires them to be in a good order. This leaves all your changed + files "Added but not yet committed", as gitlink:git-status[1] would + put it. + +--hard:: + Matches the working tree and index to that of the tree being + switched to. Any changes to tracked files in the working tree + since <commit> are lost. + +<commit>:: + Commit to make the current HEAD. + +Examples +-------- + +Undo a commit and redo:: ++ +------------ +$ git commit ... +$ git reset --soft HEAD^ <1> +$ edit <2> +$ git commit -a -c ORIG_HEAD <3> +------------ ++ +<1> This is most often done when you remembered what you +just committed is incomplete, or you misspelled your commit +message, or both. Leaves working tree as it was before "reset". +<2> make corrections to working tree files. +<3> "reset" copies the old head to .git/ORIG_HEAD; redo the +commit by starting with its log message. If you do not need to +edit the message further, you can give -C option instead. + +Undo commits permanently:: ++ +------------ +$ git commit ... +$ git reset --hard HEAD~3 <1> +------------ ++ +<1> The last three commits (HEAD, HEAD^, and HEAD~2) were bad +and you do not want to ever see them again. Do *not* do this if +you have already given these commits to somebody else. + +Undo a commit, making it a topic branch:: ++ +------------ +$ git branch topic/wip <1> +$ git reset --hard HEAD~3 <2> +$ git checkout topic/wip <3> +------------ ++ +<1> You have made some commits, but realize they were premature +to be in the "master" branch. You want to continue polishing +them in a topic branch, so create "topic/wip" branch off of the +current HEAD. +<2> Rewind the master branch to get rid of those three commits. +<3> Switch to "topic/wip" branch and keep working. + +Undo update-index:: ++ +------------ +$ edit <1> +$ git-update-index frotz.c filfre.c +$ mailx <2> +$ git reset <3> +$ git pull git://info.example.com/ nitfol <4> +------------ ++ +<1> you are happily working on something, and find the changes +in these files are in good order. You do not want to see them +when you run "git diff", because you plan to work on other files +and changes with these files are distracting. +<2> somebody asks you to pull, and the changes sounds worthy of merging. +<3> however, you already dirtied the index (i.e. your index does +not match the HEAD commit). But you know the pull you are going +to make does not affect frotz.c nor filfre.c, so you revert the +index changes for these two files. Your changes in working tree +remain there. +<4> then you can pull and merge, leaving frotz.c and filfre.c +changes still in the working tree. + +Undo a merge or pull:: ++ +------------ +$ git pull <1> +Auto-merging nitfol +CONFLICT (content): Merge conflict in nitfol +Automatic merge failed/prevented; fix up by hand +$ git reset --hard <2> +$ git pull . topic/branch <3> +Updating from 41223... to 13134... +Fast forward +$ git reset --hard ORIG_HEAD <4> +------------ ++ +<1> try to update from the upstream resulted in a lot of +conflicts; you were not ready to spend a lot of time merging +right now, so you decide to do that later. +<2> "pull" has not made merge commit, so "git reset --hard" +which is a synonym for "git reset --hard HEAD" clears the mess +from the index file and the working tree. +<3> merge a topic branch into the current branch, which resulted +in a fast forward. +<4> but you decided that the topic branch is not ready for public +consumption yet. "pull" or "merge" always leaves the original +tip of the current branch in ORIG_HEAD, so resetting hard to it +brings your index file and the working tree back to that state, +and resets the tip of the branch to that commit. + +Interrupted workflow:: ++ +Suppose you are interrupted by an urgent fix request while you +are in the middle of a large change. The files in your +working tree are not in any shape to be committed yet, but you +need to get to the other branch for a quick bugfix. ++ +------------ +$ git checkout feature ;# you were working in "feature" branch and +$ work work work ;# got interrupted +$ git commit -a -m 'snapshot WIP' <1> +$ git checkout master +$ fix fix fix +$ git commit ;# commit with real log +$ git checkout feature +$ git reset --soft HEAD^ ;# go back to WIP state <2> +$ git reset <3> +------------ ++ +<1> This commit will get blown away so a throw-away log message is OK. +<2> This removes the 'WIP' commit from the commit history, and sets + your working tree to the state just before you made that snapshot. +<3> At this point the index file still has all the WIP changes you + committed as 'snapshot WIP'. This updates the index to show your + WIP files as uncommitted. + +Author +------ +Written by Junio C Hamano <junkio@cox.net> and Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-resolve.txt b/Documentation/git-resolve.txt new file mode 100644 index 0000000000..7fde665fb5 --- /dev/null +++ b/Documentation/git-resolve.txt @@ -0,0 +1,38 @@ +git-resolve(1) +============== + +NAME +---- +git-resolve - Merge two commits + + +SYNOPSIS +-------- +'git-resolve' <current> <merged> <message> + +DESCRIPTION +----------- +DEPRECATED and will be removed in 1.5.1. Use `git-merge` instead. + +Given two commits and a merge message, merge the <merged> commit +into <current> commit, with the commit log message <message>. + +When <current> is a descendant of <merged>, or <current> is an +ancestor of <merged>, no new commit is created and the <message> +is ignored. The former is informally called "already up to +date", and the latter is often called "fast forward". + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and +Dan Holmsand <holmsand@gmail.com>. + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-rev-list.txt b/Documentation/git-rev-list.txt new file mode 100644 index 0000000000..c742117595 --- /dev/null +++ b/Documentation/git-rev-list.txt @@ -0,0 +1,305 @@ +git-rev-list(1) +=============== + +NAME +---- +git-rev-list - Lists commit objects in reverse chronological order + + +SYNOPSIS +-------- +[verse] +'git-rev-list' [ \--max-count=number ] + [ \--skip=number ] + [ \--max-age=timestamp ] + [ \--min-age=timestamp ] + [ \--sparse ] + [ \--no-merges ] + [ \--remove-empty ] + [ \--not ] + [ \--all ] + [ \--stdin ] + [ \--topo-order ] + [ \--parents ] + [ \--encoding[=<encoding>] ] + [ \--(author|committer|grep)=<pattern> ] + [ [\--objects | \--objects-edge] [ \--unpacked ] ] + [ \--pretty | \--header ] + [ \--bisect ] + [ \--merge ] + [ \--walk-reflogs ] + <commit>... [ \-- <paths>... ] + +DESCRIPTION +----------- + +Lists commit objects in reverse chronological order starting at the +given commit(s), taking ancestry relationship into account. This is +useful to produce human-readable log output. + +Commits which are stated with a preceding '{caret}' cause listing to +stop at that point. Their parents are implied. Thus the following +command: + +----------------------------------------------------------------------- + $ git-rev-list foo bar ^baz +----------------------------------------------------------------------- + +means "list all the commits which are included in 'foo' and 'bar', but +not in 'baz'". + +A special notation "'<commit1>'..'<commit2>'" can be used as a +short-hand for "{caret}'<commit1>' '<commit2>'". For example, either of +the following may be used interchangeably: + +----------------------------------------------------------------------- + $ git-rev-list origin..HEAD + $ git-rev-list HEAD ^origin +----------------------------------------------------------------------- + +Another special notation is "'<commit1>'...'<commit2>'" which is useful +for merges. The resulting set of commits is the symmetric difference +between the two operands. The following two commands are equivalent: + +----------------------------------------------------------------------- + $ git-rev-list A B --not $(git-merge-base --all A B) + $ git-rev-list A...B +----------------------------------------------------------------------- + +gitlink:git-rev-list[1] is a very essential git program, since it +provides the ability to build and traverse commit ancestry graphs. For +this reason, it has a lot of different options that enables it to be +used by commands as different as gitlink:git-bisect[1] and +gitlink:git-repack[1]. + +OPTIONS +------- + +Commit Formatting +~~~~~~~~~~~~~~~~~ + +Using these options, gitlink:git-rev-list[1] will act similar to the +more specialized family of commit log tools: gitlink:git-log[1], +gitlink:git-show[1], and gitlink:git-whatchanged[1] + +include::pretty-formats.txt[] + +--relative-date:: + + Show dates relative to the current time, e.g. "2 hours ago". + Only takes effect for dates shown in human-readable format, such + as when using "--pretty". + +--header:: + + Print the contents of the commit in raw-format; each record is + separated with a NUL character. + +--parents:: + + Print the parents of the commit. + +Diff Formatting +~~~~~~~~~~~~~~~ + +Below are listed options that control the formatting of diff output. +Some of them are specific to gitlink:git-rev-list[1], however other diff +options may be given. See gitlink:git-diff-files[1] for more options. + +-c:: + + This flag changes the way a merge commit is displayed. It shows + the differences from each of the parents to the merge result + simultaneously instead of showing pairwise diff between a parent + and the result one at a time. Furthermore, it lists only files + which were modified from all parents. + +--cc:: + + This flag implies the '-c' options and further compresses the + patch output by omitting hunks that show differences from only + one parent, or show the same change from all but one parent for + an Octopus merge. + +-r:: + + Show recursive diffs. + +-t:: + + Show the tree objects in the diff output. This implies '-r'. + +Commit Limiting +~~~~~~~~~~~~~~~ + +Besides specifying a range of commits that should be listed using the +special notations explained in the description, additional commit +limiting may be applied. + +-- + +-n 'number', --max-count='number':: + + Limit the number of commits output. + +--skip='number':: + + Skip 'number' commits before starting to show the commit output. + +--since='date', --after='date':: + + Show commits more recent than a specific date. + +--until='date', --before='date':: + + Show commits older than a specific date. + +--max-age='timestamp', --min-age='timestamp':: + + Limit the commits output to specified time range. + +--author='pattern', --committer='pattern':: + + Limit the commits output to ones with author/committer + header lines that match the specified pattern. + +--grep='pattern':: + + Limit the commits output to ones with log message that + matches the specified pattern. + +--remove-empty:: + + Stop when a given path disappears from the tree. + +--no-merges:: + + Do not print commits with more than one parent. + +--not:: + + Reverses the meaning of the '{caret}' prefix (or lack thereof) + for all following revision specifiers, up to the next '--not'. + +--all:: + + Pretend as if all the refs in `$GIT_DIR/refs/` are listed on the + command line as '<commit>'. + +--stdin:: + + In addition to the '<commit>' listed on the command + line, read them from the standard input. + +-g, --walk-reflogs:: + + Instead of walking the commit ancestry chain, walk + reflog entries from the most recent one to older ones. + When this option is used you cannot specify commits to + exclude (that is, '{caret}commit', 'commit1..commit2', + nor 'commit1...commit2' notations cannot be used). ++ +With '\--pretty' format other than oneline (for obvious reasons), +this causes the output to have two extra lines of information +taken from the reflog. By default, 'commit@{Nth}' notation is +used in the output. When the starting commit is specified as +'commit@{now}', output also uses 'commit@{timestamp}' notation +instead. Under '\--pretty=oneline', the commit message is +prefixed with this information on the same line. + +--merge:: + + After a failed merge, show refs that touch files having a + conflict and don't exist on all heads to merge. + +--boundary:: + + Output uninteresting commits at the boundary, which are usually + not shown. + +--dense, --sparse:: + +When optional paths are given, the default behaviour ('--dense') is to +only output commits that changes at least one of them, and also ignore +merges that do not touch the given paths. + +Use the '--sparse' flag to makes the command output all eligible commits +(still subject to count and age limitation), but apply merge +simplification nevertheless. + +--bisect:: + +Limit output to the one commit object which is roughly halfway between +the included and excluded commits. Thus, if + +----------------------------------------------------------------------- + $ git-rev-list --bisect foo ^bar ^baz +----------------------------------------------------------------------- + +outputs 'midpoint', the output of the two commands + +----------------------------------------------------------------------- + $ git-rev-list foo ^midpoint + $ git-rev-list midpoint ^bar ^baz +----------------------------------------------------------------------- + +would be of roughly the same length. Finding the change which +introduces a regression is thus reduced to a binary search: repeatedly +generate and test new 'midpoint's until the commit chain is of length +one. + +-- + +Commit Ordering +~~~~~~~~~~~~~~~ + +By default, the commits are shown in reverse chronological order. + +--topo-order:: + + This option makes them appear in topological order (i.e. + descendant commits are shown before their parents). + +--date-order:: + + This option is similar to '--topo-order' in the sense that no + parent comes before all of its children, but otherwise things + are still ordered in the commit timestamp order. + +Object Traversal +~~~~~~~~~~~~~~~~ + +These options are mostly targeted for packing of git repositories. + +--objects:: + + Print the object IDs of any object referenced by the listed + commits. 'git-rev-list --objects foo ^bar' thus means "send me + all object IDs which I need to download if I have the commit + object 'bar', but not 'foo'". + +--objects-edge:: + + Similar to '--objects', but also print the IDs of excluded + commits prefixed with a "-" character. This is used by + gitlink:git-pack-objects[1] to build "thin" pack, which records + objects in deltified form based on objects contained in these + excluded commits to reduce network traffic. + +--unpacked:: + + Only useful with '--objects'; print the object IDs that are not + in packs. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano, Jonas Fonseca +and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt new file mode 100644 index 0000000000..4041a16070 --- /dev/null +++ b/Documentation/git-rev-parse.txt @@ -0,0 +1,282 @@ +git-rev-parse(1) +================ + +NAME +---- +git-rev-parse - Pick out and massage parameters + + +SYNOPSIS +-------- +'git-rev-parse' [ --option ] <args>... + +DESCRIPTION +----------- + +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 +downstream of `git-rev-list`. This command is used to +distinguish between them. + + +OPTIONS +------- +--revs-only:: + Do not output flags and parameters not meant for + `git-rev-list` command. + +--no-revs:: + Do not output flags and parameters meant for + `git-rev-list` command. + +--flags:: + Do not output non-flag parameters. + +--no-flags:: + Do not output flag parameters. + +--default <arg>:: + If there is no parameter given by the user, use `<arg>` + instead. + +--verify:: + The parameter given must be usable as a single, valid + object name. Otherwise barf and abort. + +--sq:: + Usually the output is made one line per flag and + parameter. This option makes output a single line, + properly quoted for consumption by shell. Useful when + you expect your parameter to contain whitespaces and + newlines (e.g. when using pickaxe `-S` with + `git-diff-\*`). + +--not:: + When showing object names, prefix them with '{caret}' and + strip '{caret}' prefix from the object names that already have + one. + +--symbolic:: + Usually the object names are output in SHA1 form (with + possible '{caret}' prefix); this option makes them output in a + form as close to the original input as possible. + + +--all:: + Show all refs found in `$GIT_DIR/refs`. + +--branches:: + Show branch refs found in `$GIT_DIR/refs/heads`. + +--tags:: + Show tag refs found in `$GIT_DIR/refs/tags`. + +--remotes:: + Show tag refs found in `$GIT_DIR/refs/remotes`. + +--show-prefix:: + When the command is invoked from a subdirectory, show the + path of the current directory relative to the top-level + directory. + +--show-cdup:: + When the command is invoked from a subdirectory, show the + path of the top-level directory relative to the current + directory (typically a sequence of "../", or an empty string). + +--git-dir:: + Show `$GIT_DIR` if defined else show the path to the .git directory. + +--short, --short=number:: + Instead of outputting the full SHA1 values of object names try to + abbreviate them to a shorter unique name. When no length is specified + 7 is used. The minimum length is 4. + +--since=datestring, --after=datestring:: + Parses the date string, and outputs corresponding + --max-age= parameter for git-rev-list command. + +--until=datestring, --before=datestring:: + Parses the date string, and outputs corresponding + --min-age= parameter for git-rev-list command. + +<args>...:: + Flags and parameters to be parsed. + + +SPECIFYING REVISIONS +-------------------- + +A revision parameter typically, but not necessarily, names a +commit object. They use what is called an 'extended SHA1' +syntax. Here are various ways to spell object names. The +ones listed near the end of this list are to name trees and +blobs contained in a commit. + +* The full SHA1 object name (40-byte hexadecimal string), or + a substring of such that is unique within the repository. + E.g. dae86e1950b1277e545cee180551750029cfe735 and dae86e both + name the same commit object if there are no other object in + your repository whose object name starts with dae86e. + +* An output from `git-describe`; i.e. a closest tag, followed by a + dash, a `g`, and an abbreviated object name. + +* A symbolic ref name. E.g. 'master' typically means the commit + object referenced by $GIT_DIR/refs/heads/master. If you + happen to have both heads/master and tags/master, you can + explicitly say 'heads/master' to tell git which one you mean. + When ambiguous, a `<name>` is disambiguated by taking the + first match in the following rules: + + . if `$GIT_DIR/<name>` exists, that is what you mean (this is usually + useful only for `HEAD`, `FETCH_HEAD` and `MERGE_HEAD`); + + . otherwise, `$GIT_DIR/refs/<name>` if exists; + + . otherwise, `$GIT_DIR/refs/tags/<name>` if exists; + + . otherwise, `$GIT_DIR/refs/heads/<name>` if exists; + + . otherwise, `$GIT_DIR/refs/remotes/<name>` if exists; + + . otherwise, `$GIT_DIR/refs/remotes/<name>/HEAD` if exists. + +* A ref followed by the suffix '@' with 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 ref followed by the suffix '@' with an ordinal specification + enclosed in a brace pair (e.g. '\{1\}', '\{15\}') to specify + the n-th prior value of that ref. For example 'master@\{1\}' + is the immediate prior value of 'master' while 'master@\{5\}' + is the 5th prior value of 'master'. This suffix may only be used + immediately following a ref name and the ref must have an existing + log ($GIT_DIR/logs/<ref>). + +* You can use the '@' construct with an empty ref part to get at a + reflog of the current branch. For example, if you are on the + branch 'blabla', then '@\{1\}' means the same as 'blabla@\{1\}'. + +* 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 equivalent to 'rev{caret}1'). As a special rule, + 'rev{caret}0' means the commit itself and is used when 'rev' is the + object name of a tag object that refers to a commit object. + +* A suffix '{tilde}<n>' to a revision parameter means the commit + object that is the <n>th generation grand-parent of the named + commit object, following only the first parent. I.e. rev~3 is + equivalent to rev{caret}{caret}{caret} which is equivalent to + rev{caret}1{caret}1{caret}1. See below for a illustration of + the usage of this form. + +* A suffix '{caret}' followed by an object type name enclosed in + brace pair (e.g. `v0.99.8{caret}\{commit\}`) means the object + could be a tag, and dereference the tag recursively until an + object of that type is found or the object cannot be + dereferenced anymore (in which case, barf). `rev{caret}0` + introduced earlier is a short-hand for `rev{caret}\{commit\}`. + +* A suffix '{caret}' followed by an empty brace pair + (e.g. `v0.99.8{caret}\{\}`) means the object could be a tag, + and dereference the tag recursively until a non-tag object is + found. + +* A suffix ':' followed by a path; this names the blob or tree + at the given path in the tree-ish object named by the part + before the colon. + +* A colon, optionally followed by a stage number (0 to 3) and a + colon, followed by a path; this names a blob object in the + index at the given path. Missing stage number (and the colon + that follows it) names an stage 0 entry. + +Here is an illustration, by Jon Loeliger. Both node B and C are +a commit parents of commit node A. Parent commits are ordered +left-to-right. + + G H I J + \ / \ / + D E F + \ | / \ + \ | / | + \|/ | + B C + \ / + \ / + A + + A = = A^0 + B = A^ = A^1 = A~1 + C = A^2 = A^2 + D = A^^ = A^1^1 = A~2 + E = B^2 = A^^2 + F = B^3 = A^^3 + G = A^^^ = A^1^1^1 = A~3 + H = D^2 = B^^2 = A^^^2 = A~2^2 + I = F^ = B^3^ = A^^3^ + J = F^2 = B^3^2 = A^^3^2 + + +SPECIFYING RANGES +----------------- + +History traversing commands such as `git-log` operate on a set +of commits, not just a single commit. To these commands, +specifying a single revision with the notation described in the +previous section means the set of commits reachable from that +commit, following the commit ancestry chain. + +To exclude commits reachable from a commit, a prefix `{caret}` +notation is used. E.g. "`{caret}r1 r2`" means commits reachable +from `r2` but exclude the ones reachable from `r1`. + +This set operation appears so often that there is a shorthand +for it. "`r1..r2`" is equivalent to "`{caret}r1 r2`". It is +the difference of two sets (subtract the set of commits +reachable from `r1` from the set of commits reachable from +`r2`). + +A similar notation "`r1\...r2`" is called symmetric difference +of `r1` and `r2` and is defined as +"`r1 r2 --not $(git-merge-base --all r1 r2)`". +It it the set of commits that are reachable from either one of +`r1` or `r2` but not from both. + +Two other shorthands for naming a set that is formed by a commit +and its parent commits exists. `r1{caret}@` notation means all +parents of `r1`. `r1{caret}!` includes commit `r1` but excludes +its all parents. + +Here are a handful examples: + + D A B D + D F A B C D F + ^A G B D + ^A F B C F + G...I C D F G I + ^B G I C D F G I + F^@ A B C + F^! H D F H + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and +Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-revert.txt b/Documentation/git-revert.txt new file mode 100644 index 0000000000..8081bbaffa --- /dev/null +++ b/Documentation/git-revert.txt @@ -0,0 +1,59 @@ +git-revert(1) +============= + +NAME +---- +git-revert - Revert an existing commit + +SYNOPSIS +-------- +'git-revert' [--edit | --no-edit] [-n] <commit> + +DESCRIPTION +----------- +Given one existing commit, revert the change the patch introduces, and record a +new commit that records it. This requires your working tree to be clean (no +modifications from the HEAD commit). + +OPTIONS +------- +<commit>:: + Commit to revert. + For a more complete list of ways to spell commit names, see + "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1]. + +-e|--edit:: + With this option, `git-revert` will let you edit the commit + message prior committing the revert. This is the default if + you run the command from a terminal. + +--no-edit:: + With this option, `git-revert` will not start the commit + message editor. + +-n|--no-commit:: + Usually the command automatically creates a commit with + a commit log message stating which commit was reverted. + This flag applies the change necessary to revert the + named commit to your working tree, but does not make the + commit. In addition, when this option is used, your + working tree does not have to match the HEAD commit. + The revert is done against the beginning state of your + working tree. ++ +This is useful when reverting more than one commits' +effect to your working tree in a row. + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt new file mode 100644 index 0000000000..6feebc0400 --- /dev/null +++ b/Documentation/git-rm.txt @@ -0,0 +1,91 @@ +git-rm(1) +========= + +NAME +---- +git-rm - Remove files from the working tree and from the index + +SYNOPSIS +-------- +'git-rm' [-f] [-n] [-r] [--cached] [--] <file>... + +DESCRIPTION +----------- +Remove files from the working tree and from the index. The +files have to be identical to the tip of the branch, and no +updates to its contents must have been placed in the staging +area (aka index). + + +OPTIONS +------- +<file>...:: + Files to remove. Fileglobs (e.g. `*.c`) can be given to + remove all matching files. Also a leading directory name + (e.g. `dir` to add `dir/file1` and `dir/file2`) can be + given to remove all files in the directory, recursively, + but this requires `-r` option to be given for safety. + +-f:: + Override the up-to-date check. + +-n:: + Don't actually remove the file(s), just show if they exist in + the index. + +-r:: + Allow recursive removal when a leading directory name is + given. + +\--:: + This option can be used to separate command-line options from + the list of files, (useful when filenames might be mistaken + for command-line options). + +\--cached:: + This option can be used to tell the command to remove + the paths only from the index, leaving working tree + files. + + +DISCUSSION +---------- + +The list of <file> given to the command can be exact pathnames, +file glob patterns, or leading directory name. The command +removes only the paths that is known to git. Giving the name of +a file that you have not told git about does not remove that file. + + +EXAMPLES +-------- +git-rm Documentation/\\*.txt:: + Removes all `\*.txt` files from the index that are under the + `Documentation` directory and any of its subdirectories. ++ +Note that the asterisk `\*` is quoted from the shell in this +example; this lets the command include the files from +subdirectories of `Documentation/` directory. + +git-rm -f git-*.sh:: + Remove all git-*.sh scripts that are in the index. + Because this example lets the shell expand the asterisk + (i.e. you are listing the files explicitly), it + does not remove `subdir/git-foo.sh`. + +See Also +-------- +gitlink:git-add[1] + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-runstatus.txt b/Documentation/git-runstatus.txt new file mode 100644 index 0000000000..89d7b92731 --- /dev/null +++ b/Documentation/git-runstatus.txt @@ -0,0 +1,69 @@ +git-runstatus(1) +================ + +NAME +---- +git-runstatus - A helper for git-status and git-commit + + +SYNOPSIS +-------- +'git-runstatus' [--color|--nocolor] [--amend] [--verbose] [--untracked] + + +DESCRIPTION +----------- +Examines paths in the working tree that has changes unrecorded +to the index file, and changes between the index file and the +current HEAD commit. The former paths are what you _could_ +commit by running 'git-update-index' before running 'git +commit', and the latter paths are what you _would_ commit by +running 'git commit'. + +If there is no path that is different between the index file and +the current HEAD commit, the command exits with non-zero status. + +Note that this is _not_ the user level command you would want to +run from the command line. Use 'git-status' instead. + + +OPTIONS +------- +--color:: + Show colored status, highlighting modified file names. + +--nocolor:: + Turn off coloring. + +--amend:: + Show status based on HEAD^1, not HEAD, i.e. show what + 'git-commit --amend' would do. + +--verbose:: + Show unified diff of all file changes. + +--untracked:: + Show files in untracked directories, too. Without this + option only its name and a trailing slash are displayed + for each untracked directory. + + +OUTPUT +------ +The output from this command is designed to be used as a commit +template comments, and all the output lines are prefixed with '#'. + + +Author +------ +Originally written by Linus Torvalds <torvalds@osdl.org> as part +of git-commit, and later rewritten in C by Jeff King. + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-send-email.txt b/Documentation/git-send-email.txt new file mode 100644 index 0000000000..4c8d907bd5 --- /dev/null +++ b/Documentation/git-send-email.txt @@ -0,0 +1,108 @@ +git-send-email(1) +================= + +NAME +---- +git-send-email - Send a collection of patches as emails + + +SYNOPSIS +-------- +'git-send-email' [options] <file|directory> [... file|directory] + + + +DESCRIPTION +----------- +Takes the patches given on the command line and emails them out. + +The header of the email is configurable by command line options. If not +specified on the command line, the user will be prompted with a ReadLine +enabled interface to provide the necessary information. + +OPTIONS +------- +The options available are: + +--bcc:: + Specify a "Bcc:" value for each email. + + The --bcc option must be repeated for each user you want on the bcc list. + +--cc:: + Specify a starting "Cc:" value for each email. + + The --cc option must be repeated for each user you want on the cc list. + +--chain-reply-to, --no-chain-reply-to:: + If this is set, each email will be sent as a reply to the previous + email sent. If disabled with "--no-chain-reply-to", all emails after + the first will be sent as replies to the first email sent. When using + this, it is recommended that the first file given be an overview of the + entire patch series. + Default is --chain-reply-to + +--compose:: + Use $EDITOR to edit an introductory message for the + patch series. + +--from:: + Specify the sender of the emails. This will default to + the value GIT_COMMITTER_IDENT, as returned by "git-var -l". + The user will still be prompted to confirm this entry. + +--in-reply-to:: + Specify the contents of the first In-Reply-To header. + Subsequent emails will refer to the previous email + instead of this if --chain-reply-to is set (the default) + Only necessary if --compose is also set. If --compose + is not set, this will be prompted for. + +--no-signed-off-by-cc:: + 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 + all that is output. + +--smtp-server:: + If set, specifies the outgoing SMTP server to use. A full + pathname of a sendmail-like program can be specified instead; + the program must support the `-i` option. Default value can + be specified by the 'sendemail.smtpserver' configuration + option; the built-in default is `/usr/sbin/sendmail` or + `/usr/lib/sendmail` if such program is available, or + `localhost` otherwise. + +--subject:: + Specify the initial subject of the email thread. + Only necessary if --compose is also set. If --compose + is not set, this will be prompted for. + +--suppress-from:: + Do not add the From: address to the cc: list, if it shows up in a From: + line. + +--to:: + Specify the primary recipient of the emails generated. + Generally, this will be the upstream maintainer of the + project involved. + + The --to option must be repeated for each user you want on the to list. + + +Author +------ +Written by Ryan Anderson <ryan@michonline.com> + +git-send-email is originally based upon +send_lots_of_email.pl by Greg Kroah-Hartman. + +Documentation +-------------- +Documentation by Ryan Anderson + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-send-pack.txt b/Documentation/git-send-pack.txt new file mode 100644 index 0000000000..205bfd2d25 --- /dev/null +++ b/Documentation/git-send-pack.txt @@ -0,0 +1,123 @@ +git-send-pack(1) +================ + +NAME +---- +git-send-pack - Push objects over git protocol to another repository + + +SYNOPSIS +-------- +'git-send-pack' [--all] [--force] [--receive-pack=<git-receive-pack>] [--verbose] [--thin] [<host>:]<directory> [<ref>...] + +DESCRIPTION +----------- +Usually you would want to use gitlink:git-push[1] which is a +higher level wrapper of this command instead. + +Invokes 'git-receive-pack' on a possibly remote repository, and +updates it from the current repository, sending named refs. + + +OPTIONS +------- +\--receive-pack=<git-receive-pack>:: + Path to the 'git-receive-pack' program on the remote + end. Sometimes useful when pushing to a remote + repository over ssh, and you do not have the program in + a directory on the default $PATH. + +\--exec=<git-receive-pack>:: + Same as \--receive-pack=<git-receive-pack>. + +\--all:: + Instead of explicitly specifying which refs to update, + update all refs that locally exist. + +\--force:: + Usually, the command refuses to update a remote ref that + is not an ancestor of the local ref used to overwrite it. + This flag disables the check. What this means is that + the remote repository can lose commits; use it with + care. + +\--verbose:: + Run verbosely. + +\--thin:: + Spend extra cycles to minimize the number of objects to be sent. + Use it on slower connection. + +<host>:: + A remote host to house the repository. When this + part is specified, 'git-receive-pack' is invoked via + ssh. + +<directory>:: + The repository to update. + +<ref>...:: + The remote refs to update. + + +Specifying the Refs +------------------- + +There are three ways to specify which refs to update on the +remote end. + +With '--all' flag, all refs that exist locally are transferred to +the remote side. You cannot specify any '<ref>' if you use +this flag. + +Without '--all' and without any '<ref>', the refs that exist +both on the local side and on the remote side are updated. + +When one or more '<ref>' are specified explicitly, it can be either a +single pattern, or a pair of such pattern separated by a colon +":" (this means that a ref name cannot have a colon in it). A +single pattern '<name>' is just a shorthand for '<name>:<name>'. + +Each pattern pair consists of the source side (before the colon) +and the destination side (after the colon). The ref to be +pushed is determined by finding a match that matches the source +side, and where it is pushed is determined by using the +destination side. + + - It is an error if <src> does not match exactly one of the + local refs. + + - It is an error if <dst> matches more than one remote refs. + + - If <dst> does not match any remote ref, either + + * it has to start with "refs/"; <dst> is used as the + destination literally in this case. + + * <src> == <dst> and the ref that matched the <src> must not + exist in the set of remote refs; the ref matched <src> + locally is used as the name of the destination. + +Without '--force', the <src> ref is stored at the remote only if +<dst> does not exist, or <dst> is a proper subset (i.e. an +ancestor) of <src>. This check, known as "fast forward check", +is performed in order to avoid accidentally overwriting the +remote ref and lose other peoples' commits from there. + +With '--force', the fast forward check is disabled for all refs. + +Optionally, a <ref> parameter can be prefixed with a plus '+' sign +to disable the fast-forward check only on that ref. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-sh-setup.txt b/Documentation/git-sh-setup.txt new file mode 100644 index 0000000000..2b2abebd60 --- /dev/null +++ b/Documentation/git-sh-setup.txt @@ -0,0 +1,72 @@ +git-sh-setup(1) +=============== + +NAME +---- +git-sh-setup - Common git shell script setup code + +SYNOPSIS +-------- +'git-sh-setup' + +DESCRIPTION +----------- + +This is not a command the end user would want to run. Ever. +This documentation is meant for people who are studying the +Porcelain-ish scripts and/or are writing new ones. + +The `git-sh-setup` scriptlet is designed to be sourced (using +`.`) by other shell scripts to set up some variables pointing at +the normal git directories and a few helper shell functions. + +Before sourcing it, your script should set up a few variables; +`USAGE` (and `LONG_USAGE`, if any) is used to define message +given by `usage()` shell function. `SUBDIRECTORY_OK` can be set +if the script can run from a subdirectory of the working tree +(some commands do not). + +The scriptlet sets `GIT_DIR` and `GIT_OBJECT_DIRECTORY` shell +variables, but does *not* export them to the environment. + +FUNCTIONS +--------- + +die:: + exit after emitting the supplied error message to the + standard error stream. + +usage:: + die with the usage message. + +set_reflog_action:: + set the message that will be recorded to describe the + end-user action in the reflog, when the script updates a + ref. + +is_bare_repository:: + outputs `true` or `false` to the standard output stream + to indicate if the repository is a bare repository + (i.e. without an associated working tree). + +cd_to_toplevel:: + runs chdir to the toplevel of the working tree. + +require_work_tree:: + checks if the repository is a bare repository, and dies + if so. Used by scripts that require working tree + (e.g. `checkout`). + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-shell.txt b/Documentation/git-shell.txt new file mode 100644 index 0000000000..228b9f14f3 --- /dev/null +++ b/Documentation/git-shell.txt @@ -0,0 +1,35 @@ +git-shell(1) +============ + +NAME +---- +git-shell - Restricted login shell for GIT-only SSH access + + +SYNOPSIS +-------- +'git-shell' -c <command> <argument> + +DESCRIPTION +----------- +This is meant to be used as a login shell for SSH accounts you want +to restrict to GIT pull/push access only. It permits execution only +of server-side GIT commands implementing the pull/push functionality. +The commands can be executed only by the '-c' option; the shell is not +interactive. + +Currently, only the `git-receive-pack` and `git-upload-pack` commands +are permitted to be called, with a single required argument. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Petr Baudis and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-shortlog.txt b/Documentation/git-shortlog.txt new file mode 100644 index 0000000000..b0df92e819 --- /dev/null +++ b/Documentation/git-shortlog.txt @@ -0,0 +1,57 @@ +git-shortlog(1) +=============== + +NAME +---- +git-shortlog - Summarize 'git log' output + +SYNOPSIS +-------- +git-log --pretty=short | 'git-shortlog' [-h] [-n] [-s] +git-shortlog [-n|--number] [-s|--summary] [<committish>...] + +DESCRIPTION +----------- +Summarizes 'git log' output in a format suitable for inclusion +in release announcements. Each commit will be grouped by author and +the first line of the commit message will be shown. + +Additionally, "[PATCH]" will be stripped from the commit description. + +OPTIONS +------- + +-h:: + Print a short usage message and exit. + +-n:: + Sort output according to the number of commits per author instead + of author alphabetic order. + +-s:: + Suppress commit description and provide a commit count summary only. + +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 +------ +Written by Jeff Garzik <jgarzik@pobox.com> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-show-branch.txt b/Documentation/git-show-branch.txt new file mode 100644 index 0000000000..ba5313d51f --- /dev/null +++ b/Documentation/git-show-branch.txt @@ -0,0 +1,193 @@ +git-show-branch(1) +================== + +NAME +---- +git-show-branch - Show branches and their commits + +SYNOPSIS +-------- +[verse] +'git-show-branch' [--all] [--remotes] [--topo-order] [--current] + [--more=<n> | --list | --independent | --merge-base] + [--no-name | --sha1-name] [--topics] [<rev> | <glob>]... +'git-show-branch' (-g|--reflog)[=<n>[,<base>]] [--list] [<ref>] + +DESCRIPTION +----------- + +Shows the commit ancestry graph starting from the commits named +with <rev>s or <globs>s (or all refs under $GIT_DIR/refs/heads +and/or $GIT_DIR/refs/tags) semi-visually. + +It cannot show more than 29 branches and commits at a time. + +It uses `showbranch.default` multi-valued configuration items if +no <rev> nor <glob> is given on the command line. + + +OPTIONS +------- +<rev>:: + Arbitrary extended SHA1 expression (see `git-rev-parse`) + that typically names a branch HEAD or a tag. + +<glob>:: + A glob pattern that matches branch or tag names under + $GIT_DIR/refs. For example, if you have many topic + branches under $GIT_DIR/refs/heads/topic, giving + `topic/*` would show all of them. + +-r|--remotes:: + Show the remote-tracking branches. + +-a|--all:: + Show both remote-tracking branches and local branches. + +--current:: + With this option, the command includes the current + branch to the list of revs to be shown when it is not + given on the command line. + +--topo-order:: + By default, the branches and their commits are shown in + reverse chronological order. This option makes them + appear in topological order (i.e., descendant commits + are shown before their parents). + +--sparse:: + By default, the output omits merges that are reachable + from only one tip being shown. This option makes them + visible. + +--more=<n>:: + Usually the command stops output upon showing the commit + that is the common ancestor of all the branches. This + flag tells the command to go <n> more common commits + beyond that. When <n> is negative, display only the + <reference>s given, without showing the commit ancestry + tree. + +--list:: + Synonym to `--more=-1` + +--merge-base:: + Instead of showing the commit list, just act like the + 'git-merge-base -a' command, except that it can accept + more than two heads. + +--independent:: + Among the <reference>s given, display only the ones that + cannot be reached from any other <reference>. + +--no-name:: + Do not show naming strings for each commit. + +--sha1-name:: + Instead of naming the commits using the path to reach + them from heads (e.g. "master~2" to mean the grandparent + of "master"), name them with the unique prefix of their + object names. + +--topics:: + Shows only commits that are NOT on the first branch given. + This helps track topic branches by hiding any commit that + is already in the main line of development. When given + "git show-branch --topics master topic1 topic2", this + will show the revisions given by "git rev-list {caret}master + topic1 topic2" + +--reflog[=<n>[,<base>]] [<ref>]:: + Shows <n> most recent ref-log entries for the given + ref. If <base> is given, <n> entries going back from + that entry. <base> can be specified as count or date. + `-g` can be used as a short-hand for this option. When + no explicit <ref> parameter is given, it defaults to the + current branch (or `HEAD` if it is detached). + +Note that --more, --list, --independent and --merge-base options +are mutually exclusive. + + +OUTPUT +------ +Given N <references>, the first N lines are the one-line +description from their commit message. The branch head that is +pointed at by $GIT_DIR/HEAD is prefixed with an asterisk `*` +character while other heads are prefixed with a `!` character. + +Following these N lines, one-line log for each commit is +displayed, indented N places. If a commit is on the I-th +branch, the I-th indentation character shows a `+` sign; +otherwise it shows a space. Merge commits are denoted by +a `-` sign. Each commit shows a short name that +can be used as an extended SHA1 to name that commit. + +The following example shows three branches, "master", "fixes" +and "mhf": + +------------------------------------------------ +$ git show-branch master fixes mhf +* [master] Add 'git show-branch'. + ! [fixes] Introduce "reset type" flag to "git reset" + ! [mhf] Allow "+remote:local" refspec to cause --force when fetching. +--- + + [mhf] Allow "+remote:local" refspec to cause --force when fetching. + + [mhf~1] Use git-octopus when pulling more than one heads. + + [fixes] Introduce "reset type" flag to "git reset" + + [mhf~2] "git fetch --force". + + [mhf~3] Use .git/remote/origin, not .git/branches/origin. + + [mhf~4] Make "git pull" and "git fetch" default to origin + + [mhf~5] Infamous 'octopus merge' + + [mhf~6] Retire git-parse-remote. + + [mhf~7] Multi-head fetch. + + [mhf~8] Start adding the $GIT_DIR/remotes/ support. +*++ [master] Add 'git show-branch'. +------------------------------------------------ + +These three branches all forked from a common commit, [master], +whose commit message is "Add 'git show-branch'. "fixes" branch +adds one commit 'Introduce "reset type"'. "mhf" branch has many +other commits. The current branch is "master". + + +EXAMPLE +------- + +If you keep your primary branches immediately under +`$GIT_DIR/refs/heads`, and topic branches in subdirectories of +it, having the following in the configuration file may help: + +------------ +[showbranch] + default = --topo-order + default = heads/* + +------------ + +With this, `git show-branch` without extra parameters would show +only the primary branches. In addition, if you happen to be on +your topic branch, it is shown as well. + +------------ +$ git show-branch --reflog='10,1 hour ago' --list master +------------ + +shows 10 reflog entries going back from the tip as of 1 hour ago. +Without `--list`, the output also shows how these tips are +topologically related with each other. + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + + +Documentation +-------------- +Documentation by Junio C Hamano. + + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-show-index.txt b/Documentation/git-show-index.txt new file mode 100644 index 0000000000..be09b62beb --- /dev/null +++ b/Documentation/git-show-index.txt @@ -0,0 +1,35 @@ +git-show-index(1) +================= + +NAME +---- +git-show-index - Show packed archive index + + +SYNOPSIS +-------- +'git-show-index' < idx-file + + +DESCRIPTION +----------- +Reads given idx file for packed git archive created with +git-pack-objects command, and dumps its contents. + +The information it outputs is subset of what you can get from +'git-verify-pack -v'; this command only shows the packfile +offset and SHA1 of each object. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-show-ref.txt b/Documentation/git-show-ref.txt new file mode 100644 index 0000000000..5973a82517 --- /dev/null +++ b/Documentation/git-show-ref.txt @@ -0,0 +1,156 @@ +git-show-ref(1) +=============== + +NAME +---- +git-show-ref - List references in a local repository + +SYNOPSIS +-------- +[verse] +'git-show-ref' [-q|--quiet] [--verify] [-h|--head] [-d|--dereference] + [-s|--hash] [--abbrev] [--tags] [--heads] [--] <pattern>... + +DESCRIPTION +----------- + +Displays references available in a local repository along with the associated +commit IDs. Results can be filtered using a pattern and tags can be +dereferenced into object IDs. Additionally, it can be used to test whether a +particular ref exists. + +Use of this utility is encouraged in favor of directly accessing files under +in the `.git` directory. + +OPTIONS +------- + +-h, --head:: + + Show the HEAD reference. + +--tags, --heads:: + + Limit to only "refs/heads" and "refs/tags", respectively. These + options are not mutually exclusive; when given both, references stored + in "refs/heads" and "refs/tags" are displayed. + +-d, --dereference:: + + Dereference tags into object IDs as well. They will be shown with "^{}" + appended. + +-s, --hash:: + + Only show the SHA1 hash, not the reference name. When also using + --dereference the dereferenced tag will still be shown after the SHA1. + +--verify:: + + Enable stricter reference checking by requiring an exact ref path. + Aside from returning an error code of 1, it will also print an error + message if '--quiet' was not specified. + +--abbrev, --abbrev=len:: + + Abbreviate the object name. When using `--hash`, you do + not have to say `--hash --abbrev`; `--hash=len` would do. + +-q, --quiet:: + + Do not print any results to stdout. When combined with '--verify' this + can be used to silently check if a reference exists. + +<pattern>:: + + Show references matching one or more patterns. + +OUTPUT +------ + +The output is in the format: '<SHA-1 ID>' '<space>' '<reference name>'. + +----------------------------------------------------------------------------- +$ git show-ref --head --dereference +832e76a9899f560a90ffd62ae2ce83bbeff58f54 HEAD +832e76a9899f560a90ffd62ae2ce83bbeff58f54 refs/heads/master +832e76a9899f560a90ffd62ae2ce83bbeff58f54 refs/heads/origin +3521017556c5de4159da4615a39fa4d5d2c279b5 refs/tags/v0.99.9c +6ddc0964034342519a87fe013781abf31c6db6ad refs/tags/v0.99.9c^{} +055e4ae3ae6eb344cbabf2a5256a49ea66040131 refs/tags/v1.0rc4 +423325a2d24638ddcc82ce47be5e40be550f4507 refs/tags/v1.0rc4^{} +... +----------------------------------------------------------------------------- + +When using --hash (and not --dereference) the output format is: '<SHA-1 ID>' + +----------------------------------------------------------------------------- +$ git show-ref --heads --hash +2e3ba0114a1f52b47df29743d6915d056be13278 +185008ae97960c8d551adcd9e23565194651b5d1 +03adf42c988195b50e1a1935ba5fcbc39b2b029b +... +----------------------------------------------------------------------------- + +EXAMPLE +------- + +To show all references called "master", whether tags or heads or anything +else, and regardless of how deep in the reference naming hierarchy they are, +use: + +----------------------------------------------------------------------------- + git show-ref master +----------------------------------------------------------------------------- + +This will show "refs/heads/master" but also "refs/remote/other-repo/master", +if such references exists. + +When using the '--verify' flag, the command requires an exact path: + +----------------------------------------------------------------------------- + git show-ref --verify refs/heads/master +----------------------------------------------------------------------------- + +will only match the exact branch called "master". + +If nothing matches, gitlink:git-show-ref[1] will return an error code of 1, +and in the case of verification, it will show an error message. + +For scripting, you can ask it to be quiet with the "--quiet" flag, which +allows you to do things like + +----------------------------------------------------------------------------- + git-show-ref --quiet --verify -- "refs/heads/$headname" || + echo "$headname is not a valid branch" +----------------------------------------------------------------------------- + +to check whether a particular branch exists or not (notice how we don't +actually want to show any results, and we want to use the full refname for it +in order to not trigger the problem with ambiguous partial matches). + +To show only tags, or only proper branch heads, use "--tags" and/or "--heads" +respectively (using both means that it shows tags and heads, but not other +random references under the refs/ subdirectory). + +To do automatic tag object dereferencing, use the "-d" or "--dereference" +flag, so you can do + +----------------------------------------------------------------------------- + git show-ref --tags --dereference +----------------------------------------------------------------------------- + +to get a listing of all tags together with what they dereference. + +SEE ALSO +-------- +gitlink:git-ls-remote[1], gitlink:git-peek-remote[1] + +AUTHORS +------- +Written by Linus Torvalds <torvalds@osdl.org>. +Man page by Jonas Fonseca <fonseca@diku.dk>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-show.txt b/Documentation/git-show.txt new file mode 100644 index 0000000000..f56f164983 --- /dev/null +++ b/Documentation/git-show.txt @@ -0,0 +1,84 @@ +git-show(1) +=========== + +NAME +---- +git-show - Show various types of objects + + +SYNOPSIS +-------- +'git-show' [options] <object>... + +DESCRIPTION +----------- +Shows one or more objects (blobs, trees, tags and commits). + +For commits it shows the log message and textual diff. It also +presents the merge commit in a special format as produced by +'git-diff-tree --cc'. + +For tags, it shows the tag message and the referenced objects. + +For trees, it shows the names (equivalent to gitlink:git-ls-tree[1] +with \--name-only). + +For plain blobs, it shows the plain contents. + +The command takes options applicable to the gitlink:git-diff-tree[1] command to +control how the changes the commit introduces are shown. + +This manual page describes only the most frequently used options. + + +OPTIONS +------- +<object>:: + The name of the object to show. + For a more complete list of ways to spell object names, see + "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1]. + +include::pretty-formats.txt[] + + +EXAMPLES +-------- + +git show v1.0.0:: + Shows the tag `v1.0.0`, along with the object the tags + points at. + +git show v1.0.0^{tree}:: + Shows the tree pointed to by the tag `v1.0.0`. + +git show next~10:Documentation/README + Shows the contents of the file `Documentation/README` as + they were current in the 10th last commit of the branch + `next`. + +git show master:Makefile master:t/Makefile + Concatenates the contents of said Makefiles in the head + of the branch `master`. + +Discussion +---------- + +include::i18n.txt[] + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and +Junio C Hamano <junkio@cox.net>. Significantly enhanced by +Johannes Schindelin <Johannes.Schindelin@gmx.de>. + + +Documentation +------------- +Documentation by David Greaves, Petr Baudis 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 + diff --git a/Documentation/git-ssh-fetch.txt b/Documentation/git-ssh-fetch.txt new file mode 100644 index 0000000000..192b1f15a9 --- /dev/null +++ b/Documentation/git-ssh-fetch.txt @@ -0,0 +1,51 @@ +git-ssh-fetch(1) +================ + +NAME +---- +git-ssh-fetch - Fetch from a remote repository over ssh connection + + + +SYNOPSIS +-------- +'git-ssh-fetch' [-c] [-t] [-a] [-d] [-v] [-w filename] [--recover] commit-id url + +DESCRIPTION +----------- +Pulls from a remote repository over ssh connection, invoking +git-ssh-upload on the other end. It functions identically to +git-ssh-upload, aside from which end you run it on. + + +OPTIONS +------- +commit-id:: + Either the hash or the filename under [URL]/refs/ to + pull. + +-c:: + Get the commit objects. +-t:: + Get trees associated with the commit objects. +-a:: + Get all the objects. +-v:: + Report what is downloaded. +-w:: + Writes the commit-id into the filename under $GIT_DIR/refs/ on + the local end after the transfer is complete. + + +Author +------ +Written by Daniel Barkalow <barkalow@iabervon.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-ssh-upload.txt b/Documentation/git-ssh-upload.txt new file mode 100644 index 0000000000..a9b7e9f974 --- /dev/null +++ b/Documentation/git-ssh-upload.txt @@ -0,0 +1,47 @@ +git-ssh-upload(1) +================= + +NAME +---- +git-ssh-upload - Push to a remote repository over ssh connection + + +SYNOPSIS +-------- +'git-ssh-upload' [-c] [-t] [-a] [-d] [-v] [-w filename] [--recover] commit-id url + +DESCRIPTION +----------- +Pushes from a remote repository over ssh connection, invoking +git-ssh-fetch on the other end. It functions identically to +git-ssh-fetch, aside from which end you run it on. + +OPTIONS +------- +commit-id:: + Id of commit to push. + +-c:: + Get the commit objects. +-t:: + Get tree associated with the requested commit object. +-a:: + Get all the objects. +-v:: + Report what is uploaded. +-w:: + Writes the commit-id into the filename under [URL]/refs/ on + the remote end after the transfer is complete. + +Author +------ +Written by Daniel Barkalow <barkalow@iabervon.org> + +Documentation +-------------- +Documentation by Daniel Barkalow + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-status.txt b/Documentation/git-status.txt new file mode 100644 index 0000000000..03871e5d73 --- /dev/null +++ b/Documentation/git-status.txt @@ -0,0 +1,58 @@ +git-status(1) +============= + +NAME +---- +git-status - Show the working tree status + + +SYNOPSIS +-------- +'git-status' <options>... + +DESCRIPTION +----------- +Examines paths in the working tree that has changes unrecorded +to the index file, and changes between the index file and the +current HEAD commit. The former paths are what you _could_ +commit by running 'git-update-index' before running 'git +commit', and the latter paths are what you _would_ commit by +running 'git commit'. + +If there is no path that is different between the index file and +the current HEAD commit, the command exits with non-zero +status. + +The command takes the same set of options as `git-commit`; it +shows what would be committed if the same options are given to +`git-commit`. + + +OUTPUT +------ +The output from this command is designed to be used as a commit +template comments, and all the output lines are prefixed with '#'. + + +CONFIGURATION +------------- + +The command honors `color.status` (or `status.color` -- they +mean the same thing and the latter is kept for backward +compatibility) and `color.status.<slot>` configuration variables +to colorize its output. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and +Junio C Hamano <junkio@cox.net>. + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-stripspace.txt b/Documentation/git-stripspace.txt new file mode 100644 index 0000000000..3a03dd0410 --- /dev/null +++ b/Documentation/git-stripspace.txt @@ -0,0 +1,33 @@ +git-stripspace(1) +================= + +NAME +---- +git-stripspace - Filter out empty lines + + +SYNOPSIS +-------- +'git-stripspace' < <stream> + +DESCRIPTION +----------- +Remove multiple empty lines, and empty lines at beginning and end. + +OPTIONS +------- +<stream>:: + Byte stream to act on. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt new file mode 100644 index 0000000000..6ce6a3944d --- /dev/null +++ b/Documentation/git-svn.txt @@ -0,0 +1,501 @@ +git-svn(1) +========== + +NAME +---- +git-svn - Bidirectional operation between a single Subversion branch and git + +SYNOPSIS +-------- +'git-svn' <command> [options] [arguments] + +DESCRIPTION +----------- +git-svn is a simple conduit for changesets between Subversion and git. +It is not to be confused with gitlink:git-svnimport[1], which is +read-only and geared towards tracking multiple branches. + +git-svn was originally designed for an individual developer who wants a +bidirectional flow of changesets between a single branch in Subversion +and an arbitrary number of branches in git. Since its inception, +git-svn has gained the ability to track multiple branches in a manner +similar to git-svnimport; but it cannot (yet) automatically detect new +branches and tags like git-svnimport does. + +git-svn is especially useful when it comes to tracking repositories +not organized in the way Subversion developers recommend (trunk, +branches, tags directories). + +COMMANDS +-------- +-- + +'init':: + Creates an empty git repository with additional metadata + directories for git-svn. The Subversion URL must be specified + as a command-line argument. Optionally, the target directory + to operate on can be specified as a second argument. Normally + this command initializes the current directory. + +'fetch':: + +Fetch unfetched revisions from the Subversion URL we are +tracking. refs/remotes/git-svn will be updated to the +latest revision. + +Note: You should never attempt to modify the remotes/git-svn +branch outside of git-svn. Instead, create a branch from +remotes/git-svn and work on that branch. Use the 'dcommit' +command (see below) to write git commits back to +remotes/git-svn. + +See '<<fetch-args,Additional Fetch Arguments>>' if you are interested in +manually joining branches on commit. + +'dcommit':: + Commit each diff from a specified head directly to the SVN + repository, and then rebase or reset (depending on whether or + not there is a diff between SVN and head). This will create + a revision in SVN for each commit in git. + It is recommended that you run git-svn fetch and rebase (not + pull or merge) your commits against the latest changes in the + SVN repository. + An optional command-line argument may be specified as an + alternative to HEAD. + This is advantageous over 'set-tree' (below) because it produces + cleaner, more linear history. + +'log':: + This should make it easy to look up svn log messages when svn + users refer to -r/--revision numbers. + + The following features from `svn log' are supported: + + --revision=<n>[:<n>] - is supported, non-numeric args are not: + HEAD, NEXT, BASE, PREV, etc ... + -v/--verbose - it's not completely compatible with + the --verbose output in svn log, but + reasonably close. + --limit=<n> - is NOT the same as --max-count, + doesn't count merged/excluded commits + --incremental - supported + + New features: + + --show-commit - shows the git commit sha1, as well + --oneline - our version of --pretty=oneline + + Any other arguments are passed directly to `git log' + +'set-tree':: + You should consider using 'dcommit' instead of this command. + Commit specified commit or tree objects to SVN. This relies on + your imported fetch data being up-to-date. This makes + absolutely no attempts to do patching when committing to SVN, it + simply overwrites files with those specified in the tree or + commit. All merging is assumed to have taken place + independently of git-svn functions. + +'rebuild':: + Not a part of daily usage, but this is a useful command if + you've just cloned a repository (using gitlink:git-clone[1]) that was + tracked with git-svn. Unfortunately, git-clone does not clone + git-svn metadata and the svn working tree that git-svn uses for + its operations. This rebuilds the metadata so git-svn can + resume fetch operations. A Subversion URL may be optionally + specified at the command-line if the directory/repository you're + tracking has moved or changed protocols. + +'show-ignore':: + Recursively finds and lists the svn:ignore property on + directories. The output is suitable for appending to + the $GIT_DIR/info/exclude file. + +'commit-diff':: + Commits the diff of two tree-ish arguments from the + command-line. This command is intended for interoperability with + git-svnimport and does not rely on being inside an git-svn + init-ed repository. This command takes three arguments, (a) the + original tree to diff against, (b) the new tree result, (c) the + URL of the target Subversion repository. The final argument + (URL) may be omitted if you are working from a git-svn-aware + repository (that has been init-ed with git-svn). + The -r<revision> option is required for this. + +'graft-branches':: + This command attempts to detect merges/branches from already + imported history. Techniques used currently include regexes, + file copies, and tree-matches). This command generates (or + modifies) the $GIT_DIR/info/grafts file. This command is + considered experimental, and inherently flawed because + merge-tracking in SVN is inherently flawed and inconsistent + across different repositories. + +'multi-init':: + This command supports git-svnimport-like command-line syntax for + importing repositories that are laid out as recommended by the + SVN folks. This is a bit more tolerant than the git-svnimport + command-line syntax and doesn't require the user to figure out + where the repository URL ends and where the repository path + begins. + +-T<trunk_subdir>:: +--trunk=<trunk_subdir>:: +-t<tags_subdir>:: +--tags=<tags_subdir>:: +-b<branches_subdir>:: +--branches=<branches_subdir>:: + These are the command-line options for multi-init. Each of + these flags can point to a relative repository path + (--tags=project/tags') or a full url + (--tags=https://foo.org/project/tags) + +--prefix=<prefix> + This allows one to specify a prefix which is prepended to the + names of remotes. The prefix does not automatically include a + trailing slash, so be sure you include one in the argument if + that is what you want. This is useful if you wish to track + multiple projects that share a common repository. + +'multi-fetch':: + This runs fetch on all known SVN branches we're tracking. This + will NOT discover new branches (unlike git-svnimport), so + multi-init will need to be re-run (it's idempotent). + +-- + +OPTIONS +------- +-- + +--shared:: +--template=<template_directory>:: + Only used with the 'init' command. + These are passed directly to gitlink:git-init[1]. + +-r <ARG>:: +--revision <ARG>:: + +Only used with the 'fetch' command. + +Takes any valid -r<argument> svn would accept and passes it +directly to svn. -r<ARG1>:<ARG2> ranges and "{" DATE "}" syntax +is also supported. This is passed directly to svn, see svn +documentation for more details. + +This can allow you to make partial mirrors when running fetch. + +-:: +--stdin:: + +Only used with the 'set-tree' command. + +Read a list of commits from stdin and commit them in reverse +order. Only the leading sha1 is read from each line, so +git-rev-list --pretty=oneline output can be used. + +--rmdir:: + +Only used with the 'dcommit', 'set-tree' and 'commit-diff' commands. + +Remove directories from the SVN tree if there are no files left +behind. SVN can version empty directories, and they are not +removed by default if there are no files left in them. git +cannot version empty directories. Enabling this flag will make +the commit to SVN act like git. + +config key: svn.rmdir + +-e:: +--edit:: + +Only used with the 'dcommit', 'set-tree' and 'commit-diff' commands. + +Edit the commit message before committing to SVN. This is off by +default for objects that are commits, and forced on when committing +tree objects. + +config key: svn.edit + +-l<num>:: +--find-copies-harder:: + +Only used with the 'dcommit', 'set-tree' and 'commit-diff' commands. + +They are both passed directly to git-diff-tree see +gitlink:git-diff-tree[1] for more information. + +[verse] +config key: svn.l +config key: svn.findcopiesharder + +-A<filename>:: +--authors-file=<filename>:: + +Syntax is compatible with the files used by git-svnimport and +git-cvsimport: + +------------------------------------------------------------------------ + loginname = Joe User <user@example.com> +------------------------------------------------------------------------ + +If this option is specified and git-svn encounters an SVN +committer name that does not exist in the authors-file, git-svn +will abort operation. The user will then have to add the +appropriate entry. Re-running the previous git-svn command +after the authors-file is modified should continue operation. + +config key: svn.authorsfile + +-q:: +--quiet:: + Make git-svn less verbose. + +--repack[=<n>]:: +--repack-flags=<flags> + These should help keep disk usage sane for large fetches + with many revisions. + + --repack takes an optional argument for the number of revisions + to fetch before repacking. This defaults to repacking every + 1000 commits fetched if no argument is specified. + + --repack-flags are passed directly to gitlink:git-repack[1]. + +config key: svn.repack +config key: svn.repackflags + +-m:: +--merge:: +-s<strategy>:: +--strategy=<strategy>:: + +These are only used with the 'dcommit' command. + +Passed directly to git-rebase when using 'dcommit' if a +'git-reset' cannot be used (see dcommit). + +-n:: +--dry-run:: + +This is only used with the 'dcommit' command. + +Print out the series of git arguments that would show +which diffs would be committed to SVN. + +-- + +ADVANCED OPTIONS +---------------- +-- + +-b<refname>:: +--branch <refname>:: +Used with 'fetch', 'dcommit' or 'set-tree'. + +This can be used to join arbitrary git branches to remotes/git-svn +on new commits where the tree object is equivalent. + +When used with different GIT_SVN_ID values, tags and branches in +SVN can be tracked this way, as can some merges where the heads +end up having completely equivalent content. This can even be +used to track branches across multiple SVN _repositories_. + +This option may be specified multiple times, once for each +branch. + +config key: svn.branch + +-i<GIT_SVN_ID>:: +--id <GIT_SVN_ID>:: + +This sets GIT_SVN_ID (instead of using the environment). See the +section on +'<<tracking-multiple-repos,Tracking Multiple Repositories or Branches>>' +for more information on using GIT_SVN_ID. + +--follow-parent:: + This is especially helpful when we're tracking a directory + that has been moved around within the repository, or if we + started tracking a branch and never tracked the trunk it was + descended from. + +config key: svn.followparent + +--no-metadata:: + This gets rid of the git-svn-id: lines at the end of every commit. + + With this, you lose the ability to use the rebuild command. If + you ever lose your .git/svn/git-svn/.rev_db file, you won't be + able to fetch again, either. This is fine for one-shot imports. + + The 'git-svn log' command will not work on repositories using this, + either. + +config key: svn.nometadata + +-- + +COMPATIBILITY OPTIONS +--------------------- +-- + +--upgrade:: +Only used with the 'rebuild' command. + +Run this if you used an old version of git-svn that used +"git-svn-HEAD" instead of "remotes/git-svn" as the branch +for tracking the remote. + +--ignore-nodate:: +Only used with the 'fetch' command. + +By default git-svn will crash if it tries to import a revision +from SVN which has '(no date)' listed as the date of the revision. +This is repository corruption on SVN's part, plain and simple. +But sometimes you really need those revisions anyway. + +If supplied git-svn will convert '(no date)' entries to the UNIX +epoch (midnight on Jan. 1, 1970). Yes, that's probably very wrong. +SVN was very wrong. + +-- + +Basic Examples +~~~~~~~~~~~~~~ + +Tracking and contributing to a the trunk of a Subversion-managed project: + +------------------------------------------------------------------------ +# Initialize a repo (like git init): + git-svn init http://svn.foo.org/project/trunk +# Fetch remote revisions: + git-svn fetch +# Create your own branch to hack on: + git checkout -b my-branch remotes/git-svn +# Do some work, and then commit your new changes to SVN, as well as +# automatically updating your working HEAD: + git-svn dcommit +# Something is committed to SVN, rebase the latest into your branch: + git-svn fetch && git rebase remotes/git-svn +# Append svn:ignore settings to the default git exclude file: + git-svn show-ignore >> .git/info/exclude +------------------------------------------------------------------------ + +Tracking and contributing to an entire Subversion-managed project +(complete with a trunk, tags and branches): +See also: +'<<tracking-multiple-repos,Tracking Multiple Repositories or Branches>>' + +------------------------------------------------------------------------ +# Initialize a repo (like git init): + git-svn multi-init http://svn.foo.org/project \ + -T trunk -b branches -t tags +# Fetch remote revisions: + git-svn multi-fetch +# Create your own branch of trunk to hack on: + git checkout -b my-trunk remotes/trunk +# Do some work, and then commit your new changes to SVN, as well as +# automatically updating your working HEAD: + git-svn dcommit -i trunk +# Something has been committed to trunk, rebase the latest into your branch: + git-svn multi-fetch && git rebase remotes/trunk +# Append svn:ignore settings of trunk to the default git exclude file: + git-svn show-ignore -i trunk >> .git/info/exclude +# Check for new branches and tags (no arguments are needed): + git-svn multi-init +------------------------------------------------------------------------ + +REBASE VS. PULL/MERGE +--------------------- + +Originally, git-svn recommended that the remotes/git-svn branch be +pulled or merged from. This is because the author favored +'git-svn set-tree B' to commit a single head rather than the +'git-svn set-tree A..B' notation to commit multiple commits. + +If you use 'git-svn set-tree A..B' to commit several diffs and you do +not have the latest remotes/git-svn merged into my-branch, you should +use 'git rebase' to update your work branch instead of 'git pull' or +'git merge'. 'pull/merge' can cause non-linear history to be flattened +when committing into SVN, which can lead to merge commits reversing +previous commits in SVN. + +DESIGN PHILOSOPHY +----------------- +Merge tracking in Subversion is lacking and doing branched development +with Subversion is cumbersome as a result. git-svn does not do +automated merge/branch tracking by default and leaves it entirely up to +the user on the git side. + +[[tracking-multiple-repos]] +TRACKING MULTIPLE REPOSITORIES OR BRANCHES +------------------------------------------ +Because git-svn does not care about relationships between different +branches or directories in a Subversion repository, git-svn has a simple +hack to allow it to track an arbitrary number of related _or_ unrelated +SVN repositories via one git repository. Simply use the --id/-i flag or +set the GIT_SVN_ID environment variable to a name other other than +"git-svn" (the default) and git-svn will ignore the contents of the +$GIT_DIR/svn/git-svn directory and instead do all of its work in +$GIT_DIR/svn/$GIT_SVN_ID for that invocation. The interface branch will +be remotes/$GIT_SVN_ID, instead of remotes/git-svn. Any +remotes/$GIT_SVN_ID branch should never be modified by the user outside +of git-svn commands. + +[[fetch-args]] +ADDITIONAL FETCH ARGUMENTS +-------------------------- +This is for advanced users, most users should ignore this section. + +Unfetched SVN revisions may be imported as children of existing commits +by specifying additional arguments to 'fetch'. Additional parents may +optionally be specified in the form of sha1 hex sums at the +command-line. Unfetched SVN revisions may also be tied to particular +git commits with the following syntax: + +------------------------------------------------ + svn_revision_number=git_commit_sha1 +------------------------------------------------ + +This allows you to tie unfetched SVN revision 375 to your current HEAD: + +------------------------------------------------ + git-svn fetch 375=$(git-rev-parse HEAD) +------------------------------------------------ + +If you're tracking a directory that has moved, or otherwise been +branched or tagged off of another directory in the repository and you +care about the full history of the project, then you can use +the --follow-parent option. + +------------------------------------------------ + git-svn fetch --follow-parent +------------------------------------------------ + +BUGS +---- + +We ignore all SVN properties except svn:executable. Too difficult to +map them since we rely heavily on git write-tree being _exactly_ the +same on both the SVN and git working trees and I prefer not to clutter +working trees with metadata files. + +Renamed and copied directories are not detected by git and hence not +tracked when committing to SVN. I do not plan on adding support for +this as it's quite difficult and time-consuming to get working for all +the possible corner cases (git doesn't do it, either). Renamed and +copied files are fully supported if they're similar enough for git to +detect them. + +SEE ALSO +-------- +gitlink:git-rebase[1] + +Author +------ +Written by Eric Wong <normalperson@yhbt.net>. + +Documentation +------------- +Written by Eric Wong <normalperson@yhbt.net>. diff --git a/Documentation/git-svnimport.txt b/Documentation/git-svnimport.txt new file mode 100644 index 0000000000..b166cf3327 --- /dev/null +++ b/Documentation/git-svnimport.txt @@ -0,0 +1,177 @@ +git-svnimport(1) +================ +v0.1, July 2005 + +NAME +---- +git-svnimport - Import a SVN repository into git + + +SYNOPSIS +-------- +[verse] +'git-svnimport' [ -o <branch-for-HEAD> ] [ -h ] [ -v ] [ -d | -D ] + [ -C <GIT_repository> ] [ -i ] [ -u ] [-l limit_rev] + [ -b branch_subdir ] [ -T trunk_subdir ] [ -t tag_subdir ] + [ -s start_chg ] [ -m ] [ -r ] [ -M regex ] + [ -I <ignorefile_name> ] [ -A <author_file> ] + [ -R <repack_each_revs>] [ -P <path_from_trunk> ] + <SVN_repository_URL> [ <path> ] + + +DESCRIPTION +----------- +Imports a SVN repository into git. It will either create a new +repository, or incrementally import into an existing one. + +SVN access is done by the SVN::Perl module. + +git-svnimport assumes that SVN repositories are organized into one +"trunk" directory where the main development happens, "branch/FOO" +directories for branches, and "/tags/FOO" directories for tags. +Other subdirectories are ignored. + +git-svnimport creates a file ".git/svn2git", which is required for +incremental SVN imports. + +OPTIONS +------- +-C <target-dir>:: + The GIT repository to import to. If the directory doesn't + exist, it will be created. Default is the current directory. + +-s <start_rev>:: + Start importing at this SVN change number. The default is 1. ++ +When importing incrementally, you might need to edit the .git/svn2git file. + +-i:: + Import-only: don't perform a checkout after importing. This option + ensures the working directory and index remain untouched and will + not create them if they do not exist. + +-T <trunk_subdir>:: + Name the SVN trunk. Default "trunk". + +-t <tag_subdir>:: + Name the SVN subdirectory for tags. Default "tags". + +-b <branch_subdir>:: + Name the SVN subdirectory for branches. Default "branches". + +-o <branch-for-HEAD>:: + The 'trunk' branch from SVN is imported to the 'origin' branch within + the git repository. Use this option if you want to import into a + different branch. + +-r:: + Prepend 'rX: ' to commit messages, where X is the imported + subversion revision. + +-I <ignorefile_name>:: + Import the svn:ignore directory property to files with this + name in each directory. (The Subversion and GIT ignore + syntaxes are similar enough that using the Subversion patterns + directly with "-I .gitignore" will almost always just work.) + +-A <author_file>:: + Read a file with lines on the form ++ +------ + username = User's Full Name <email@addr.es> + +------ ++ +and use "User's Full Name <email@addr.es>" as the GIT +author and committer for Subversion commits made by +"username". If encountering a commit made by a user not in the +list, abort. ++ +For convenience, this data is saved to $GIT_DIR/svn-authors +each time the -A option is provided, and read from that same +file each time git-svnimport is run with an existing GIT +repository without -A. + +-m:: + Attempt to detect merges based on the commit message. This option + will enable default regexes that try to capture the name source + branch name from the commit message. + +-M <regex>:: + Attempt to detect merges based on the commit message with a custom + regex. It can be used with -m to also see the default regexes. + You must escape forward slashes. + +-l <max_rev>:: + Specify a maximum revision number to pull. ++ +Formerly, this option controlled how many revisions to pull, +due to SVN memory leaks. (These have been worked around.) + +-R <repack_each_revs>:: + Specify how often git repository should be repacked. ++ +The default value is 1000. git-svnimport will do import in chunks of 1000 +revisions, after each chunk git repository will be repacked. To disable +this behavior specify some big value here which is mote than number of +revisions to import. + +-P <path_from_trunk>:: + Partial import of the SVN tree. ++ +By default, the whole tree on the SVN trunk (/trunk) is imported. +'-P my/proj' will import starting only from '/trunk/my/proj'. +This option is useful when you want to import one project from a +svn repo which hosts multiple projects under the same trunk. + +-v:: + Verbosity: let 'svnimport' report what it is doing. + +-d:: + Use direct HTTP requests if possible. The "<path>" argument is used + only for retrieving the SVN logs; the path to the contents is + included in the SVN log. + +-D:: + Use direct HTTP requests if possible. The "<path>" argument is used + for retrieving the logs, as well as for the contents. ++ +There's no safe way to automatically find out which of these options to +use, so you need to try both. Usually, the one that's wrong will die +with a 40x error pretty quickly. + +<SVN_repository_URL>:: + The URL of the SVN module you want to import. For local + repositories, use "file:///absolute/path". ++ +If you're using the "-d" or "-D" option, this is the URL of the SVN +repository itself; it usually ends in "/svn". + +<path>:: + The path to the module you want to check out. + +-h:: + Print a short usage message and exit. + +OUTPUT +------ +If '-v' is specified, the script reports what it is doing. + +Otherwise, success is indicated the Unix way, i.e. by simply exiting with +a zero exit status. + +Author +------ +Written by Matthias Urlichs <smurf@smurf.noris.de>, with help from +various participants of the git-list <git@vger.kernel.org>. + +Based on a cvs2git script by the same author. + +Documentation +-------------- +Documentation by Matthias Urlichs <smurf@smurf.noris.de>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-symbolic-ref.txt b/Documentation/git-symbolic-ref.txt new file mode 100644 index 0000000000..a88f722860 --- /dev/null +++ b/Documentation/git-symbolic-ref.txt @@ -0,0 +1,61 @@ +git-symbolic-ref(1) +=================== + +NAME +---- +git-symbolic-ref - Read and modify symbolic refs + +SYNOPSIS +-------- +'git-symbolic-ref' [-q] [-m <reason>] <name> [<ref>] + +DESCRIPTION +----------- +Given one argument, reads which branch head the given symbolic +ref refers to and outputs its path, relative to the `.git/` +directory. Typically you would give `HEAD` as the <name> +argument to see on which branch your working tree is on. + +Give two arguments, create or update a symbolic ref <name> to +point at the given branch <ref>. + +A symbolic ref is a regular file that stores a string that +begins with `ref: refs/`. For example, your `.git/HEAD` is +a regular file whose contents is `ref: refs/heads/master`. + +OPTIONS +------- + +-q:: + Do not issue an error message if the <name> is not a + symbolic ref but a detached HEAD; instead exit with + non-zero status silently. + +-m:: + Update the reflog for <name> with <reason>. This is valid only + when creating or updating a symbolic ref. + +NOTES +----- +In the past, `.git/HEAD` was a symbolic link pointing at +`refs/heads/master`. When we wanted to switch to another branch, +we did `ln -sf refs/heads/newbranch .git/HEAD`, and when we wanted +to find out which branch we are on, we did `readlink .git/HEAD`. +This was fine, and internally that is what still happens by +default, but on platforms that do not have working symlinks, +or that do not have the `readlink(1)` command, this was a bit +cumbersome. On some platforms, `ln -sf` does not even work as +advertised (horrors). Therefore symbolic links are now deprecated +and symbolic refs are used by default. + +git-symbolic-ref will exit with status 0 if the contents of the +symbolic ref were printed correctly, with status 1 if the requested +name is not a symbolic ref, or 128 if another error occurs. + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-tag.txt b/Documentation/git-tag.txt new file mode 100644 index 0000000000..70235e8ddb --- /dev/null +++ b/Documentation/git-tag.txt @@ -0,0 +1,225 @@ +git-tag(1) +========== + +NAME +---- +git-tag - Create, list, delete or verify a tag object signed with GPG + + +SYNOPSIS +-------- +[verse] +'git-tag' [-a | -s | -u <key-id>] [-f | -v] [-m <msg> | -F <file>] <name> [<head>] +'git-tag' -d <name>... +'git-tag' -l [<pattern>] + +DESCRIPTION +----------- +Adds a 'tag' reference in `.git/refs/tags/` + +Unless `-f` is given, the tag must not yet exist in +`.git/refs/tags/` directory. + +If one of `-a`, `-s`, or `-u <key-id>` is passed, the command +creates a 'tag' object, and requires the tag message. Unless +`-m <msg>` is given, an editor is started for the user to type +in the tag message. + +Otherwise just the SHA1 object name of the commit object is +written (i.e. a lightweight tag). + +A GnuPG signed tag object will be created when `-s` or `-u +<key-id>` is used. When `-u <key-id>` is not used, the +committer identity for the current user is used to find the +GnuPG key for signing. + +`-d <tag>` deletes the tag. + +`-v <tag>` verifies the gpg signature of the tag. + +`-l <pattern>` lists tags that match the given pattern (or all +if no pattern is given). + +OPTIONS +------- +-a:: + Make an unsigned, annotated tag object + +-s:: + Make a GPG-signed tag, using the default e-mail address's key + +-u <key-id>:: + Make a GPG-signed tag, using the given key + +-f:: + Replace an existing tag with the given name (instead of failing) + +-d:: + Delete existing tags with the given names. + +-v:: + Verify the gpg signature of given the tag + +-l <pattern>:: + List tags that match the given pattern (or all if no pattern is given). + +-m <msg>:: + Use the given tag message (instead of prompting) + +-F <file>:: + Take the tag message from the given file. Use '-' to + read the message from the standard input. + +CONFIGURATION +------------- +By default, git-tag in sign-with-default mode (-s) will use your +committer identity (of the form "Your Name <your@email.address>") to +find a key. If you want to use a different default key, you can specify +it in the repository configuration as follows: + +[user] + signingkey = <gpg-key-id> + + +DISCUSSION +---------- + +On Re-tagging +~~~~~~~~~~~~~ + +What should you do when you tag a wrong commit and you would +want to re-tag? + +If you never pushed anything out, just re-tag it. Use "-f" to +replace the old one. And you're done. + +But if you have pushed things out (or others could just read +your repository directly), then others will have already seen +the old tag. In that case you can do one of two things: + +. The sane thing. +Just admit you screwed up, and use a different name. Others have +already seen one tag-name, and if you keep the same name, you +may be in the situation that two people both have "version X", +but they actually have 'different' "X"'s. So just call it "X.1" +and be done with it. + +. The insane thing. +You really want to call the new version "X" too, 'even though' +others have already seen the old one. So just use "git tag -f" +again, as if you hadn't already published the old one. + +However, Git does *not* (and it should not)change tags behind +users back. So if somebody already got the old tag, doing a "git +pull" on your tree shouldn't just make them overwrite the old +one. + +If somebody got a release tag from you, you cannot just change +the tag for them by updating your own one. This is a big +security issue, in that people MUST be able to trust their +tag-names. If you really want to do the insane thing, you need +to just fess up to it, and tell people that you messed up. You +can do that by making a very public announcement saying: + +------------ +Ok, I messed up, and I pushed out an earlier version tagged as X. I +then fixed something, and retagged the *fixed* tree as X again. + +If you got the wrong tag, and want the new one, please delete +the old one and fetch the new one by doing: + + git tag -d X + git fetch origin tag X + +to get my updated tag. + +You can test which tag you have by doing + + git rev-parse X + +which should return 0123456789abcdef.. if you have the new version. + +Sorry for inconvenience. +------------ + +Does this seem a bit complicated? It *should* be. There is no +way that it would be correct to just "fix" it behind peoples +backs. People need to know that their tags might have been +changed. + + +On Automatic following +~~~~~~~~~~~~~~~~~~~~~~ + +If you are following somebody else's tree, you are most likely +using tracking branches (`refs/heads/origin` in traditional +layout, or `refs/remotes/origin/master` in the separate-remote +layout). You usually want the tags from the other end. + +On the other hand, if you are fetching because you would want a +one-shot merge from somebody else, you typically do not want to +get tags from there. This happens more often for people near +the toplevel but not limited to them. Mere mortals when pulling +from each other do not necessarily want to automatically get +private anchor point tags from the other person. + +You would notice "please pull" messages on the mailing list says +repo URL and branch name alone. This is designed to be easily +cut&pasted to "git fetch" command line: + +------------ +Linus, please pull from + + git://git..../proj.git master + +to get the following updates... +------------ + +becomes: + +------------ +$ git pull git://git..../proj.git master +------------ + +In such a case, you do not want to automatically follow other's +tags. + +One important aspect of git is it is distributed, and being +distributed largely means there is no inherent "upstream" or +"downstream" in the system. On the face of it, the above +example might seem to indicate that the tag namespace is owned +by upper echelon of people and tags only flow downwards, but +that is not the case. It only shows that the usage pattern +determines who are interested in whose tags. + +A one-shot pull is a sign that a commit history is now crossing +the boundary between one circle of people (e.g. "people who are +primarily interested in networking part of the kernel") who may +have their own set of tags (e.g. "this is the third release +candidate from the networking group to be proposed for general +consumption with 2.6.21 release") to another circle of people +(e.g. "people who integrate various subsystem improvements"). +The latter are usually not interested in the detailed tags used +internally in the former group (that is what "internal" means). +That is why it is desirable not to follow tags automatically in +this case. + +It may well be that among networking people, they may want to +exchange the tags internal to their group, but in that workflow +they are most likely tracking with each other's progress by +having tracking branches. Again, the heuristic to automatically +follow such tags is a good thing. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org>, +Junio C Hamano <junkio@cox.net> and Chris Wright <chrisw@osdl.org>. + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-tar-tree.txt b/Documentation/git-tar-tree.txt new file mode 100644 index 0000000000..595940524e --- /dev/null +++ b/Documentation/git-tar-tree.txt @@ -0,0 +1,93 @@ +git-tar-tree(1) +=============== + +NAME +---- +git-tar-tree - Create a tar archive of the files in the named tree object + + +SYNOPSIS +-------- +'git-tar-tree' [--remote=<repo>] <tree-ish> [ <base> ] + +DESCRIPTION +----------- +THIS COMMAND IS DEPRECATED. Use `git-archive` with `--format=tar` +option instead. + +Creates a tar archive containing the tree structure for the named tree. +When <base> is specified it is added as a leading path to the files in the +generated tar archive. + +git-tar-tree behaves differently when given a tree ID versus when given +a commit ID or tag ID. In the first case the current time is used as +modification time of each file in the archive. In the latter case the +commit time as recorded in the referenced commit object is used instead. +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. + +CONFIGURATION +------------- +By default, file and directories modes are set to 0666 or 0777. It is +possible to change this by setting the "umask" variable in the +repository configuration as follows : + +[tar] + umask = 002 ;# group friendly + +The special umask value "user" indicates that the user's current umask +will be used instead. The default value is 002, which means group +readable/writable files and directories. + +EXAMPLES +-------- +git tar-tree HEAD junk | (cd /var/tmp/ && tar xf -):: + + 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 v1.4.0 git-1.4.0 | gzip >git-1.4.0.tar.gz:: + + Create a tarball for v1.4.0 release. + +git tar-tree v1.4.0{caret}\{tree\} git-1.4.0 | gzip >git-1.4.0.tar.gz:: + + Create a tarball for v1.4.0 release, but without a + global extended pax header. + +git tar-tree --remote=example.com:git.git v1.4.0 >git-1.4.0.tar:: + + Get a tarball v1.4.0 from example.com. + +git tar-tree HEAD:Documentation/ git-docs > git-1.4.0-docs.tar:: + + Put everything in the current head's Documentation/ directory + into 'git-1.4.0-docs.tar', with the prefix 'git-docs/'. + +Author +------ +Written by Rene Scharfe. + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-tools.txt b/Documentation/git-tools.txt new file mode 100644 index 0000000000..10653ff898 --- /dev/null +++ b/Documentation/git-tools.txt @@ -0,0 +1,115 @@ +A short git tools survey +======================== + + +Introduction +------------ + +Apart from git contrib/ area there are some others third-party tools +you may want to look. + +This document presents a brief summary of each tool and the corresponding +link. + + +Alternative/Augmentative Porcelains +----------------------------------- + + - *Cogito* (http://www.kernel.org/pub/software/scm/cogito/) + + Cogito is a version control system layered on top of the git tree history + storage system. It aims at seamless user interface and ease of use, + providing generally smoother user experience than the "raw" Core GIT + itself and indeed many other version control systems. + + + - *pg* (http://www.spearce.org/category/projects/scm/pg/) + + pg is a shell script wrapper around GIT to help the user manage a set of + patches to files. pg is somewhat like quilt or StGIT, but it does have a + slightly different feature set. + + + - *StGit* (http://www.procode.org/stgit/) + + Stacked GIT provides a quilt-like patch management functionality in the + GIT environment. You can easily manage your patches in the scope of GIT + until they get merged upstream. + + +History Viewers +--------------- + + - *gitk* (shipped with git-core) + + gitk is a simple Tk GUI for browsing history of GIT repositories easily. + + + - *gitview* (contrib/) + + gitview is a GTK based repository browser for git + + + - *gitweb* (shipped with git-core) + + GITweb provides full-fledged web interface for GIT repositories. + + + - *qgit* (http://digilander.libero.it/mcostalba/) + + QGit is a git/StGIT GUI viewer built on Qt/C++. QGit could be used + to browse history and directory tree, view annotated files, commit + changes cherry picking single files or applying patches. + Currently it is the fastest and most feature rich among the git + viewers and commit tools. + + - *tig* (http://jonas.nitro.dk/tig/) + + tig by Jonas Fonseca is a simple git repository browser + written using ncurses. Basically, it just acts as a front-end + for git-log and git-show/git-diff. Additionally, you can also + use it as a pager for git commands. + + +Foreign SCM interface +--------------------- + + - *git-svn* (shipped with git-core) + + git-svn is a simple conduit for changesets between a single Subversion + branch and git. + + + - *quilt2git / git2quilt* (http://home-tj.org/wiki/index.php/Misc) + + These utilities convert patch series in a quilt repository and commit + series in git back and forth. + + + - *hg-to-git* (contrib/) + + hg-to-git converts a Mercurial repository into a git one, and + preserves the full branch history in the process. hg-to-git can + also be used in an incremental way to keep the git repository + in sync with the master Mercurial repository. + + +Others +------ + + - *(h)gct* (http://www.cyd.liu.se/users/~freku045/gct/) + + Commit Tool or (h)gct is a GUI enabled commit tool for git and + Mercurial (hg). It allows the user to view diffs, select which files + to committed (or ignored / reverted) write commit messages and + perform the commit itself. + + - *git.el* (contrib/) + + This is an Emacs interface for git. The user interface is modeled on + pcl-cvs. It has been developed on Emacs 21 and will probably need some + tweaking to work on XEmacs. + + +http://git.or.cz/gitwiki/InterfacesFrontendsAndTools has more +comprehensive list. diff --git a/Documentation/git-unpack-file.txt b/Documentation/git-unpack-file.txt new file mode 100644 index 0000000000..213dc8196b --- /dev/null +++ b/Documentation/git-unpack-file.txt @@ -0,0 +1,36 @@ +git-unpack-file(1) +================== + +NAME +---- +git-unpack-file - Creates a temporary file with a blob's contents + + + +SYNOPSIS +-------- +'git-unpack-file' <blob> + +DESCRIPTION +----------- +Creates a file holding the contents of the blob specified by sha1. It +returns the name of the temporary file in the following format: + .merge_file_XXXXX + +OPTIONS +------- +<blob>:: + Must be a blob id + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-unpack-objects.txt b/Documentation/git-unpack-objects.txt new file mode 100644 index 0000000000..ff6184b0f7 --- /dev/null +++ b/Documentation/git-unpack-objects.txt @@ -0,0 +1,55 @@ +git-unpack-objects(1) +===================== + +NAME +---- +git-unpack-objects - Unpack objects from a packed archive + + +SYNOPSIS +-------- +'git-unpack-objects' [-n] [-q] [-r] <pack-file + + +DESCRIPTION +----------- +Read a packed archive (.pack) from the standard input, expanding +the objects contained within and writing them into the repository in +"loose" (one object per file) format. + +Objects that already exist in the repository will *not* be unpacked +from the pack-file. Therefore, nothing will be unpacked if you use +this command on a pack-file that exists within the target repository. + +Please see the `git-repack` documentation for options to generate +new packs and replace existing ones. + +OPTIONS +------- +-n:: + Only list the objects that would be unpacked, don't actually unpack + them. + +-q:: + The command usually shows percentage progress. This + flag suppresses it. + +-r:: + When unpacking a corrupt packfile, the command dies at + the first corruption. This flag tells it to keep going + and make the best effort to recover as many objects as + possible. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +------------- +Documentation by Junio C Hamano + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-update-index.txt b/Documentation/git-update-index.txt new file mode 100644 index 0000000000..b161c8b32b --- /dev/null +++ b/Documentation/git-update-index.txt @@ -0,0 +1,318 @@ +git-update-index(1) +=================== + +NAME +---- +git-update-index - Register file contents in the working tree to the index + + +SYNOPSIS +-------- +[verse] +'git-update-index' + [--add] [--remove | --force-remove] [--replace] + [--refresh] [-q] [--unmerged] [--ignore-missing] + [--cacheinfo <mode> <object> <file>]\* + [--chmod=(+|-)x] + [--assume-unchanged | --no-assume-unchanged] + [--really-refresh] [--unresolve] [--again | -g] + [--info-only] [--index-info] + [-z] [--stdin] + [--verbose] + [--] [<file>]\* + +DESCRIPTION +----------- +Modifies the index or directory cache. Each file mentioned is updated +into the index and any 'unmerged' or 'needs updating' state is +cleared. + +The way "git-update-index" handles files it is told about can be modified +using the various options: + +OPTIONS +------- +--add:: + If a specified file isn't in the index already then it's + added. + Default behaviour is to ignore new files. + +--remove:: + If a specified file is in the index but is missing then it's + removed. + Default behavior is to ignore removed file. + +--refresh:: + Looks at the current index and checks to see if merges or + updates are needed by checking stat() information. + +-q:: + Quiet. If --refresh finds that the index needs an update, the + default behavior is to error out. This option makes + git-update-index continue anyway. + +--unmerged:: + If --refresh finds unmerged changes in the index, the default + behavior is to error out. This option makes git-update-index + continue anyway. + +--ignore-missing:: + Ignores missing files during a --refresh + +--cacheinfo <mode> <object> <path>:: + Directly insert the specified info into the index. + +--index-info:: + Read index information from stdin. + +--chmod=(+|-)x:: + Set the execute permissions on the updated files. + +--assume-unchanged, --no-assume-unchanged:: + When these flags are specified, the object name recorded + for the paths are not updated. Instead, these options + sets and unsets the "assume unchanged" bit for the + paths. When the "assume unchanged" bit is on, git stops + checking the working tree files for possible + modifications, so you need to manually unset the bit to + tell git when you change the working tree file. This is + sometimes helpful when working with a big project on a + filesystem that has very slow lstat(2) system call + (e.g. cifs). + +--again, -g:: + Runs `git-update-index` itself on the paths whose index + entries are different from those from the `HEAD` commit. + +--unresolve:: + Restores the 'unmerged' or 'needs updating' state of a + file during a merge if it was cleared by accident. + +--info-only:: + Do not create objects in the object database for all + <file> arguments that follow this flag; just insert + their object IDs into the index. + +--force-remove:: + Remove the file from the index even when the working directory + still has such a file. (Implies --remove.) + +--replace:: + By default, when a file `path` exists in the index, + git-update-index refuses an attempt to add `path/file`. + Similarly if a file `path/file` exists, a file `path` + cannot be added. With --replace flag, existing entries + that conflicts with the entry being added are + automatically removed with warning messages. + +--stdin:: + Instead of taking list of paths from the command line, + read list of paths from the standard input. Paths are + separated by LF (i.e. one path per line) by default. + +--verbose:: + Report what is being added and removed from index. + +-z:: + Only meaningful with `--stdin`; paths are separated with + NUL character instead of LF. + +\--:: + Do not interpret any more arguments as options. + +<file>:: + Files to act on. + Note that files beginning with '.' are discarded. This includes + `./file` and `dir/./file`. If you don't want this, then use + cleaner names. + The same applies to directories ending '/' and paths with '//' + +Using --refresh +--------------- +'--refresh' does not calculate a new sha1 file or bring the index +up-to-date for mode/content changes. But what it *does* do is to +"re-match" the stat information of a file with the index, so that you +can refresh the index for a file that hasn't been changed but where +the stat entry is out of date. + +For example, you'd want to do this after doing a "git-read-tree", to link +up the stat index details with the proper files. + +Using --cacheinfo or --info-only +-------------------------------- +'--cacheinfo' is used to register a file that is not in the +current working directory. This is useful for minimum-checkout +merging. + +To pretend you have a file with mode and sha1 at path, say: + +---------------- +$ git-update-index --cacheinfo mode sha1 path +---------------- + +'--info-only' is used to register files without placing them in the object +database. This is useful for status-only repositories. + +Both '--cacheinfo' and '--info-only' behave similarly: the index is updated +but the object database isn't. '--cacheinfo' is useful when the object is +in the database but the file isn't available locally. '--info-only' is +useful when the file is available, but you do not wish to update the +object database. + + +Using --index-info +------------------ + +`--index-info` is a more powerful mechanism that lets you feed +multiple entry definitions from the standard input, and designed +specifically for scripts. It can take inputs of three formats: + + . mode SP sha1 TAB path ++ +The first format is what "git-apply --index-info" +reports, and used to reconstruct a partial tree +that is used for phony merge base tree when falling +back on 3-way merge. + + . mode SP type SP sha1 TAB path ++ +The second format is to stuff git-ls-tree output +into the index file. + + . mode SP sha1 SP stage TAB path ++ +This format is to put higher order stages into the +index file and matches git-ls-files --stage output. + +To place a higher stage entry to the index, the path should +first be removed by feeding a mode=0 entry for the path, and +then feeding necessary input lines in the third format. + +For example, starting with this index: + +------------ +$ git ls-files -s +100644 8a1218a1024a212bb3db30becd860315f9f3ac52 0 frotz +------------ + +you can feed the following input to `--index-info`: + +------------ +$ git update-index --index-info +0 0000000000000000000000000000000000000000 frotz +100644 8a1218a1024a212bb3db30becd860315f9f3ac52 1 frotz +100755 8a1218a1024a212bb3db30becd860315f9f3ac52 2 frotz +------------ + +The first line of the input feeds 0 as the mode to remove the +path; the SHA1 does not matter as long as it is well formatted. +Then the second and third line feeds stage 1 and stage 2 entries +for that path. After the above, we would end up with this: + +------------ +$ git ls-files -s +100644 8a1218a1024a212bb3db30becd860315f9f3ac52 1 frotz +100755 8a1218a1024a212bb3db30becd860315f9f3ac52 2 frotz +------------ + + +Using ``assume unchanged'' bit +------------------------------ + +Many operations in git depend on your filesystem to have an +efficient `lstat(2)` implementation, so that `st_mtime` +information for working tree files can be cheaply checked to see +if the file contents have changed from the version recorded in +the index file. Unfortunately, some filesystems have +inefficient `lstat(2)`. If your filesystem is one of them, you +can set "assume unchanged" bit to paths you have not changed to +cause git not to do this check. Note that setting this bit on a +path does not mean git will check the contents of the file to +see if it has changed -- it makes git to omit any checking and +assume it has *not* changed. When you make changes to working +tree files, you have to explicitly tell git about it by dropping +"assume unchanged" bit, either before or after you modify them. + +In order to set "assume unchanged" bit, use `--assume-unchanged` +option. To unset, use `--no-assume-unchanged`. + +The command looks at `core.ignorestat` configuration variable. When +this is true, paths updated with `git-update-index paths...` and +paths updated with other git commands that update both index and +working tree (e.g. `git-apply --index`, `git-checkout-index -u`, +and `git-read-tree -u`) are automatically marked as "assume +unchanged". Note that "assume unchanged" bit is *not* set if +`git-update-index --refresh` finds the working tree file matches +the index (use `git-update-index --really-refresh` if you want +to mark them as "assume unchanged"). + + +Examples +-------- +To update and refresh only the files already checked out: + +---------------- +$ git-checkout-index -n -f -a && git-update-index --ignore-missing --refresh +---------------- + +On an inefficient filesystem with `core.ignorestat` set:: ++ +------------ +$ git update-index --really-refresh <1> +$ git update-index --no-assume-unchanged foo.c <2> +$ git diff --name-only <3> +$ edit foo.c +$ git diff --name-only <4> +M foo.c +$ git update-index foo.c <5> +$ git diff --name-only <6> +$ edit foo.c +$ git diff --name-only <7> +$ git update-index --no-assume-unchanged foo.c <8> +$ git diff --name-only <9> +M foo.c +------------ ++ +<1> forces lstat(2) to set "assume unchanged" bits for paths that match index. +<2> mark the path to be edited. +<3> this does lstat(2) and finds index matches the path. +<4> this does lstat(2) and finds index does *not* match the path. +<5> registering the new version to index sets "assume unchanged" bit. +<6> and it is assumed unchanged. +<7> even after you edit it. +<8> you can tell about the change after the fact. +<9> now it checks with lstat(2) and finds it has been changed. + + +Configuration +------------- + +The command honors `core.filemode` configuration variable. If +your repository is on an filesystem whose executable bits are +unreliable, this should be set to 'false' (see gitlink:git-config[1]). +This causes the command to ignore differences in file modes recorded +in the index and the file mode on the filesystem if they differ only on +executable bit. On such an unfortunate filesystem, you may +need to use `git-update-index --chmod=`. + +The command looks at `core.ignorestat` configuration variable. See +'Using "assume unchanged" bit' section above. + + +See Also +-------- +gitlink:git-config[1] + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt new file mode 100644 index 0000000000..9424feab32 --- /dev/null +++ b/Documentation/git-update-ref.txt @@ -0,0 +1,90 @@ +git-update-ref(1) +================= + +NAME +---- +git-update-ref - Update the object name stored in a ref safely + +SYNOPSIS +-------- +'git-update-ref' [-m <reason>] (-d <ref> <oldvalue> | <ref> <newvalue> [<oldvalue>]) + +DESCRIPTION +----------- +Given two arguments, stores the <newvalue> in the <ref>, possibly +dereferencing the symbolic refs. E.g. `git-update-ref HEAD +<newvalue>` updates the current branch head to the new object. + +Given three arguments, stores the <newvalue> in the <ref>, +possibly dereferencing the symbolic refs, after verifying that +the current value of the <ref> matches <oldvalue>. +E.g. `git-update-ref refs/heads/master <newvalue> <oldvalue>` +updates the master branch head to <newvalue> only if its current +value is <oldvalue>. You can specify 40 "0" or an empty string +as <oldvalue> to make sure that the ref you are creating does +not exist. + +It also allows a "ref" file to be a symbolic pointer to another +ref file by starting with the four-byte header sequence of +"ref:". + +More importantly, it allows the update of a ref file to follow +these symbolic pointers, whether they are symlinks or these +"regular file symbolic refs". It follows *real* symlinks only +if they start with "refs/": otherwise it will just try to read +them and update them as a regular file (i.e. it will allow the +filesystem to follow them, but will overwrite such a symlink to +somewhere else with a regular filename). + +In general, using + + git-update-ref HEAD "$head" + +should be a _lot_ safer than doing + + echo "$head" > "$GIT_DIR/HEAD" + +both from a symlink following standpoint *and* an error checking +standpoint. The "refs/" rule for symlinks means that symlinks +that point to "outside" the tree are safe: they'll be followed +for reading but not for writing (so we'll never write through a +ref symlink to some other tree, if you have copied a whole +archive by creating a symlink tree). + +With `-d` flag, it deletes the named <ref> after verifying it +still contains <oldvalue>. + + +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>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-update-server-info.txt b/Documentation/git-update-server-info.txt new file mode 100644 index 0000000000..88a03c7c5e --- /dev/null +++ b/Documentation/git-update-server-info.txt @@ -0,0 +1,58 @@ +git-update-server-info(1) +========================= + +NAME +---- +git-update-server-info - Update auxiliary info file to help dumb servers + + +SYNOPSIS +-------- +'git-update-server-info' [--force] + +DESCRIPTION +----------- +A dumb server that does not do on-the-fly pack generations must +have some auxiliary information files in $GIT_DIR/info and +$GIT_OBJECT_DIRECTORY/info directories to help clients discover +what references and packs the server has. This command +generates such auxiliary files. + + +OPTIONS +------- + +-f|--force:: + Update the info files from scratch. + + +OUTPUT +------ + +Currently the command updates the following files. Please see +link:repository-layout.html[repository-layout] for description +of what they are for: + +* objects/info/packs + +* info/refs + + +BUGS +---- +When you remove an existing ref, the command fails to update +info/refs file unless `--force` flag is given. + + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-upload-archive.txt b/Documentation/git-upload-archive.txt new file mode 100644 index 0000000000..403871d7c6 --- /dev/null +++ b/Documentation/git-upload-archive.txt @@ -0,0 +1,37 @@ +git-upload-archive(1) +==================== + +NAME +---- +git-upload-archive - Send archive back to git-archive + + +SYNOPSIS +-------- +'git-upload-archive' <directory> + +DESCRIPTION +----------- +Invoked by 'git-archive --remote' and sends a generated 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-archive' side, and the program pair +is meant to be used to get an archive from a remote repository. + +OPTIONS +------- +<directory>:: + The repository to get a tar archive from. + +Author +------ +Written by Franck Bui-Huu. + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-upload-pack.txt b/Documentation/git-upload-pack.txt new file mode 100644 index 0000000000..9da062d5c9 --- /dev/null +++ b/Documentation/git-upload-pack.txt @@ -0,0 +1,39 @@ +git-upload-pack(1) +================== + +NAME +---- +git-upload-pack - Send objects packed back to git-fetch-pack + + +SYNOPSIS +-------- +'git-upload-pack' <directory> + +DESCRIPTION +----------- +Invoked by 'git-fetch-pack', learns what +objects the other side is missing, and sends them after packing. + +This command is usually not invoked directly by the end user. +The UI for the protocol is on the 'git-fetch-pack' side, and the +program pair is meant to be used to pull updates from a remote +repository. For push operations, see 'git-send-pack'. + + +OPTIONS +------- +<directory>:: + The repository to sync from. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the gitlink:git[7] suite diff --git a/Documentation/git-var.txt b/Documentation/git-var.txt new file mode 100644 index 0000000000..9b0de1c111 --- /dev/null +++ b/Documentation/git-var.txt @@ -0,0 +1,65 @@ +git-var(1) +========== + +NAME +---- +git-var - Show a git logical variable + + +SYNOPSIS +-------- +'git-var' [ -l | <variable> ] + +DESCRIPTION +----------- +Prints a git logical variable. + +OPTIONS +------- +-l:: + Cause the logical variables to be listed. In addition, all the + variables of the git configuration file .git/config are listed + as well. (However, the configuration variables listing functionality + is deprecated in favor of `git-config -l`.) + +EXAMPLE +-------- + $ git-var GIT_AUTHOR_IDENT + Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600 + + +VARIABLES +---------- +GIT_AUTHOR_IDENT:: + The author of a piece of code. + +GIT_COMMITTER_IDENT:: + The person who put a piece of code into git. + +Diagnostics +----------- +You don't exist. Go away!:: + The passwd(5) gecos field couldn't be read +Your parents must have hated you!:: + The password(5) gecos field is longer than a giant static buffer. +Your sysadmin must hate you!:: + The password(5) name field is longer than a giant static buffer. + +See Also +-------- +gitlink:git-commit-tree[1] +gitlink:git-tag[1] +gitlink:git-config[1] + +Author +------ +Written by Eric Biederman <ebiederm@xmission.com> + +Documentation +-------------- +Documentation by Eric Biederman and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-verify-pack.txt b/Documentation/git-verify-pack.txt new file mode 100644 index 0000000000..7a6132b016 --- /dev/null +++ b/Documentation/git-verify-pack.txt @@ -0,0 +1,54 @@ +git-verify-pack(1) +================== + +NAME +---- +git-verify-pack - Validate packed git archive files + + +SYNOPSIS +-------- +'git-verify-pack' [-v] [--] <pack>.idx ... + + +DESCRIPTION +----------- +Reads given idx file for packed git archive created with +git-pack-objects command and verifies idx file and the +corresponding pack file. + +OPTIONS +------- +<pack>.idx ...:: + The idx files to verify. + +-v:: + After verifying the pack, show list of objects contained + in the pack. +\--:: + Do not interpret any more arguments as options. + +OUTPUT FORMAT +------------- +When specifying the -v option the format used is: + + SHA1 type size offset-in-packfile + +for objects that are not deltified in the pack, and + + SHA1 type size offset-in-packfile depth base-SHA1 + +for objects that are deltified. + +Author +------ +Written by Junio C Hamano <junkio@cox.net> + +Documentation +-------------- +Documentation by Junio C Hamano + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-verify-tag.txt b/Documentation/git-verify-tag.txt new file mode 100644 index 0000000000..0f9bdb58dc --- /dev/null +++ b/Documentation/git-verify-tag.txt @@ -0,0 +1,32 @@ +git-verify-tag(1) +================= + +NAME +---- +git-verify-tag - Check the GPG signature of tag + +SYNOPSIS +-------- +'git-verify-tag' <tag> + +DESCRIPTION +----------- +Validates the gpg signature created by git-tag. + +OPTIONS +------- +<tag>:: + SHA1 identifier of a git tag object. + +Author +------ +Written by Jan Harkes <jaharkes@cs.cmu.edu> and Eric W. Biederman <ebiederm@xmission.com> + +Documentation +-------------- +Documentation by Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-whatchanged.txt b/Documentation/git-whatchanged.txt new file mode 100644 index 0000000000..399bff3bbc --- /dev/null +++ b/Documentation/git-whatchanged.txt @@ -0,0 +1,81 @@ +git-whatchanged(1) +================== + +NAME +---- +git-whatchanged - Show logs with difference each commit introduces + + +SYNOPSIS +-------- +'git-whatchanged' <option>... + +DESCRIPTION +----------- +Shows commit logs and diff output each commit introduces. The +command internally invokes 'git-rev-list' piped to +'git-diff-tree', and takes command line options for both of +these commands. + +This manual page describes only the most frequently used options. + + +OPTIONS +------- +-p:: + Show textual diffs, instead of the git internal diff + output format that is useful only to tell the changed + paths and their nature of changes. + +-<n>:: + Limit output to <n> commits. + +<since>..<until>:: + Limit output to between the two named commits (bottom + exclusive, top inclusive). + +-r:: + Show git internal diff output, but for the whole tree, + not just the top level. + +--pretty=<format>:: + Controls the output format for the commit logs. + <format> can be one of 'raw', 'medium', 'short', 'full', + and 'oneline'. + +-m:: + By default, differences for merge commits are not shown. + With this flag, show differences to that commit from all + of its parents. ++ +However, it is not very useful in general, although it +*is* useful on a file-by-file basis. + +Examples +-------- +git-whatchanged -p v2.6.12.. include/scsi drivers/scsi:: + + Show as patches the commits since version 'v2.6.12' that changed + any file in the include/scsi or drivers/scsi subdirectories + +git-whatchanged --since="2 weeks ago" \-- gitk:: + + Show the changes during the last two weeks to the file 'gitk'. + The "--" is necessary to avoid confusion with the *branch* named + 'gitk' + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and +Junio C Hamano <junkio@cox.net> + + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git-write-tree.txt b/Documentation/git-write-tree.txt new file mode 100644 index 0000000000..96d5e07b11 --- /dev/null +++ b/Documentation/git-write-tree.txt @@ -0,0 +1,50 @@ +git-write-tree(1) +================= + +NAME +---- +git-write-tree - Create a tree object from the current index + + +SYNOPSIS +-------- +'git-write-tree' [--missing-ok] [--prefix=<prefix>/] + +DESCRIPTION +----------- +Creates a tree object using the current index. + +The index must be in a fully merged state. + +Conceptually, `git-write-tree` sync()s the current index contents +into a set of tree files. +In order to have that match what is actually in your directory right +now, you need to have done a `git-update-index` phase before you did the +`git-write-tree`. + + +OPTIONS +------- +--missing-ok:: + Normally `git-write-tree` ensures that the objects referenced by the + 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> + +Documentation +-------------- +Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/git.txt b/Documentation/git.txt new file mode 100644 index 0000000000..29ee24c34f --- /dev/null +++ b/Documentation/git.txt @@ -0,0 +1,375 @@ +git(7) +====== + +NAME +---- +git - the stupid content tracker + + +SYNOPSIS +-------- +[verse] +'git' [--version] [--exec-path[=GIT_EXEC_PATH]] [-p|--paginate] + [--bare] [--git-dir=GIT_DIR] [--help] COMMAND [ARGS] + +DESCRIPTION +----------- +Git is a fast, scalable, distributed revision control system with an +unusually rich command set that provides both high-level operations +and full access to internals. + +See this link:tutorial.html[tutorial] to get started, then see +link:everyday.html[Everyday Git] for a useful minimum set of commands, and +"man git-commandname" for documentation of each command. CVS users may +also want to read link:cvs-migration.html[CVS migration]. +link:user-manual.html[Git User's Manual] is still work in +progress, but when finished hopefully it will guide a new user +in a coherent way to git enlightenment ;-). + +The COMMAND is either a name of a Git command (see below) or an alias +as defined in the configuration file (see gitlink:git-config[1]). + +OPTIONS +------- +--version:: + Prints the git suite version that the 'git' program came from. + +--help:: + Prints the synopsis and a list of the most commonly used + commands. If a git command is named this option will bring up + the man-page for that command. If the option '--all' or '-a' is + given then all available commands are printed. + +--exec-path:: + Path to wherever your core git programs are installed. + This can also be controlled by setting the GIT_EXEC_PATH + environment variable. If no path is given 'git' will print + the current setting and then exit. + +-p|--paginate:: + Pipe all output into 'less' (or if set, $PAGER). + +--git-dir=<path>:: + Set the path to the repository. This can also be controlled by + setting the GIT_DIR environment variable. + +--bare:: + Same as --git-dir=`pwd`. + +FURTHER DOCUMENTATION +--------------------- + +See the references above to get started using git. The following is +probably more detail than necessary for a first-time user. + +The <<Discussion,Discussion>> section below and the +link:core-tutorial.html[Core tutorial] both provide introductions to the +underlying git architecture. + +See also the link:howto-index.html[howto] documents for some useful +examples. + +GIT COMMANDS +------------ + +We divide git into high level ("porcelain") commands and low level +("plumbing") commands. + +High-level commands (porcelain) +------------------------------- + +We separate the porcelain commands into the main commands and some +ancillary user utilities. + +Main porcelain commands +~~~~~~~~~~~~~~~~~~~~~~~ + +include::cmds-mainporcelain.txt[] + +Ancillary Commands +~~~~~~~~~~~~~~~~~~ +Manipulators: + +include::cmds-ancillarymanipulators.txt[] + +Interrogators: + +include::cmds-ancillaryinterrogators.txt[] + + +Interacting with Others +~~~~~~~~~~~~~~~~~~~~~~~ + +These commands are to interact with foreign SCM and with other +people via patch over e-mail. + +include::cmds-foreignscminterface.txt[] + + +Low-level commands (plumbing) +----------------------------- + +Although git includes its +own porcelain layer, its low-level commands are sufficient to support +development of alternative porcelains. Developers of such porcelains +might start by reading about gitlink:git-update-index[1] and +gitlink:git-read-tree[1]. + +The interface (input, output, set of options and the semantics) +to these low-level commands are meant to be a lot more stable +than Porcelain level commands, because these commands are +primarily for scripted use. The interface to Porcelain commands +on the other hand are subject to change in order to improve the +end user experience. + +The following description divides +the low-level commands into commands that manipulate objects (in +the repository, index, and working tree), commands that interrogate and +compare objects, and commands that move objects and references between +repositories. + + +Manipulation commands +~~~~~~~~~~~~~~~~~~~~~ + +include::cmds-plumbingmanipulators.txt[] + + +Interrogation commands +~~~~~~~~~~~~~~~~~~~~~~ + +include::cmds-plumbinginterrogators.txt[] + +In general, the interrogate commands do not touch the files in +the working tree. + + +Synching repositories +~~~~~~~~~~~~~~~~~~~~~ + +include::cmds-synchingrepositories.txt[] + +The following are helper programs used by the above; end users +typically do not use them directly. + +include::cmds-synchelpers.txt[] + + +Internal helper commands +~~~~~~~~~~~~~~~~~~~~~~~~ + +These are internal helper commands used by other commands; end +users typically do not use them directly. + +include::cmds-purehelpers.txt[] + + +Configuration Mechanism +----------------------- + +Starting from 0.99.9 (actually mid 0.99.8.GIT), `.git/config` file +is used to hold per-repository configuration options. It is a +simple text file modeled after `.ini` format familiar to some +people. Here is an example: + +------------ +# +# A '#' or ';' character indicates a comment. +# + +; core variables +[core] + ; Don't trust file modes + filemode = false + +; user identity +[user] + name = "Junio C Hamano" + email = "junkio@twinsun.com" + +------------ + +Various commands read from the configuration file and adjust +their operation accordingly. + + +Identifier Terminology +---------------------- +<object>:: + Indicates the object name for any type of object. + +<blob>:: + Indicates a blob object name. + +<tree>:: + Indicates a tree object name. + +<commit>:: + Indicates a commit object name. + +<tree-ish>:: + Indicates a tree, commit or tag object name. A + command that takes a <tree-ish> argument ultimately wants to + operate on a <tree> object but automatically dereferences + <commit> and <tag> objects that point at a <tree>. + +<type>:: + Indicates that an object type is required. + Currently one of: `blob`, `tree`, `commit`, or `tag`. + +<file>:: + Indicates a filename - almost always relative to the + root of the tree structure `GIT_INDEX_FILE` describes. + +Symbolic Identifiers +-------------------- +Any git command accepting any <object> can also use the following +symbolic notation: + +HEAD:: + indicates the head of the current branch (i.e. the + contents of `$GIT_DIR/HEAD`). + +<tag>:: + a valid tag 'name' + (i.e. the contents of `$GIT_DIR/refs/tags/<tag>`). + +<head>:: + a valid head 'name' + (i.e. the contents of `$GIT_DIR/refs/heads/<head>`). + +For a more complete list of ways to spell object names, see +"SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1]. + + +File/Directory Structure +------------------------ + +Please see link:repository-layout.html[repository layout] document. + +Read link:hooks.html[hooks] for more details about each hook. + +Higher level SCMs may provide and manage additional information in the +`$GIT_DIR`. + + +Terminology +----------- +Please see link:glossary.html[glossary] document. + + +Environment Variables +--------------------- +Various git commands use the following environment variables: + +The git Repository +~~~~~~~~~~~~~~~~~~ +These environment variables apply to 'all' core git commands. Nb: it +is worth noting that they may be used/overridden by SCMS sitting above +git so take care if using Cogito etc. + +'GIT_INDEX_FILE':: + This environment allows the specification of an alternate + index file. If not specified, the default of `$GIT_DIR/index` + is used. + +'GIT_OBJECT_DIRECTORY':: + If the object storage directory is specified via this + environment variable then the sha1 directories are created + underneath - otherwise the default `$GIT_DIR/objects` + directory is used. + +'GIT_ALTERNATE_OBJECT_DIRECTORIES':: + Due to the immutable nature of git objects, old objects can be + archived into shared, read-only directories. This variable + specifies a ":" separated list of git object directories which + can be used to search for git objects. New objects will not be + written to these directories. + +'GIT_DIR':: + If the 'GIT_DIR' environment variable is set then it + specifies a path to use instead of the default `.git` + for the base of the repository. + +git Commits +~~~~~~~~~~~ +'GIT_AUTHOR_NAME':: +'GIT_AUTHOR_EMAIL':: +'GIT_AUTHOR_DATE':: +'GIT_COMMITTER_NAME':: +'GIT_COMMITTER_EMAIL':: + see gitlink:git-commit-tree[1] + +git Diffs +~~~~~~~~~ +'GIT_DIFF_OPTS':: + Only valid setting is "--unified=??" or "-u??" to set the + number of context lines shown when a unified diff is created. + This takes precedence over any "-U" or "--unified" option + value passed on the git diff command line. + +'GIT_EXTERNAL_DIFF':: + When the environment variable 'GIT_EXTERNAL_DIFF' is set, the + program named by it is called, instead of the diff invocation + described above. For a path that is added, removed, or modified, + 'GIT_EXTERNAL_DIFF' is called with 7 parameters: + + path old-file old-hex old-mode new-file new-hex new-mode ++ +where: + + <old|new>-file:: are files GIT_EXTERNAL_DIFF can use to read the + contents of <old|new>, + <old|new>-hex:: are the 40-hexdigit SHA1 hashes, + <old|new>-mode:: are the octal representation of the file modes. + ++ +The file parameters can point at the user's working file +(e.g. `new-file` in "git-diff-files"), `/dev/null` (e.g. `old-file` +when a new file is added), or a temporary file (e.g. `old-file` in the +index). 'GIT_EXTERNAL_DIFF' should not worry about unlinking the +temporary file --- it is removed when 'GIT_EXTERNAL_DIFF' exits. ++ +For a path that is unmerged, 'GIT_EXTERNAL_DIFF' is called with 1 +parameter, <path>. + +other +~~~~~ +'GIT_PAGER':: + This environment variable overrides `$PAGER`. + +'GIT_TRACE':: + If this variable is set to "1", "2" or "true" (comparison + is case insensitive), git will print `trace:` messages on + stderr telling about alias expansion, built-in command + execution and external command execution. + If this variable is set to an integer value greater than 1 + and lower than 10 (strictly) then git will interpret this + value as an open file descriptor and will try to write the + trace messages into this file descriptor. + Alternatively, if this variable is set to an absolute path + (starting with a '/' character), git will interpret this + as a file path and will try to write the trace messages + into it. + +Discussion[[Discussion]] +------------------------ +include::core-intro.txt[] + +Authors +------- +* git's founding father is Linus Torvalds <torvalds@osdl.org>. +* The current git nurse is Junio C Hamano <junkio@cox.net>. +* The git potty was written by Andres Ericsson <ae@op5.se>. +* General upbringing is handled by the git-list <git@vger.kernel.org>. + +Documentation +-------------- +The documentation for git suite was started by David Greaves +<david@dgreaves.com>, and later enhanced greatly by the +contributors on the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/gitk.txt b/Documentation/gitk.txt new file mode 100644 index 0000000000..48c5894736 --- /dev/null +++ b/Documentation/gitk.txt @@ -0,0 +1,102 @@ +gitk(1) +======= + +NAME +---- +gitk - The git repository browser + +SYNOPSIS +-------- +'gitk' [<option>...] [<revs>] [--] [<path>...] + +DESCRIPTION +----------- +Displays changes in a repository or a selected set of commits. This includes +visualizing the commit graph, showing information related to each commit, and +the files in the trees of each revision. + +Historically, gitk was the first repository browser. It's written in tcl/tk +and started off in a separate repository but was later merged into the main +git repository. + +OPTIONS +------- +To control which revisions to shown, the command takes options applicable to +the gitlink:git-rev-list[1] command. This manual page describes only the most +frequently used options. + +-n <number>, --max-count=<number>:: + + Limits the number of commits to show. + +--since=<date>:: + + Show commits more recent than a specific date. + +--until=<date>:: + + Show commits older than a specific date. + +--all:: + + Show all branches. + +<revs>:: + + Limit the revisions to show. This can be either a single revision + meaning show from the given revision and back, or it can be a range in + the form "'<from>'..'<to>'" to show all revisions between '<from>' and + back to '<to>'. Note, more advanced revision selection can be applied. + For a more complete list of ways to spell object names, see + "SPECIFYING REVISIONS" section in gitlink:git-rev-parse[1]. + +<path>:: + + Limit commits to the ones touching files in the given paths. Note, to + avoid ambiguity wrt. revision names use "--" to separate the paths + from any preceding options. + +Examples +-------- +gitk v2.6.12.. include/scsi drivers/scsi:: + + Show as the changes since version 'v2.6.12' that changed any + file in the include/scsi or drivers/scsi subdirectories + +gitk --since="2 weeks ago" \-- gitk:: + + Show the changes during the last two weeks to the file 'gitk'. + The "--" is necessary to avoid confusion with the *branch* named + 'gitk' + +gitk --max-count=100 --all -- Makefile:: + + Show at most 100 changes made to the file 'Makefile'. Instead of only + looking for changes in the current branch look in all branches. + +See Also +-------- +'qgit(1)':: + A repository browser written in C++ using Qt. + +'gitview(1)':: + A repository browser written in Python using Gtk. It's based on + 'bzrk(1)' and distributed in the contrib area of the git repository. + +'tig(1)':: + A minimal repository browser and git tool output highlighter written + in C using Ncurses. + +Author +------ +Written by Paul Mackerras <paulus@samba.org>. + +Documentation +-------------- +Documentation by Junio C Hamano, Jonas Fonseca, and the git-list +<git@vger.kernel.org>. + +GIT +--- +Part of the gitlink:git[7] suite + diff --git a/Documentation/glossary.txt b/Documentation/glossary.txt new file mode 100644 index 0000000000..d20eb6270c --- /dev/null +++ b/Documentation/glossary.txt @@ -0,0 +1,356 @@ +alternate object database:: + Via the alternates mechanism, a repository can inherit part of its + object database from another object database, which is called + "alternate". + +bare repository:: + A bare repository is normally an appropriately named + directory with a `.git` suffix that does not have a + locally checked-out copy of any of the files under revision + control. That is, all of the `git` administrative and + control files that would normally be present in the + hidden `.git` sub-directory are directly present in + the `repository.git` directory instead, and no other files + are present and checked out. Usually publishers of public + repositories make bare repositories available. + +blob object:: + Untyped object, e.g. the contents of a file. + +branch:: + A non-cyclical graph of revisions, i.e. the complete history of + a particular revision, which is called the branch head. The + branch heads are stored in `$GIT_DIR/refs/heads/`. + +cache:: + Obsolete for: index. + +chain:: + A list of objects, where each object in the list contains a + reference to its successor (for example, the successor of a commit + could be one of its parents). + +changeset:: + BitKeeper/cvsps speak for "commit". Since git does not store + changes, but states, it really does not make sense to use + the term "changesets" with git. + +checkout:: + The action of updating the working tree to a revision which was + stored in the object database. + +cherry-picking:: + In SCM jargon, "cherry pick" means to choose a subset of + changes out of a series of changes (typically commits) + and record them as a new series of changes on top of + different codebase. In GIT, this is performed by + "git cherry-pick" command to extract the change + introduced by an existing commit and to record it based + on the tip of the current branch as a new commit. + +clean:: + A working tree is clean, if it corresponds to the revision + referenced by the current head. Also see "dirty". + +commit:: + As a verb: The action of storing the current state of the index in the + object database. The result is a revision. + As a noun: Short hand for commit object. + +commit object:: + An object which contains the information about a particular + revision, such as parents, committer, author, date and the + tree object which corresponds to the top directory of the + stored revision. + +core git:: + Fundamental data structures and utilities of git. Exposes only + limited source code management tools. + +DAG:: + Directed acyclic graph. The commit objects form a directed acyclic + graph, because they have parents (directed), and the graph of commit + objects is acyclic (there is no chain which begins and ends with the + same object). + +dircache:: + You are *waaaaay* behind. + +dirty:: + A working tree is said to be dirty if it contains modifications + which have not been committed to the current branch. + +directory:: + The list you get with "ls" :-) + +ent:: + Favorite synonym to "tree-ish" by some total geeks. See + `http://en.wikipedia.org/wiki/Ent_(Middle-earth)` for an in-depth + explanation. Avoid this term, not to confuse people. + +fast forward:: + A fast-forward is a special type of merge where you have + a revision and you are "merging" another branch's changes + that happen to be a descendant of what you have. + In such these cases, you do not make a new merge commit but + instead just update to his revision. This will happen + frequently on a tracking branch of a remote repository. + +fetch:: + Fetching a branch means to get the branch's head ref from a + remote repository, to find out which objects are missing from + the local object database, and to get them, too. + +file system:: + Linus Torvalds originally designed git to be a user space file + system, i.e. the infrastructure to hold files and directories. + That ensured the efficiency and speed of git. + +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. + +head:: + The top of a branch. It contains a ref to the corresponding + commit object. + +head ref:: + A ref pointing to a head. Often, this is abbreviated to "head". + Head refs are stored in `$GIT_DIR/refs/heads/`. + +hook:: + During the normal execution of several git commands, + call-outs are made to optional scripts that allow + a developer to add functionality or checking. + Typically, the hooks allow for a command to be pre-verified + and potentially aborted, and allow for a post-notification + after the operation is done. + The hook scripts are found in the `$GIT_DIR/hooks/` directory, + and are enabled by simply making them executable. + +index:: + A collection of files with stat information, whose contents are + stored as objects. The index is a stored version of your working + tree. Truth be told, it can also contain a second, and even a third + version of a working tree, which are used when merging. + +index entry:: + The information regarding a particular file, stored in the index. + An index entry can be unmerged, if a merge was started, but not + yet finished (i.e. if the index contains multiple versions of + that file). + +master:: + The default development branch. Whenever you create a git + repository, a branch named "master" is created, and becomes + the active branch. In most cases, this contains the local + development, though that is purely conventional and not required. + +merge:: + To merge branches means to try to accumulate the changes since a + common ancestor and apply them to the first branch. An automatic + merge uses heuristics to accomplish that. Evidently, an automatic + merge can fail. + +object:: + The unit of storage in git. It is uniquely identified by + the SHA1 of its contents. Consequently, an object can not + be changed. + +object database:: + Stores a set of "objects", and an individual object is identified + by its object name. The objects usually live in `$GIT_DIR/objects/`. + +object identifier:: + Synonym for object name. + +object name:: + The unique identifier of an object. The hash of the object's contents + using the Secure Hash Algorithm 1 and usually represented by the 40 + character hexadecimal encoding of the hash of the object (possibly + followed by a white space). + +object type:: + One of the identifiers "commit","tree","tag" and "blob" describing + the type of an object. + +octopus:: + To merge more than two branches. Also denotes an intelligent + predator. + +origin:: + The default upstream repository. Most projects have at + least one upstream project which they track. By default + 'origin' is used for that purpose. New upstream updates + will be fetched into remote tracking branches named + origin/name-of-upstream-branch, which you can see using + "git branch -r". + +pack:: + A set of objects which have been compressed into one file (to save + space or to transmit them efficiently). + +pack index:: + The list of identifiers, and other information, of the objects in a + pack, to assist in efficiently accessing the contents of a pack. + +parent:: + A commit object contains a (possibly empty) list of the logical + predecessor(s) in the line of development, i.e. its parents. + +pickaxe:: + The term pickaxe refers to an option to the diffcore routines + that help select changes that add or delete a given text string. + With the --pickaxe-all option, it can be used to view the + full changeset that introduced or removed, say, a particular + line of text. See gitlink:git-diff[1]. + +plumbing:: + Cute name for core git. + +porcelain:: + Cute name for programs and program suites depending on core git, + presenting a high level access to core git. Porcelains expose + more of a SCM interface than the plumbing. + +pull:: + Pulling a branch means to fetch it and merge it. + +push:: + Pushing a branch means to get the branch's head ref from a remote + repository, find out if it is an ancestor to the branch's local + head ref is a direct, and in that case, putting all objects, which + are reachable from the local head ref, and which are missing from + the remote repository, into the remote object database, and updating + the remote head ref. If the remote head is not an ancestor to the + local head, the push fails. + +reachable:: + All of the ancestors of a given commit are said to be reachable from + that commit. More generally, one object is reachable from another if + we can reach the one from the other by a chain that follows tags to + whatever they tag, commits to their parents or trees, and trees to the + trees or blobs that they contain. + +rebase:: + To clean a branch by starting from the head of the main line of + development ("master"), and reapply the (possibly cherry-picked) + changes from that branch. + +ref:: + A 40-byte hex representation of a SHA1 or a name that denotes + a particular object. These may be stored in `$GIT_DIR/refs/`. + +refspec:: + A refspec is used by fetch and push to describe the mapping + between remote ref and local ref. They are combined with + a colon in the format <src>:<dst>, preceded by an optional + plus sign, +. For example: + `git fetch $URL refs/heads/master:refs/heads/origin` + means "grab the master branch head from the $URL and store + it as my origin branch head". + And `git push $URL refs/heads/master:refs/heads/to-upstream` + means "publish my master branch head as to-upstream branch + at $URL". See also gitlink:git-push[1] + +repository:: + A collection of refs together with an object database containing + all objects, which are reachable from the refs, possibly accompanied + by meta data from one or more porcelains. A repository can + share an object database with other repositories. + +resolve:: + The action of fixing up manually what a failed automatic merge + left behind. + +revision:: + A particular state of files and directories which was stored in + the object database. It is referenced by a commit object. + +rewind:: + To throw away part of the development, i.e. to assign the head to + an earlier revision. + +SCM:: + Source code management (tool). + +SHA1:: + Synonym for object name. + +shallow repository:: + A shallow repository has an incomplete history some of + whose commits have parents cauterized away (in other + words, git is told to pretend that these commits do not + have the parents, even though they are recorded in the + commit object). This is sometimes useful when you are + interested only in the recent history of a project even + though the real history recorded in the upstream is + much larger. A shallow repository is created by giving + `--depth` option to gitlink:git-clone[1], and its + history can be later deepened with gitlink:git-fetch[1]. + +symref:: + Symbolic reference: instead of containing the SHA1 id itself, it + is of the format 'ref: refs/some/thing' and when referenced, it + recursively dereferences to this reference. 'HEAD' is a prime + example of a symref. Symbolic references are manipulated with + the gitlink:git-symbolic-ref[1] command. + +topic branch:: + A regular git branch that is used by a developer to + identify a conceptual line of development. Since branches + are very easy and inexpensive, it is often desirable to + have several small branches that each contain very well + defined concepts or small incremental yet related changes. + +tracking branch:: + A regular git branch that is used to follow changes from + another repository. A tracking branch should not contain + direct modifications or have local commits made to it. + A tracking branch can usually be identified as the + right-hand-side ref in a Pull: refspec. + +tree object:: + An object containing a list of file names and modes along with refs + to the associated blob and/or tree objects. A tree is equivalent + to a directory. + +tree:: + Either a working tree, or a tree object together with the + dependent blob and tree objects (i.e. a stored representation + of a working tree). + +tree-ish:: + A ref pointing to either a commit object, a tree object, or a + tag object pointing to a tag or commit or tree object. + +tag object:: + An object containing a ref pointing to another object, which can + contain a message just like a commit object. It can also + contain a (PGP) signature, in which case it is called a "signed + tag object". + +tag:: + A ref pointing to a tag or commit object. In contrast to a head, + a tag is not changed by a commit. Tags (not tag objects) are + stored in `$GIT_DIR/refs/tags/`. A git tag has nothing to do with + a Lisp tag (which is called object type in git's context). + A tag is most typically used to mark a particular point in the + commit ancestry chain. + +unmerged index:: + An index which contains unmerged index entries. + +working tree:: + The set of files and directories currently being worked on, + i.e. you can work in your working tree without using git at all. + diff --git a/Documentation/hooks.txt b/Documentation/hooks.txt new file mode 100644 index 0000000000..b083290d12 --- /dev/null +++ b/Documentation/hooks.txt @@ -0,0 +1,159 @@ +Hooks used by git +================= + +Hooks are little scripts you can place in `$GIT_DIR/hooks` +directory to trigger action at certain points. When +`git-init` is run, a handful example hooks are copied in the +`hooks` directory of the new repository, but by default they are +all disabled. To enable a hook, make it executable with `chmod +x`. + +This document describes the currently defined hooks. + +applypatch-msg +-------------- + +This hook is invoked by `git-applypatch` script, which is +typically invoked by `git-applymbox`. It takes a single +parameter, the name of the file that holds the proposed commit +log message. Exiting with non-zero status causes +`git-applypatch` to abort before applying the patch. + +The hook is allowed to edit the message file in place, and can +be used to normalize the message into some project standard +format (if the project has one). It can also be used to refuse +the commit after inspecting the message file. + +The default 'applypatch-msg' hook, when enabled, runs the +'commit-msg' hook, if the latter is enabled. + +pre-applypatch +-------------- + +This hook is invoked by `git-applypatch` script, which is +typically invoked by `git-applymbox`. It takes no parameter, +and is invoked after the patch is applied, but before a commit +is made. Exiting with non-zero status causes the working tree +after application of the patch not committed. + +It can be used to inspect the current working tree and refuse to +make a commit if it does not pass certain test. + +The default 'pre-applypatch' hook, when enabled, runs the +'pre-commit' hook, if the latter is enabled. + +post-applypatch +--------------- + +This hook is invoked by `git-applypatch` script, which is +typically invoked by `git-applymbox`. It takes no parameter, +and is invoked after the patch is applied and a commit is made. + +This hook is meant primarily for notification, and cannot affect +the outcome of `git-applypatch`. + +pre-commit +---------- + +This hook is invoked by `git-commit`, and can be bypassed +with `\--no-verify` option. It takes no parameter, and is +invoked before obtaining the proposed commit log message and +making a commit. Exiting with non-zero status from this script +causes the `git-commit` to abort. + +The default 'pre-commit' hook, when enabled, catches introduction +of lines with trailing whitespaces and aborts the commit when +such a line is found. + +commit-msg +---------- + +This hook is invoked by `git-commit`, and can be bypassed +with `\--no-verify` option. It takes a single parameter, the +name of the file that holds the proposed commit log message. +Exiting with non-zero status causes the `git-commit` to +abort. + +The hook is allowed to edit the message file in place, and can +be used to normalize the message into some project standard +format (if the project has one). It can also be used to refuse +the commit after inspecting the message file. + +The default 'commit-msg' hook, when enabled, detects duplicate +"Signed-off-by" lines, and aborts the commit if one is found. + +post-commit +----------- + +This hook is invoked by `git-commit`. It takes no +parameter, and is invoked after a commit is made. + +This hook is meant primarily for notification, and cannot affect +the outcome of `git-commit`. + +update +------ + +This hook is invoked by `git-receive-pack` on the remote repository, +which 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. Its exit status determines the success or failure of +the ref update. + +The hook executes once for each ref to be updated, and takes +three parameters: + + - the name of the ref being updated, + - the old object name stored in the ref, + - and the new objectname to be stored in the ref. + +A zero exit from the update hook allows the ref to be updated. +Exiting with a non-zero status prevents `git-receive-pack` +from updating the ref. + +This hook can be used to prevent 'forced' update on certain refs by +making sure that the object name is a commit object that is a +descendant of the commit object named by the old object name. +That is, to enforce a "fast forward only" policy. + +It could also be used to log the old..new status. However, it +does not know the entire set of branches, so it would end up +firing one e-mail per ref when used naively, though. + +Another use suggested on the mailing list is to use this hook to +implement access control which is finer grained than the one +based on filesystem group. + +The standard output of this hook is sent to `stderr`, so if you +want to report something to the `git-send-pack` on the other end, +you can simply `echo` your messages. + +The default 'update' hook, when enabled, demonstrates how to +send out a notification e-mail. + +post-update +----------- + +This hook is invoked by `git-receive-pack` on the remote repository, +which happens when a `git push` is done on a local repository. +It executes on the remote repository once after all the refs have +been updated. + +It takes a variable number of parameters, each of which is the +name of ref that was actually updated. + +This hook is meant primarily for notification, and cannot affect +the outcome of `git-receive-pack`. + +The 'post-update' hook can tell what are the heads that were pushed, +but it does not know what their original and updated values are, +so it is a poor place to do log old..new. + +When enabled, the default 'post-update' hook runs +`git-update-server-info` to keep the information used by dumb +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. + +The standard output of this hook is sent to `/dev/null`; if you +want to report something to the `git-send-pack` on the other end, +you can redirect your output to your `stderr`. diff --git a/Documentation/howto-index.sh b/Documentation/howto-index.sh new file mode 100755 index 0000000000..34aa30c5b9 --- /dev/null +++ b/Documentation/howto-index.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +cat <<\EOF +GIT Howto Index +=============== + +Here is a collection of mailing list postings made by various +people describing how they use git in their workflow. + +EOF + +for txt +do + title=`expr "$txt" : '.*/\(.*\)\.txt$'` + from=`sed -ne ' + /^$/q + /^From:[ ]/{ + s/// + s/^[ ]*// + s/[ ]*$// + s/^/by / + p + } + ' "$txt"` + + abstract=`sed -ne ' + /^Abstract:[ ]/{ + s/^[^ ]*// + x + s/.*// + x + : again + /^[ ]/{ + s/^[ ]*// + H + n + b again + } + x + p + q + }' "$txt"` + + if grep 'Content-type: text/asciidoc' >/dev/null $txt + then + file=`expr "$txt" : '\(.*\)\.txt$'`.html + else + file="$txt" + fi + + echo "* link:$file[$title] $from +$abstract + +" + +done diff --git a/Documentation/howto/dangling-objects.txt b/Documentation/howto/dangling-objects.txt new file mode 100644 index 0000000000..e82ddae3cf --- /dev/null +++ b/Documentation/howto/dangling-objects.txt @@ -0,0 +1,109 @@ +From: Linus Torvalds <torvalds@linux-foundation.org> +Subject: Re: Question about fsck-objects output +Date: Thu, 25 Jan 2007 12:01:06 -0800 (PST) +Message-ID: <Pine.LNX.4.64.0701251144290.25027@woody.linux-foundation.org> +Archived-At: <http://permalink.gmane.org/gmane.comp.version-control.git/37754> +Abstract: Linus describes what dangling objects are, when they + are left behind, and how to view their relationship with branch + heads in gitk + +On Thu, 25 Jan 2007, Larry Streepy wrote: + +> Sorry to ask such a basic question, but I can't quite decipher the output of +> fsck-objects. When I run it, I get this: +> +> git fsck-objects +> dangling commit 2213f6d4dd39ca8baebd0427723723e63208521b +> dangling commit f0d4e00196bd5ee54463e9ea7a0f0e8303da767f +> dangling blob 6a6d0b01b3e96d49a8f2c7addd4ef8c3bd1f5761 +> +> +> Even after a "repack -a -d" they still exist. The man page has a short +> explanation, but, at least for me, it wasn't fully enlightening. :-) +> +> The man page says that dangling commits could be "root" commits, but since my +> repo started as a clone of another repo, I don't see how I could have any root +> commits. Also, the page doesn't really describe what a dangling blob is. +> +> So, can someone explain what these artifacts are and if they are a problem +> that I should be worried about? + +The most common situation is that you've rebased a branch (or you have +pulled from somebody else who rebased a branch, like the "pu" branch in +the git.git archive itself). + +What happens is that the old head of the original branch still exists, as +does obviously everything it pointed to. The branch pointer itself just +doesn't, since you replaced it with another one. + +However, there are certainly other situations too that cause dangling +objects. For example, the "dangling blob" situation you have tends to be +because you did a "git add" of a file, but then, before you actually +committed it and made it part of the bigger picture, you changed something +else in that file and committed that *updated* thing - the old state that +you added originally ends up not being pointed to by any commit/tree, so +it's now a dangling blob object. + +Similarly, when the "recursive" merge strategy runs, and finds that there +are criss-cross merges and thus more than one merge base (which is fairly +unusual, but it does happen), it will generate one temporary midway tree +(or possibly even more, if you had lots of criss-crossing merges and +more than two merge bases) as a temporary internal merge base, and again, +those are real objects, but the end result will not end up pointing to +them, so they end up "dangling" in your repository. + +Generally, dangling objects aren't anything to worry about. They can even +be very useful: if you screw something up, the dangling objects can be how +you recover your old tree (say, you did a rebase, and realized that you +really didn't want to - you can look at what dangling objects you have, +and decide to reset your head to some old dangling state). + +For commits, the most useful thing to do with dangling objects tends to be +to do a simple + + gitk <dangling-commit-sha-goes-here> --not --all + +which means exactly what it sounds like: it says that you want to see the +commit history that is described by the dangling commit(s), but you do NOT +want to see the history that is described by all your branches and tags +(which are the things you normally reach). That basically shows you in a +nice way what the danglign commit was (and notice that it might not be +just one commit: we only report the "tip of the line" as being dangling, +but there might be a whole deep and complex commit history that has gotten +dropped - rebasing will do that). + +For blobs and trees, you can't do the same, but you can examine them. You +can just do + + git show <dangling-blob/tree-sha-goes-here> + +to show what the contents of the blob were (or, for a tree, basically what +the "ls" for that directory was), and that may give you some idea of what +the operation was that left that dangling object. + +Usually, dangling blobs and trees aren't very interesting. They're almost +always the result of either being a half-way mergebase (the blob will +often even have the conflict markers from a merge in it, if you have had +conflicting merges that you fixed up by hand), or simply because you +interrupted a "git fetch" with ^C or something like that, leaving _some_ +of the new objects in the object database, but just dangling and useless. + +Anyway, once you are sure that you're not interested in any dangling +state, you can just prune all unreachable objects: + + git prune + +and they'll be gone. But you should only run "git prune" on a quiescent +repository - it's kind of like doing a filesystem fsck recovery: you don't +want to do that while the filesystem is mounted. + +(The same is true of "git-fsck-objects" itself, btw - but since +git-fsck-objects never actually *changes* the repository, it just reports +on what it found, git-fsck-objects itself is never "dangerous" to run. +Running it while somebody is actually changing the repository can cause +confusing and scary messages, but it won't actually do anything bad. In +contrast, running "git prune" while somebody is actively changing the +repository is a *BAD* idea). + + Linus + diff --git a/Documentation/howto/isolate-bugs-with-bisect.txt b/Documentation/howto/isolate-bugs-with-bisect.txt new file mode 100644 index 0000000000..926bbdc3cb --- /dev/null +++ b/Documentation/howto/isolate-bugs-with-bisect.txt @@ -0,0 +1,65 @@ +From: Linus Torvalds <torvalds () osdl ! org> +To: git@vger.kernel.org +Date: 2005-11-08 1:31:34 +Subject: Real-life kernel debugging scenario +Abstract: Short-n-sweet, Linus tells us how to leverage `git-bisect` to perform + bug isolation on a repository where "good" and "bad" revisions are known + in order to identify a suspect commit. + + +How To Use git-bisect To Isolate a Bogus Commit +=============================================== + +The way to use "git bisect" couldn't be easier. + +Figure out what the oldest bad state you know about is (that's usually the +head of "master", since that's what you just tried to boot and failed at). +Also, figure out the most recent known-good commit (usually the _previous_ +kernel you ran: and if you've only done a single "pull" in between, it +will be ORIG_HEAD). + +Then do + + git bisect start + git bisect bad master <- mark "master" as the bad state + git bisect good ORIG_HEAD <- mark ORIG_HEAD as good (or + whatever other known-good + thing you booted last) + +and at this point "git bisect" will churn for a while, and tell you what +the mid-point between those two commits are, and check that state out as +the head of the new "bisect" branch. + +Compile and reboot. + +If it's good, just do + + git bisect good <- mark current head as good + +otherwise, reboot into a good kernel instead, and do (surprise surprise, +git really is very intuitive): + + git bisect bad <- mark current head as bad + +and whatever you do, git will select a new half-way point. Do this for a +while, until git tells you exactly which commit was the first bad commit. +That's your culprit. + +It really works wonderfully well, except for the case where there was +_another_ commit that broke something in between, like introduced some +stupid compile error. In that case you should not mark that commit good or +bad: you should try to find another commit close-by, and do a "git reset +--hard <newcommit>" to try out _that_ commit instead, and then test that +instead (and mark it good or bad). + +You can do "git bisect visualize" while you do all this to see what's +going on by starting up gitk on the bisection range. + +Finally, once you've figured out exactly which commit was bad, you can +then go back to the master branch, and try reverting just that commit: + + git checkout master + git revert <bad-commit-id> + +to verify that the top-of-kernel works with that single commit reverted. + diff --git a/Documentation/howto/make-dist.txt b/Documentation/howto/make-dist.txt new file mode 100644 index 0000000000..00e330b293 --- /dev/null +++ b/Documentation/howto/make-dist.txt @@ -0,0 +1,52 @@ +Date: Fri, 12 Aug 2005 22:39:48 -0700 (PDT) +From: Linus Torvalds <torvalds@osdl.org> +To: Dave Jones <davej@redhat.com> +cc: git@vger.kernel.org +Subject: Re: Fwd: Re: git checkout -f branch doesn't remove extra files +Abstract: In this article, Linus talks about building a tarball, + incremental patch, and ChangeLog, given a base release and two + rc releases, following the convention of giving the patch from + the base release and the latest rc, with ChangeLog between the + last rc and the latest rc. + +On Sat, 13 Aug 2005, Dave Jones wrote: +> +> > Git actually has a _lot_ of nifty tools. I didn't realize that people +> > didn't know about such basic stuff as "git-tar-tree" and "git-ls-files". +> +> Maybe its because things are moving so fast :) Or maybe I just wasn't +> paying attention on that day. (I even read the git changes via RSS, +> so I should have no excuse). + +Well, git-tar-tree has been there since late April - it's actually one of +those really early commands. I'm pretty sure the RSS feed came later ;) + +I use it all the time in doing releases, it's a lot faster than creating a +tar tree by reading the filesystem (even if you don't have to check things +out). A hidden pearl. + +This is my crappy "release-script": + + [torvalds@g5 ~]$ cat bin/release-script + #!/bin/sh + stable="$1" + last="$2" + new="$3" + echo "# git-tag v$new" + echo "git-tar-tree v$new linux-$new | gzip -9 > ../linux-$new.tar.gz" + echo "git-diff-tree -p v$stable v$new | gzip -9 > ../patch-$new.gz" + echo "git-rev-list --pretty v$new ^v$last > ../ChangeLog-$new" + echo "git-rev-list --pretty=short v$new ^v$last | git-shortlog > ../ShortLog" + echo "git-diff-tree -p v$last v$new | git-apply --stat > ../diffstat-$new" + +and when I want to do a new kernel release I literally first tag it, and +then do + + release-script 2.6.12 2.6.13-rc6 2.6.13-rc7 + +and check that things look sane, and then just cut-and-paste the commands. + +Yeah, it's stupid. + + Linus + diff --git a/Documentation/howto/rebase-and-edit.txt b/Documentation/howto/rebase-and-edit.txt new file mode 100644 index 0000000000..646c55cc69 --- /dev/null +++ b/Documentation/howto/rebase-and-edit.txt @@ -0,0 +1,81 @@ +Date: Sat, 13 Aug 2005 22:16:02 -0700 (PDT) +From: Linus Torvalds <torvalds@osdl.org> +To: Steve French <smfrench@austin.rr.com> +cc: git@vger.kernel.org +Subject: Re: sending changesets from the middle of a git tree +Abstract: In this article, Linus demonstrates how a broken commit + in a sequence of commits can be removed by rewinding the head and + reapplying selected changes. + +On Sat, 13 Aug 2005, Linus Torvalds wrote: + +> That's correct. Same things apply: you can move a patch over, and create a +> new one with a modified comment, but basically the _old_ commit will be +> immutable. + +Let me clarify. + +You can entirely _drop_ old branches, so commits may be immutable, but +nothing forces you to keep them. Of course, when you drop a commit, you'll +always end up dropping all the commits that depended on it, and if you +actually got somebody else to pull that commit you can't drop it from +_their_ repository, but undoing things is not impossible. + +For example, let's say that you've made a mess of things: you've committed +three commits "old->a->b->c", and you notice that "a" was broken, but you +want to save "b" and "c". What you can do is + + # Create a branch "broken" that is the current code + # for reference + git branch broken + + # Reset the main branch to three parents back: this + # effectively undoes the three top commits + git reset HEAD^^^ + git checkout -f + + # Check the result visually to make sure you know what's + # going on + gitk --all + + # Re-apply the two top ones from "broken" + # + # First "parent of broken" (aka b): + git-diff-tree -p broken^ | git-apply --index + git commit --reedit=broken^ + + # Then "top of broken" (aka c): + git-diff-tree -p broken | git-apply --index + git commit --reedit=broken + +and you've now re-applied (and possibly edited the comments) the two +commits b/c, and commit "a" is basically gone (it still exists in the +"broken" branch, of course). + +Finally, check out the end result again: + + # Look at the new commit history + gitk --all + +to see that everything looks sensible. + +And then, you can just remove the broken branch if you decide you really +don't want it: + + # remove 'broken' branch + git branch -d broken + + # Prune old objects if you're really really sure + git prune + +And yeah, I'm sure there are other ways of doing this. And as usual, the +above is totally untested, and I just wrote it down in this email, so if +I've done something wrong, you'll have to figure it out on your own ;) + + Linus +- +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 + + diff --git a/Documentation/howto/rebase-from-internal-branch.txt b/Documentation/howto/rebase-from-internal-branch.txt new file mode 100644 index 0000000000..3b3a5c2e69 --- /dev/null +++ b/Documentation/howto/rebase-from-internal-branch.txt @@ -0,0 +1,165 @@ +From: Junio C Hamano <junkio@cox.net> +To: git@vger.kernel.org +Cc: Petr Baudis <pasky@suse.cz>, Linus Torvalds <torvalds@osdl.org> +Subject: Re: sending changesets from the middle of a git tree +Date: Sun, 14 Aug 2005 18:37:39 -0700 +Abstract: In this article, JC talks about how he rebases the + public "pu" branch using the core GIT tools when he updates + the "master" branch, and how "rebase" works. Also discussed + is how this applies to individual developers who sends patches + upstream. + +Petr Baudis <pasky@suse.cz> writes: + +> Dear diary, on Sun, Aug 14, 2005 at 09:57:13AM CEST, I got a letter +> where Junio C Hamano <junkio@cox.net> told me that... +>> Linus Torvalds <torvalds@osdl.org> writes: +>> +>> > Junio, maybe you want to talk about how you move patches from your "pu" +>> > branch to the real branches. +>> +> Actually, wouldn't this be also precisely for what StGIT is intended to? + +Exactly my feeling. I was sort of waiting for Catalin to speak +up. With its basing philosophical ancestry on quilt, this is +the kind of task StGIT is designed to do. + +I just have done a simpler one, this time using only the core +GIT tools. + +I had a handful commits that were ahead of master in pu, and I +wanted to add some documentation bypassing my usual habit of +placing new things in pu first. At the beginning, the commit +ancestry graph looked like this: + + *"pu" head + master --> #1 --> #2 --> #3 + +So I started from master, made a bunch of edits, and committed: + + $ git checkout master + $ cd Documentation; ed git.txt ... + $ cd ..; git add Documentation/*.txt + $ git commit -s + +After the commit, the ancestry graph would look like this: + + *"pu" head + master^ --> #1 --> #2 --> #3 + \ + \---> master + +The old master is now master^ (the first parent of the master). +The new master commit holds my documentation updates. + +Now I have to deal with "pu" branch. + +This is the kind of situation I used to have all the time when +Linus was the maintainer and I was a contributor, when you look +at "master" branch being the "maintainer" branch, and "pu" +branch being the "contributor" branch. Your work started at the +tip of the "maintainer" branch some time ago, you made a lot of +progress in the meantime, and now the maintainer branch has some +other commits you do not have yet. And "git rebase" was written +with the explicit purpose of helping to maintain branches like +"pu". You _could_ merge master to pu and keep going, but if you +eventually want to cherrypick and merge some but not necessarily +all changes back to the master branch, it often makes later +operations for _you_ easier if you rebase (i.e. carry forward +your changes) "pu" rather than merge. So I ran "git rebase": + + $ git checkout pu + $ git rebase master pu + +What this does is to pick all the commits since the current +branch (note that I now am on "pu" branch) forked from the +master branch, and forward port these changes. + + master^ --> #1 --> #2 --> #3 + \ *"pu" head + \---> master --> #1' --> #2' --> #3' + +The diff between master^ and #1 is applied to master and +committed to create #1' commit with the commit information (log, +author and date) taken from commit #1. On top of that #2' and #3' +commits are made similarly out of #2 and #3 commits. + +Old #3 is not recorded in any of the .git/refs/heads/ file +anymore, so after doing this you will have dangling commit if +you ran fsck-cache, which is normal. After testing "pu", you +can run "git prune" to get rid of those original three commits. + +While I am talking about "git rebase", I should talk about how +to do cherrypicking using only the core GIT tools. + +Let's go back to the earlier picture, with different labels. + +You, as an individual developer, cloned upstream repository and +made a couple of commits on top of it. + + *your "master" head + upstream --> #1 --> #2 --> #3 + +You would want changes #2 and #3 incorporated in the upstream, +while you feel that #1 may need further improvements. So you +prepare #2 and #3 for e-mail submission. + + $ git format-patch master^^ master + +This creates two files, 0001-XXXX.patch and 0002-XXXX.patch. Send +them out "To: " your project maintainer and "Cc: " your mailing +list. You could use contributed script git-send-email if +your host has necessary perl modules for this, but your usual +MUA would do as long as it does not corrupt whitespaces in the +patch. + +Then you would wait, and you find out that the upstream picked +up your changes, along with other changes. + + where *your "master" head + upstream --> #1 --> #2 --> #3 + used \ + to be \--> #A --> #2' --> #3' --> #B --> #C + *upstream head + +The two commits #2' and #3' in the above picture record the same +changes your e-mail submission for #2 and #3 contained, but +probably with the new sign-off line added by the upstream +maintainer and definitely with different committer and ancestry +information, they are different objects from #2 and #3 commits. + +You fetch from upstream, but not merge. + + $ git fetch upstream + +This leaves the updated upstream head in .git/FETCH_HEAD but +does not touch your .git/HEAD nor .git/refs/heads/master. +You run "git rebase" now. + + $ git rebase FETCH_HEAD master + +Earlier, I said that rebase applies all the commits from your +branch on top of the upstream head. Well, I lied. "git rebase" +is a bit smarter than that and notices that #2 and #3 need not +be applied, so it only applies #1. The commit ancestry graph +becomes something like this: + + where *your old "master" head + upstream --> #1 --> #2 --> #3 + used \ your new "master" head* + to be \--> #A --> #2' --> #3' --> #B --> #C --> #1' + *upstream + head + +Again, "git prune" would discard the disused commits #1-#3 and +you continue on starting from the new "master" head, which is +the #1' commit. + +-jc + +- +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 + + diff --git a/Documentation/howto/rebuild-from-update-hook.txt b/Documentation/howto/rebuild-from-update-hook.txt new file mode 100644 index 0000000000..02621b54a0 --- /dev/null +++ b/Documentation/howto/rebuild-from-update-hook.txt @@ -0,0 +1,87 @@ +Subject: [HOWTO] Using post-update hook +Message-ID: <7vy86o6usx.fsf@assigned-by-dhcp.cox.net> +From: Junio C Hamano <junkio@cox.net> +Date: Fri, 26 Aug 2005 18:19:10 -0700 +Abstract: In this how-to article, JC talks about how he + uses the post-update hook to automate git documentation page + shown at http://www.kernel.org/pub/software/scm/git/docs/. + +The pages under http://www.kernel.org/pub/software/scm/git/docs/ +are built from Documentation/ directory of the git.git project +and needed to be kept up-to-date. The www.kernel.org/ servers +are mirrored and I was told that the origin of the mirror is on +the machine $some.kernel.org, on which I was given an account +when I took over git maintainership from Linus. + +The directories relevant to this how-to are these two: + + /pub/scm/git/git.git/ The public git repository. + /pub/software/scm/git/docs/ The HTML documentation page. + +So I made a repository to generate the documentation under my +home directory over there. + + $ cd + $ mkdir doc-git && cd doc-git + $ git clone /pub/scm/git/git.git/ docgen + +What needs to happen is to update the $HOME/doc-git/docgen/ +working tree, build HTML docs there and install the result in +/pub/software/scm/git/docs/ directory. So I wrote a little +script: + + $ cat >dododoc.sh <<\EOF + #!/bin/sh + cd $HOME/doc-git/docgen || exit + + unset GIT_DIR + + git pull /pub/scm/git/git.git/ master && + cd Documentation && + make install-webdoc + EOF + +Initially I used to run this by hand whenever I push into the +public git repository. Then I did a cron job that ran twice a +day. The current round uses the post-update hook mechanism, +like this: + + $ cat >/pub/scm/git/git.git/hooks/post-update <<\EOF + #!/bin/sh + # + # An example hook script to prepare a packed repository for use over + # dumb transports. + # + # To enable this hook, make this file executable by "chmod +x post-update". + + case " $* " in + *' refs/heads/master '*) + echo $HOME/doc-git/dododoc.sh | at now + ;; + esac + exec git-update-server-info + EOF + $ chmod +x /pub/scm/git/git.git/hooks/post-update + +There are four things worth mentioning: + + - The update-hook is run after the repository accepts a "git + push", under my user privilege. It is given the full names + of refs that have been updated as arguments. My post-update + runs the dododoc.sh script only when the master head is + updated. + + - When update-hook is run, GIT_DIR is set to '.' by the calling + receive-pack. This is inherited by the dododoc.sh run via + the "at" command, and needs to be unset; otherwise, "git + pull" it does into $HOME/doc-git/docgen/ repository would not + work correctly. + + - The stdout of update hook script is not connected to git + push; I run the heavy part of the command inside "at", to + receive the execution report via e-mail. + + - This is still crude and does not protect against simultaneous + make invocations stomping on each other. I would need to add + some locking mechanism for this. + diff --git a/Documentation/howto/revert-branch-rebase.txt b/Documentation/howto/revert-branch-rebase.txt new file mode 100644 index 0000000000..d10476b56e --- /dev/null +++ b/Documentation/howto/revert-branch-rebase.txt @@ -0,0 +1,200 @@ +From: Junio C Hamano <junkio@cox.net> +To: git@vger.kernel.org +Subject: [HOWTO] Reverting an existing commit +Abstract: In this article, JC gives a small real-life example of using + 'git revert' command, and using a temporary branch and tag for safety + and easier sanity checking. +Date: Mon, 29 Aug 2005 21:39:02 -0700 +Content-type: text/asciidoc +Message-ID: <7voe7g3uop.fsf@assigned-by-dhcp.cox.net> + +Reverting an existing commit +============================ + +One of the changes I pulled into the 'master' branch turns out to +break building GIT with GCC 2.95. While they were well intentioned +portability fixes, keeping things working with gcc-2.95 was also +important. Here is what I did to revert the change in the 'master' +branch and to adjust the 'pu' branch, using core GIT tools and +barebone Porcelain. + +First, prepare a throw-away branch in case I screw things up. + +------------------------------------------------ +$ git checkout -b revert-c99 master +------------------------------------------------ + +Now I am on the 'revert-c99' branch. Let's figure out which commit to +revert. I happen to know that the top of the 'master' branch is a +merge, and its second parent (i.e. foreign commit I merged from) has +the change I would want to undo. Further I happen to know that that +merge introduced 5 commits or so: + +------------------------------------------------ +$ git show-branch --more=4 master master^2 | head +* [master] Merge refs/heads/portable from http://www.cs.berkeley.... + ! [master^2] Replace C99 array initializers with code. +-- +- [master] Merge refs/heads/portable from http://www.cs.berkeley.... +*+ [master^2] Replace C99 array initializers with code. +*+ [master^2~1] Replace unsetenv() and setenv() with older putenv(). +*+ [master^2~2] Include sys/time.h in daemon.c. +*+ [master^2~3] Fix ?: statements. +*+ [master^2~4] Replace zero-length array decls with []. +* [master~1] tutorial note about git branch +------------------------------------------------ + +The '--more=4' above means "after we reach the merge base of refs, +show until we display four more common commits". That last commit +would have been where the "portable" branch was forked from the main +git.git repository, so this would show everything on both branches +since then. I just limited the output to the first handful using +'head'. + +Now I know 'master^2~4' (pronounce it as "find the second parent of +the 'master', and then go four generations back following the first +parent") is the one I would want to revert. Since I also want to say +why I am reverting it, the '-n' flag is given to 'git revert'. This +prevents it from actually making a commit, and instead 'git revert' +leaves the commit log message it wanted to use in '.msg' file: + +------------------------------------------------ +$ git revert -n master^2~4 +$ cat .msg +Revert "Replace zero-length array decls with []." + +This reverts 6c5f9baa3bc0d63e141e0afc23110205379905a4 commit. +$ git diff HEAD ;# to make sure what we are reverting makes sense. +$ make CC=gcc-2.95 clean test ;# make sure it fixed the breakage. +$ make clean test ;# make sure it did not cause other breakage. +------------------------------------------------ + +The reverted change makes sense (from reading the 'diff' output), does +fix the problem (from 'make CC=gcc-2.95' test), and does not cause new +breakage (from the last 'make test'). I'm ready to commit: + +------------------------------------------------ +$ git commit -a -s ;# read .msg into the log, + # and explain why I am reverting. +------------------------------------------------ + +I could have screwed up in any of the above steps, but in the worst +case I could just have done 'git checkout master' to start over. +Fortunately I did not have to; what I have in the current branch +'revert-c99' is what I want. So merge that back into 'master': + +------------------------------------------------ +$ git checkout master +$ git resolve master revert-c99 fast ;# this should be a fast forward +Updating from 10d781b9caa4f71495c7b34963bef137216f86a8 to e3a693c... + cache.h | 8 ++++---- + commit.c | 2 +- + ls-files.c | 2 +- + receive-pack.c | 2 +- + server-info.c | 2 +- + 5 files changed, 8 insertions(+), 8 deletions(-) +------------------------------------------------ + +The 'fast' in the above 'git resolve' is not a magic. I knew this +'resolve' would result in a fast forward merge, and if not, there is +something very wrong (so I would do 'git reset' on the 'master' branch +and examine the situation). When a fast forward merge is done, the +message parameter to 'git resolve' is discarded, because no new commit +is created. You could have said 'junk' or 'nothing' there as well. + +There is no need to redo the test at this point. We fast forwarded +and we know 'master' matches 'revert-c99' exactly. In fact: + +------------------------------------------------ +$ git diff master..revert-c99 +------------------------------------------------ + +says nothing. + +Then we rebase the 'pu' branch as usual. + +------------------------------------------------ +$ git checkout pu +$ git tag pu-anchor pu +$ git rebase master +* Applying: Redo "revert" using three-way merge machinery. +First trying simple merge strategy to cherry-pick. +Finished one cherry-pick. +* Applying: Remove git-apply-patch-script. +First trying simple merge strategy to cherry-pick. +Simple cherry-pick fails; trying Automatic cherry-pick. +Removing Documentation/git-apply-patch-script.txt +Removing git-apply-patch-script +Finished one cherry-pick. +* Applying: Document "git cherry-pick" and "git revert" +First trying simple merge strategy to cherry-pick. +Finished one cherry-pick. +* Applying: mailinfo and applymbox updates +First trying simple merge strategy to cherry-pick. +Finished one cherry-pick. +* Applying: Show commits in topo order and name all commits. +First trying simple merge strategy to cherry-pick. +Finished one cherry-pick. +* Applying: More documentation updates. +First trying simple merge strategy to cherry-pick. +Finished one cherry-pick. +------------------------------------------------ + +The temporary tag 'pu-anchor' is me just being careful, in case 'git +rebase' screws up. After this, I can do these for sanity check: + +------------------------------------------------ +$ git diff pu-anchor..pu ;# make sure we got the master fix. +$ make CC=gcc-2.95 clean test ;# make sure it fixed the breakage. +$ make clean test ;# make sure it did not cause other breakage. +------------------------------------------------ + +Everything is in the good order. I do not need the temporary branch +nor tag anymore, so remove them: + +------------------------------------------------ +$ rm -f .git/refs/tags/pu-anchor +$ git branch -d revert-c99 +------------------------------------------------ + +It was an emergency fix, so we might as well merge it into the +'release candidate' branch, although I expect the next release would +be some days off: + +------------------------------------------------ +$ git checkout rc +$ git pull . master +Packing 0 objects +Unpacking 0 objects + +* committish: e3a693c... refs/heads/master from . +Trying to merge e3a693c... into 8c1f5f0... using 10d781b... +Committed merge 7fb9b7262a1d1e0a47bbfdcbbcf50ce0635d3f8f + cache.h | 8 ++++---- + commit.c | 2 +- + ls-files.c | 2 +- + receive-pack.c | 2 +- + server-info.c | 2 +- + 5 files changed, 8 insertions(+), 8 deletions(-) +------------------------------------------------ + +And the final repository status looks like this: + +------------------------------------------------ +$ git show-branch --more=1 master pu rc +! [master] Revert "Replace zero-length array decls with []." + ! [pu] git-repack: Add option to repack all objects. + * [rc] Merge refs/heads/master from . +--- + + [pu] git-repack: Add option to repack all objects. + + [pu~1] More documentation updates. + + [pu~2] Show commits in topo order and name all commits. + + [pu~3] mailinfo and applymbox updates + + [pu~4] Document "git cherry-pick" and "git revert" + + [pu~5] Remove git-apply-patch-script. + + [pu~6] Redo "revert" using three-way merge machinery. + - [rc] Merge refs/heads/master from . +++* [master] Revert "Replace zero-length array decls with []." + - [rc~1] Merge refs/heads/master from . +... [master~1] Merge refs/heads/portable from http://www.cs.berkeley.... +------------------------------------------------ diff --git a/Documentation/howto/separating-topic-branches.txt b/Documentation/howto/separating-topic-branches.txt new file mode 100644 index 0000000000..090e2c9b01 --- /dev/null +++ b/Documentation/howto/separating-topic-branches.txt @@ -0,0 +1,91 @@ +From: Junio C Hamano <junkio@cox.net> +Subject: Separating topic branches +Abstract: In this article, JC describes how to separate topic branches. + +This text was originally a footnote to a discussion about the +behaviour of the git diff commands. + +Often I find myself doing that [running diff against something other +than HEAD] while rewriting messy development history. For example, I +start doing some work without knowing exactly where it leads, and end +up with a history like this: + + "master" + o---o + \ "topic" + o---o---o---o---o---o + +At this point, "topic" contains something I know I want, but it +contains two concepts that turned out to be completely independent. +And often, one topic component is larger than the other. It may +contain more than two topics. + +In order to rewrite this mess to be more manageable, I would first do +"diff master..topic", to extract the changes into a single patch, start +picking pieces from it to get logically self-contained units, and +start building on top of "master": + + $ git diff master..topic >P.diff + $ git checkout -b topicA master + ... pick and apply pieces from P.diff to build + ... commits on topicA branch. + + o---o---o + / "topicA" + o---o"master" + \ "topic" + o---o---o---o---o---o + +Before doing each commit on "topicA" HEAD, I run "diff HEAD" +before update-index the affected paths, or "diff --cached HEAD" +after. Also I would run "diff --cached master" to make sure +that the changes are only the ones related to "topicA". Usually +I do this for smaller topics first. + +After that, I'd do the remainder of the original "topic", but +for that, I do not start from the patchfile I extracted by +comparing "master" and "topic" I used initially. Still on +"topicA", I extract "diff topic", and use it to rebuild the +other topic: + + $ git diff -R topic >P.diff ;# --cached also would work fine + $ git checkout -b topicB master + ... pick and apply pieces from P.diff to build + ... commits on topicB branch. + + "topicB" + o---o---o---o---o + / + /o---o---o + |/ "topicA" + o---o"master" + \ "topic" + o---o---o---o---o---o + +After I am done, I'd try a pretend-merge between "topicA" and +"topicB" in order to make sure I have not missed anything: + + $ git pull . topicA ;# merge it into current "topicB" + $ git diff topic + "topicB" + o---o---o---o---o---* (pretend merge) + / / + /o---o---o----------' + |/ "topicA" + o---o"master" + \ "topic" + o---o---o---o---o---o + +The last diff better not to show anything other than cleanups +for crufts. Then I can finally clean things up: + + $ git branch -D topic + $ git reset --hard HEAD^ ;# nuke pretend merge + + "topicB" + o---o---o---o---o + / + /o---o---o + |/ "topicA" + o---o"master" + diff --git a/Documentation/howto/setup-git-server-over-http.txt b/Documentation/howto/setup-git-server-over-http.txt new file mode 100644 index 0000000000..8eadc20494 --- /dev/null +++ b/Documentation/howto/setup-git-server-over-http.txt @@ -0,0 +1,256 @@ +From: Rutger Nijlunsing <rutger@nospam.com> +Subject: Setting up a git repository which can be pushed into and pulled from over HTTP. +Date: Thu, 10 Aug 2006 22:00:26 +0200 + +Since Apache is one of those packages people like to compile +themselves while others prefer the bureaucrat's dream Debian, it is +impossible to give guidelines which will work for everyone. Just send +some feedback to the mailing list at git@vger.kernel.org to get this +document tailored to your favorite distro. + + +What's needed: + +- Have an Apache web-server + + On Debian: + $ apt-get install apache2 + To get apache2 by default started, + edit /etc/default/apache2 and set NO_START=0 + +- can edit the configuration of it. + + This could be found under /etc/httpd, or refer to your Apache documentation. + + On Debian: this means being able to edit files under /etc/apache2 + +- can restart it. + + 'apachectl --graceful' might do. If it doesn't, just stop and + restart apache. Be warning that active connections to your server + might be aborted by this. + + On Debian: + $ /etc/init.d/apache2 restart + or + $ /etc/init.d/apache2 force-reload + (which seems to do the same) + This adds symlinks from the /etc/apache2/mods-enabled to + /etc/apache2/mods-available. + +- have permissions to chown a directory + +- have git installed at the server _and_ client + +In effect, this probably means you're going to be root. + + +Step 1: setup a bare GIT repository +----------------------------------- + +At the time of writing, git-http-push cannot remotely create a GIT +repository. So we have to do that at the server side with git. Another +option would be to generate an empty repository at the client and copy +it to the server with WebDAV. But then you're probably the first to +try that out :) + +Create the directory under the DocumentRoot of the directories served +by Apache. As an example we take /usr/local/apache2, but try "grep +DocumentRoot /where/ever/httpd.conf" to find your root: + + $ cd /usr/local/apache/htdocs + $ mkdir my-new-repo.git + + On Debian: + + $ cd /var/www + $ mkdir my-new-repo.git + + +Initialize a bare repository + + $ cd my-new-repo.git + $ git --bare init + + +Change the ownership to your web-server's credentials. Use "grep ^User +httpd.conf" and "grep ^Group httpd.conf" to find out: + + $ chown -R www.www . + + On Debian: + + $ chown -R www-data.www-data . + + +If you do not know which user Apache runs as, you can alternatively do +a "chmod -R a+w .", inspect the files which are created later on, and +set the permissions appropriately. + +Restart apache2, and check whether http://server/my-new-repo.git gives +a directory listing. If not, check whether apache started up +successfully. + + +Step 2: enable DAV on this repository +------------------------------------- + +First make sure the dav_module is loaded. For this, insert in httpd.conf: + + LoadModule dav_module libexec/httpd/libdav.so + AddModule mod_dav.c + +Also make sure that this line exists which is the file used for +locking DAV operations: + + DAVLockDB "/usr/local/apache2/temp/DAV.lock" + + On Debian these steps can be performed with: + + Enable the dav and dav_fs modules of apache: + $ a2enmod dav_fs + (just to be sure. dav_fs might be unneeded, I don't know) + $ a2enmod dav + The DAV lock is located in /etc/apache2/mods-available/dav_fs.conf: + DAVLockDB /var/lock/apache2/DAVLock + +Of course, it can point somewhere else, but the string is actually just a +prefix in some Apache configurations, and therefore the _directory_ has to +be writable by the user Apache runs as. + +Then, add something like this to your httpd.conf + + <Location /my-new-repo.git> + DAV on + AuthType Basic + AuthName "Git" + AuthUserFile /usr/local/apache2/conf/passwd.git + Require valid-user + </Location> + + On Debian: + Create (or add to) /etc/apache2/conf.d/git.conf : + + <Location /my-new-repo.git> + DAV on + AuthType Basic + AuthName "Git" + AuthUserFile /etc/apache2/passwd.git + Require valid-user + </Location> + + Debian automatically reads all files under /etc/apach2/conf.d. + +The password file can be somewhere else, but it has to be readable by +Apache and preferably not readable by the world. + +Create this file by + $ htpasswd -c /usr/local/apache2/conf/passwd.git <user> + + On Debian: + $ htpasswd -c /etc/apache2/passwd.git <user> + +You will be asked a password, and the file is created. Subsequent calls +to htpasswd should omit the '-c' option, since you want to append to the +existing file. + +You need to restart Apache. + +Now go to http://<username>@<servername>/my-new-repo.git in your +browser to check whether it asks for a password and accepts the right +password. + +On Debian: + + To test the WebDAV part, do: + + $ apt-get install litmus + $ litmus http://<servername>/my-new-repo.git <username> <password> + + Most tests should pass. + +A command line tool to test WebDAV is cadaver. + +If you're into Windows, from XP onwards Internet Explorer supports +WebDAV. For this, do Internet Explorer -> Open Location -> +http://<servername>/my-new-repo.git [x] Open as webfolder -> login . + + +Step 3: setup the client +------------------------ + +Make sure that you have HTTP support, i.e. your git was built with curl. +The easiest way to check is to look for the executable 'git-http-push'. + +Then, add the following to your $HOME/.netrc (you can do without, but will be +asked to input your password a _lot_ of times): + + machine <servername> + login <username> + password <password> + +...and set permissions: + chmod 600 ~/.netrc + +If you want to access the web-server by its IP, you have to type that in, +instead of the server name. + +To check whether all is OK, do: + + curl --netrc --location -v http://<username>@<servername>/my-new-repo.git/ + +...this should give a directory listing in HTML of /var/www/my-new-repo.git . + + +Now, add the remote in your existing repository which contains the project +you want to export: + + $ git-config remote.upload.url \ + http://<username>@<servername>/my-new-repo.git/ + +It is important to put the last '/'; Without it, the server will send +a redirect which git-http-push does not (yet) understand, and git-http-push +will repeat the request infinitely. + + +Step 4: make the initial push +----------------------------- + +From your client repository, do + + $ git push upload master + +This pushes branch 'master' (which is assumed to be the branch you +want to export) to repository called 'upload', which we previously +defined with git-config. + + +Troubleshooting: +---------------- + +If git-http-push says + + Error: no DAV locking support on remote repo http://... + +then it means the web-server did not accept your authentication. Make sure +that the user name and password matches in httpd.conf, .netrc and the URL +you are uploading to. + +If git-http-push shows you an error (22/502) when trying to MOVE a blob, +it means that your web-server somehow does not recognize its name in the +request; This can happen when you start Apache, but then disable the +network interface. A simple restart of Apache helps. + +Errors like (22/502) are of format (curl error code/http error +code). So (22/404) means something like 'not found' at the server. + +Reading /usr/local/apache2/logs/error_log is often helpful. + + On Debian: Read /var/log/apache2/error.log instead. + + +Debian References: http://www.debian-administration.org/articles/285 + +Authors + Johannes Schindelin <Johannes.Schindelin@gmx.de> + Rutger Nijlunsing <git@wingding.demon.nl> diff --git a/Documentation/howto/update-hook-example.txt b/Documentation/howto/update-hook-example.txt new file mode 100644 index 0000000000..3a33696f00 --- /dev/null +++ b/Documentation/howto/update-hook-example.txt @@ -0,0 +1,172 @@ +From: Junio C Hamano <junkio@cox.net> and Carl Baldwin <cnb@fc.hp.com> +Subject: control access to branches. +Date: Thu, 17 Nov 2005 23:55:32 -0800 +Message-ID: <7vfypumlu3.fsf@assigned-by-dhcp.cox.net> +Abstract: An example hooks/update script is presented to + implement repository maintenance policies, such as who can push + into which branch and who can make a tag. + +When your developer runs git-push into the repository, +git-receive-pack is run (either locally or over ssh) as that +developer, so is hooks/update script. Quoting from the relevant +section of the documentation: + + Before each ref is updated, if $GIT_DIR/hooks/update file exists + and executable, it is called with three parameters: + + $GIT_DIR/hooks/update refname sha1-old sha1-new + + The refname parameter is relative to $GIT_DIR; e.g. for the + master head this is "refs/heads/master". Two sha1 are the + object names for the refname before and after the update. Note + that the hook is called before the refname is updated, so either + sha1-old is 0{40} (meaning there is no such ref yet), or it + should match what is recorded in refname. + +So if your policy is (1) always require fast-forward push +(i.e. never allow "git-push repo +branch:branch"), (2) you +have a list of users allowed to update each branch, and (3) you +do not let tags to be overwritten, then you can use something +like this as your hooks/update script. + +[jc: editorial note. This is a much improved version by Carl +since I posted the original outline] + +-- >8 -- beginning of script -- >8 -- + +#!/bin/bash + +umask 002 + +# If you are having trouble with this access control hook script +# you can try setting this to true. It will tell you exactly +# why a user is being allowed/denied access. + +verbose=false + +# Default shell globbing messes things up downstream +GLOBIGNORE=* + +function grant { + $verbose && echo >&2 "-Grant- $1" + echo grant + exit 0 +} + +function deny { + $verbose && echo >&2 "-Deny- $1" + echo deny + exit 1 +} + +function info { + $verbose && echo >&2 "-Info- $1" +} + +# Implement generic branch and tag policies. +# - Tags should not be updated once created. +# - Branches should only be fast-forwarded. +case "$1" in + refs/tags/*) + [ -f "$GIT_DIR/$1" ] && + deny >/dev/null "You can't overwrite an existing tag" + ;; + refs/heads/*) + # No rebasing or rewinding + if expr "$2" : '0*$' >/dev/null; then + info "The branch '$1' is new..." + else + # updating -- make sure it is a fast forward + mb=$(git-merge-base "$2" "$3") + case "$mb,$2" in + "$2,$mb") info "Update is fast-forward" ;; + *) deny >/dev/null "This is not a fast-forward update." ;; + esac + fi + ;; + *) + deny >/dev/null \ + "Branch is not under refs/heads or refs/tags. What are you trying to do?" + ;; +esac + +# Implement per-branch controls based on username +allowed_users_file=$GIT_DIR/info/allowed-users +username=$(id -u -n) +info "The user is: '$username'" + +if [ -f "$allowed_users_file" ]; then + rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' | + while read head_pattern user_patterns; do + matchlen=$(expr "$1" : "$head_pattern") + if [ "$matchlen" == "${#1}" ]; then + info "Found matching head pattern: '$head_pattern'" + for user_pattern in $user_patterns; do + info "Checking user: '$username' against pattern: '$user_pattern'" + matchlen=$(expr "$username" : "$user_pattern") + if [ "$matchlen" == "${#username}" ]; then + grant "Allowing user: '$username' with pattern: '$user_pattern'" + fi + done + deny "The user is not in the access list for this branch" + fi + done + ) + case "$rc" in + grant) grant >/dev/null "Granting access based on $allowed_users_file" ;; + deny) deny >/dev/null "Denying access based on $allowed_users_file" ;; + *) ;; + esac +fi + +allowed_groups_file=$GIT_DIR/info/allowed-groups +groups=$(id -G -n) +info "The user belongs to the following groups:" +info "'$groups'" + +if [ -f "$allowed_groups_file" ]; then + rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' | + while read head_pattern group_patterns; do + matchlen=$(expr "$1" : "$head_pattern") + if [ "$matchlen" == "${#1}" ]; then + info "Found matching head pattern: '$head_pattern'" + for group_pattern in $group_patterns; do + for groupname in $groups; do + info "Checking group: '$groupname' against pattern: '$group_pattern'" + matchlen=$(expr "$groupname" : "$group_pattern") + if [ "$matchlen" == "${#groupname}" ]; then + grant "Allowing group: '$groupname' with pattern: '$group_pattern'" + fi + done + done + deny "None of the user's groups are in the access list for this branch" + fi + done + ) + case "$rc" in + grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;; + deny) deny >/dev/null "Denying access based on $allowed_groups_file" ;; + *) ;; + esac +fi + +deny >/dev/null "There are no more rules to check. Denying access" + +-- >8 -- end of script -- >8 -- + +This uses two files, $GIT_DIR/info/allowed-users and +allowed-groups, to describe which heads can be pushed into by +whom. The format of each file would look like this: + + refs/heads/master junio + refs/heads/cogito$ pasky + refs/heads/bw/ linus + refs/heads/tmp/ * + refs/tags/v[0-9]* junio + +With this, Linus can push or create "bw/penguin" or "bw/zebra" +or "bw/panda" branches, Pasky can do only "cogito", and JC can +do master branch and make versioned tags. And anybody can do +tmp/blah branches. + +------------ diff --git a/Documentation/howto/using-topic-branches.txt b/Documentation/howto/using-topic-branches.txt new file mode 100644 index 0000000000..2c98194cb8 --- /dev/null +++ b/Documentation/howto/using-topic-branches.txt @@ -0,0 +1,296 @@ +Date: Mon, 15 Aug 2005 12:17:41 -0700 +From: tony.luck@intel.com +Subject: Some tutorial text (was git/cogito workshop/bof at linuxconf au?) +Abstract: In this article, Tony Luck discusses how he uses GIT + as a Linux subsystem maintainer. + +Here's something that I've been putting together on how I'm using +GIT as a Linux subsystem maintainer. + +-Tony + +Last updated w.r.t. GIT 1.1 + +Linux subsystem maintenance using GIT +------------------------------------- + +My requirements here are to be able to create two public trees: + +1) A "test" tree into which patches are initially placed so that they +can get some exposure when integrated with other ongoing development. +This tree is available to Andrew for pulling into -mm whenever he wants. + +2) A "release" tree into which tested patches are moved for final +sanity checking, and as a vehicle to send them upstream to Linus +(by sending him a "please pull" request.) + +Note that the period of time that each patch spends in the "test" tree +is dependent on the complexity of the change. Since GIT does not support +cherry picking, it is not practical to simply apply all patches to the +test tree and then pull to the release tree as that would leave trivial +patches blocked in the test tree waiting for complex changes to accumulate +enough test time to graduate. + +Back in the BitKeeper days I achieved this by creating small forests of +temporary trees, one tree for each logical grouping of patches, and then +pulling changes from these trees first to the test tree, and then to the +release tree. At first I replicated this in GIT, but then I realised +that I could so this far more efficiently using branches inside a single +GIT repository. + +So here is the step-by-step guide how this all works for me. + +First create your work tree by cloning Linus's public tree: + + $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git work + +Change directory into the cloned tree you just created + + $ cd work + +Set up a remotes file so that you can fetch the latest from Linus' master +branch into a local branch named "linus": + + $ cat > .git/remotes/linus + URL: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git + Pull: master:linus + ^D + +and create the linus branch: + + $ git branch linus + +The "linus" branch will be used to track the upstream kernel. To update it, +you simply run: + + $ git fetch linus + +you can do this frequently (and it should be safe to do so with pending +work in your tree, but perhaps not if you are in mid-merge). + +If you need to keep track of other public trees, you can add remote branches +for them too: + + $ git branch another + $ cat > .git/remotes/another + URL: ... insert URL here ... + Pull: name-of-branch-in-this-remote-tree:another + ^D + +and run: + + $ git fetch another + +Now create the branches in which you are going to work, these start +out at the current tip of the linus branch. + + $ git branch test linus + $ git branch release linus + +These can be easily kept up to date by merging from the "linus" branch: + + $ git checkout test && git merge "Auto-update from upstream" test linus + $ git checkout release && git merge "Auto-update from upstream" release linus + +Important note! If you have any local changes in these branches, then +this merge will create a commit object in the history (with no local +changes git will simply do a "Fast forward" merge). Many people dislike +the "noise" that this creates in the Linux history, so you should avoid +doing this capriciously in the "release" branch, as these noisy commits +will become part of the permanent history when you ask Linus to pull +from the release branch. + +Set up so that you can push upstream to your public tree (you need to +log-in to the remote system and create an empty tree there before the +first push). + + $ cat > .git/remotes/mytree + URL: master.kernel.org:/pub/scm/linux/kernel/git/aegl/linux-2.6.git + Push: release + Push: test + ^D + +and the push both the test and release trees using: + + $ git push mytree + +or push just one of the test and release branches using: + + $ git push mytree test +or + $ git push mytree release + +Now to apply some patches from the community. Think of a short +snappy name for a branch to hold this patch (or related group of +patches), and create a new branch from the current tip of the +linus branch: + + $ git checkout -b speed-up-spinlocks linus + +Now you apply the patch(es), run some tests, and commit the change(s). If +the patch is a multi-part series, then you should apply each as a separate +commit to this branch. + + $ ... patch ... test ... commit [ ... patch ... test ... commit ]* + +When you are happy with the state of this change, you can pull it into the +"test" branch in preparation to make it public: + + $ git checkout test && git merge "Pull speed-up-spinlock changes" test speed-up-spinlocks + +It is unlikely that you would have any conflicts here ... but you might if you +spent a while on this step and had also pulled new versions from upstream. + +Some time later when enough time has passed and testing done, you can pull the +same branch into the "release" tree ready to go upstream. This is where you +see the value of keeping each patch (or patch series) in its own branch. It +means that the patches can be moved into the "release" tree in any order. + + $ git checkout release && git merge "Pull speed-up-spinlock changes" release speed-up-spinlocks + +After a while, you will have a number of branches, and despite the +well chosen names you picked for each of them, you may forget what +they are for, or what status they are in. To get a reminder of what +changes are in a specific branch, use: + + $ git-whatchanged branchname ^linus | git-shortlog + +To see whether it has already been merged into the test or release branches +use: + + $ git-rev-list branchname ^test +or + $ git-rev-list branchname ^release + +[If this branch has not yet been merged you will see a set of SHA1 values +for the commits, if it has been merged, then there will be no output] + +Once a patch completes the great cycle (moving from test to release, then +pulled by Linus, and finally coming back into your local "linus" branch) +the branch for this change is no longer needed. You detect this when the +output from: + + $ git-rev-list branchname ^linus + +is empty. At this point the branch can be deleted: + + $ git branch -d branchname + +Some changes are so trivial that it is not necessary to create a separate +branch and then merge into each of the test and release branches. For +these changes, just apply directly to the "release" branch, and then +merge that into the "test" branch. + +To create diffstat and shortlog summaries of changes to include in a "please +pull" request to Linus you can use: + + $ git-whatchanged -p release ^linus | diffstat -p1 +and + $ git-whatchanged release ^linus | git-shortlog + + +Here are some of the scripts that I use to simplify all this even further. + +==== update script ==== +# Update a branch in my GIT tree. If the branch to be updated +# is "linus", then pull from kernel.org. Otherwise merge local +# linus branch into test|release branch + +case "$1" in +test|release) + git checkout $1 && git merge "Auto-update from upstream" $1 linus + ;; +linus) + before=$(cat .git/refs/heads/linus) + git fetch linus + after=$(cat .git/refs/heads/linus) + if [ $before != $after ] + then + git-whatchanged $after ^$before | git-shortlog + fi + ;; +*) + echo "Usage: $0 linus|test|release" 1>&2 + exit 1 + ;; +esac + +==== merge script ==== +# Merge a branch into either the test or release branch + +pname=$0 + +usage() +{ + echo "Usage: $pname branch test|release" 1>&2 + exit 1 +} + +if [ ! -f .git/refs/heads/"$1" ] +then + echo "Can't see branch <$1>" 1>&2 + usage +fi + +case "$2" in +test|release) + if [ $(git-rev-list $1 ^$2 | wc -c) -eq 0 ] + then + echo $1 already merged into $2 1>&2 + exit 1 + fi + git checkout $2 && git merge "Pull $1 into $2 branch" $2 $1 + ;; +*) + usage + ;; +esac + +==== status script ==== +# report on status of my ia64 GIT tree + +gb=$(tput setab 2) +rb=$(tput setab 1) +restore=$(tput setab 9) + +if [ `git-rev-list release ^test | wc -c` -gt 0 ] +then + echo $rb Warning: commits in release that are not in test $restore + git-whatchanged release ^test +fi + +for branch in `ls .git/refs/heads` +do + if [ $branch = linus -o $branch = test -o $branch = release ] + then + continue + fi + + echo -n $gb ======= $branch ====== $restore " " + status= + for ref in test release linus + do + if [ `git-rev-list $branch ^$ref | wc -c` -gt 0 ] + then + status=$status${ref:0:1} + fi + done + case $status in + trl) + echo $rb Need to pull into test $restore + ;; + rl) + echo "In test" + ;; + l) + echo "Waiting for linus" + ;; + "") + echo $rb All done $restore + ;; + *) + echo $rb "<$status>" $restore + ;; + esac + git-whatchanged $branch ^linus | git-shortlog +done diff --git a/Documentation/i18n.txt b/Documentation/i18n.txt new file mode 100644 index 0000000000..b4cbb3830e --- /dev/null +++ b/Documentation/i18n.txt @@ -0,0 +1,57 @@ +At the core level, git is character encoding agnostic. + + - The pathnames recorded in the index and in the tree objects + are treated as uninterpreted sequences of non-NUL bytes. + What readdir(2) returns are what are recorded and compared + with the data git keeps track of, which in turn are expected + to be what lstat(2) and creat(2) accepts. There is no such + thing as pathname encoding translation. + + - The contents of the blob objects are uninterpreted sequence + of bytes. There is no encoding translation at the core + level. + + - The commit log messages are uninterpreted sequence of non-NUL + bytes. + +Although we encourage that the commit log messages are encoded +in UTF-8, both the core and git Porcelain are designed not to +force UTF-8 on projects. If all participants of a particular +project find it more convenient to use legacy encodings, git +does not forbid it. However, there are a few things to keep in +mind. + +. `git-commit-tree` (hence, `git-commit` which uses it) issues + an warning if the commit log message given to it does not look + like a valid UTF-8 string, unless you explicitly say your + project uses a legacy encoding. The way to say this is to + have core.commitencoding in `.git/config` file, like this: ++ +------------ +[core] + commitencoding = ISO-8859-1 +------------ ++ +Commit objects created with the above setting record the value +of `core.commitencoding` in its `encoding` header. This is to +help other people who look at them later. Lack of this header +implies that the commit log message is encoded in UTF-8. + +. `git-log`, `git-show` and friends looks at the `encoding` + header of a commit object, and tries to re-code the log + message into UTF-8 unless otherwise specified. You can + specify the desired output encoding with + `core.logoutputencoding` in `.git/config` file, like this: ++ +------------ +[core] + logoutputencoding = ISO-8859-1 +------------ ++ +If you do not have this configuration variable, the value of +`core.commitencoding` is used instead. + +Note that we deliberately chose not to re-code the commit log +message when a commit is made to force UTF-8 at the commit +object level, because re-coding to UTF-8 is not necessarily a +reversible operation. diff --git a/Documentation/install-doc-quick.sh b/Documentation/install-doc-quick.sh new file mode 100755 index 0000000000..a64054948a --- /dev/null +++ b/Documentation/install-doc-quick.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# This requires a branch named in $head +# (usually 'man' or 'html', provided by the git.git repository) +set -e +head="$1" +mandir="$2" +SUBDIRECTORY_OK=t +USAGE='<refname> <target directory>' +. git-sh-setup +export GIT_DIR + +test -z "$mandir" && usage +if ! git-rev-parse --verify "$head^0" >/dev/null; then + echo >&2 "head: $head does not exist in the current repository" + usage +fi + +GIT_INDEX_FILE=`pwd`/.quick-doc.index +export GIT_INDEX_FILE +rm -f "$GIT_INDEX_FILE" +git-read-tree $head +git-checkout-index -a -f --prefix="$mandir"/ + +if test -n "$GZ"; then + cd "$mandir" + for i in `git-ls-tree -r --name-only $head` + do + gzip < $i > $i.gz && rm $i + done +fi +rm -f "$GIT_INDEX_FILE" diff --git a/Documentation/install-webdoc.sh b/Documentation/install-webdoc.sh new file mode 100755 index 0000000000..60211a5058 --- /dev/null +++ b/Documentation/install-webdoc.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +T="$1" + +for h in *.html *.txt howto/*.txt howto/*.html +do + if test -f "$T/$h" && + diff -u -I'Last updated [0-9][0-9]-[A-Z][a-z][a-z]-' "$T/$h" "$h" + then + :; # up to date + else + echo >&2 "# install $h $T/$h" + rm -f "$T/$h" + mkdir -p `dirname "$T/$h"` + cp "$h" "$T/$h" + fi +done +strip_leading=`echo "$T/" | sed -e 's|.|.|g'` +for th in "$T"/*.html "$T"/*.txt "$T"/howto/*.txt "$T"/howto/*.html +do + h=`expr "$th" : "$strip_leading"'\(.*\)'` + case "$h" in + index.html) continue ;; + esac + test -f "$h" && continue + echo >&2 "# rm -f $th" + rm -f "$th" +done +ln -sf git.html "$T/index.html" diff --git a/Documentation/merge-options.txt b/Documentation/merge-options.txt new file mode 100644 index 0000000000..182cef54be --- /dev/null +++ b/Documentation/merge-options.txt @@ -0,0 +1,24 @@ +-n, \--no-summary:: + Do not show diffstat at the end of the merge. + +--no-commit:: + Perform the merge but pretend the merge failed and do + not autocommit, to give the user a chance to inspect and + further tweak the merge result before committing. + +--squash:: + Produce the working tree and index state as if a real + merge happened, but do not actually make a commit or + move the `HEAD`, nor record `$GIT_DIR/MERGE_HEAD` to + cause the next `git commit` command to create a merge + commit. This allows you to create a single commit on + top of the current branch whose effect is the same as + merging another branch (or more in case of an octopus). + +-s <strategy>, \--strategy=<strategy>:: + Use the given merge strategy; can be supplied more than + once to specify them in the order they should be tried. + If there is no `-s` option, a built-in list of strategies + is used instead (`git-merge-recursive` when merging a single + head, `git-merge-octopus` otherwise). + diff --git a/Documentation/merge-strategies.txt b/Documentation/merge-strategies.txt new file mode 100644 index 0000000000..7df0266ba8 --- /dev/null +++ b/Documentation/merge-strategies.txt @@ -0,0 +1,35 @@ +MERGE STRATEGIES +---------------- + +resolve:: + This can only resolve two heads (i.e. the current branch + and another branch you pulled from) using 3-way merge + algorithm. It tries to carefully detect criss-cross + merge ambiguities and is considered generally safe and + fast. + +recursive:: + This can only resolve two heads using 3-way merge + algorithm. When there are more than one common + ancestors that can be used for 3-way merge, it creates a + merged tree of the common ancestors and uses that as + the reference tree for the 3-way merge. This has been + reported to result in fewer merge conflicts without + causing mis-merges by tests done on actual merge commits + taken from Linux 2.6 kernel development history. + Additionally this can detect and handle merges involving + renames. This is the default merge strategy when + pulling or merging one branch. + +octopus:: + This resolves more than two-head case, but refuses to do + complex merge that needs manual resolution. It is + primarily meant to be used for bundling topic branch + heads together. This is the default merge strategy when + pulling or merging more than one branches. + +ours:: + This resolves any number of heads, but the result of the + merge is always the current branch head. It is meant to + be used to supersede old development history of side + branches. diff --git a/Documentation/pretty-formats.txt b/Documentation/pretty-formats.txt new file mode 100644 index 0000000000..fb0b0b9582 --- /dev/null +++ b/Documentation/pretty-formats.txt @@ -0,0 +1,85 @@ +--pretty[='<format>']:: + + Pretty-prints the details of a commit. `--pretty` + without an explicit `=<format>` defaults to 'medium'. + If the commit is a merge, and if the pretty-format + is not 'oneline', 'email' or 'raw', an additional line is + inserted before the 'Author:' line. This line begins with + "Merge: " and the sha1s of ancestral commits are printed, + separated by spaces. Note that the listed commits may not + necessarily be the list of the *direct* parent commits if you + have limited your view of history: for example, if you are + only interested in changes related to a certain directory or + file. Here are some additional details for each format: + + * 'oneline' + + <sha1> <title line> ++ +This is designed to be as compact as possible. + + * 'short' + + commit <sha1> + Author: <author> + + <title line> + + * 'medium' + + commit <sha1> + Author: <author> + Date: <date> + + <title line> + + <full commit message> + + * 'full' + + commit <sha1> + Author: <author> + Commit: <committer> + + <title line> + + <full commit message> + + * 'fuller' + + commit <sha1> + Author: <author> + AuthorDate: <date & time> + Commit: <committer> + CommitDate: <date & time> + + <title line> + + <full commit message> + + + * 'email' + + From <sha1> <date> + From: <author> + Date: <date & time> + Subject: [PATCH] <title line> + + full commit message> + + + * 'raw' ++ +The 'raw' format shows the entire commit exactly as +stored in the commit object. Notably, the SHA1s are +displayed in full, regardless of whether --abbrev or +--no-abbrev are used, and 'parents' information show the +true parent commits, without taking grafts nor history +simplification into account. + +--encoding[=<encoding>]:: + The commit objects record the encoding used for the log message + in their encoding header; this option can be used to tell the + command to re-code the commit log message in the encoding + preferred by the user. For non plumbing commands this + defaults to UTF-8. diff --git a/Documentation/pull-fetch-param.txt b/Documentation/pull-fetch-param.txt new file mode 100644 index 0000000000..8d4e950abc --- /dev/null +++ b/Documentation/pull-fetch-param.txt @@ -0,0 +1,65 @@ +<repository>:: + The "remote" repository that is the source of a fetch + or pull operation. See the section <<URLS,GIT URLS>> below. + +<refspec>:: + The canonical format of a <refspec> parameter is + `+?<src>:<dst>`; that is, an optional plus `+`, followed + by the source ref, followed by a colon `:`, followed by + the destination ref. ++ +The remote ref that matches <src> +is fetched, and if <dst> is not empty string, the local +ref that matches it is fast forwarded using <src>. +Again, if the optional plus `+` is used, the local ref +is updated even if it does not result in a fast forward +update. ++ +[NOTE] +If the remote branch from which you want to pull is +modified in non-linear ways such as being rewound and +rebased frequently, then a pull will attempt a merge with +an older version of itself, likely conflict, and fail. +It is under these conditions that you would want to use +the `+` sign to indicate non-fast-forward updates will +be needed. There is currently no easy way to determine +or declare that a branch will be made available in a +repository with this behavior; the pulling user simply +must know this is the expected usage pattern for a branch. ++ +[NOTE] +You never do your own development on branches that appear +on the right hand side of a <refspec> colon on `Pull:` lines; +they are to be updated by `git-fetch`. If you intend to do +development derived from a remote branch `B`, have a `Pull:` +line to track it (i.e. `Pull: B:remote-B`), and have a separate +branch `my-B` to do your development on top of it. The latter +is created by `git branch my-B remote-B` (or its equivalent `git +checkout -b my-B remote-B`). Run `git fetch` to keep track of +the progress of the remote side, and when you see something new +on the remote branch, merge it into your development branch with +`git pull . remote-B`, while you are on `my-B` branch. ++ +[NOTE] +There is a difference between listing multiple <refspec> +directly on `git-pull` command line and having multiple +`Pull:` <refspec> lines for a <repository> and running +`git-pull` command without any explicit <refspec> parameters. +<refspec> listed explicitly on the command line are always +merged into the current branch after fetching. In other words, +if you list more than one remote refs, you would be making +an Octopus. While `git-pull` run without any explicit <refspec> +parameter takes default <refspec>s from `Pull:` lines, it +merges only the first <refspec> found into the current branch, +after fetching all the remote refs. This is because making an +Octopus from remote refs is rarely done, while keeping track +of multiple remote heads in one-go by fetching more than one +is often useful. ++ +Some short-cut notations are also supported. ++ +* `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`; + it requests fetching everything up to the given tag. +* A parameter <ref> without a colon is equivalent to + <ref>: when pulling/fetching, so it merges <ref> into the current + branch without storing the remote branch anywhere locally diff --git a/Documentation/repository-layout.txt b/Documentation/repository-layout.txt new file mode 100644 index 0000000000..863cb6710a --- /dev/null +++ b/Documentation/repository-layout.txt @@ -0,0 +1,181 @@ +git repository layout +===================== + +You may find these things in your git repository (`.git` +directory for a repository associated with your working tree, or +`'project'.git` directory for a public 'bare' repository). + +objects:: + Object store associated with this repository. Usually + an object store is self sufficient (i.e. all the objects + that are referred to by an object found in it are also + found in it), but there are couple of ways to violate + it. ++ +. You could populate the repository by running a commit walker +without `-a` option. Depending on which options are given, you +could have only commit objects without associated blobs and +trees this way, for example. A repository with this kind of +incomplete object store is not suitable to be published to the +outside world but sometimes useful for private repository. +. You also could have an incomplete but locally usable repository +by cloning shallowly. See gitlink:git-clone[1]. +. You can be using `objects/info/alternates` mechanism, or +`$GIT_ALTERNATE_OBJECT_DIRECTORIES` mechanism to 'borrow' +objects from other object stores. A repository with this kind +of incomplete object store is not suitable to be published for +use with dumb transports but otherwise is OK as long as +`objects/info/alternates` points at the right object stores +it borrows from. + +objects/[0-9a-f][0-9a-f]:: + Traditionally, each object is stored in its own file. + They are split into 256 subdirectories using the first + two letters from its object name to keep the number of + directory entries `objects` directory itself needs to + hold. Objects found here are often called 'unpacked' + (or 'loose') objects. + +objects/pack:: + Packs (files that store many object in compressed form, + along with index files to allow them to be randomly + accessed) are found in this directory. + +objects/info:: + Additional information about the object store is + recorded in this directory. + +objects/info/packs:: + This file is to help dumb transports discover what packs + are available in this object store. Whenever a pack is + added or removed, `git update-server-info` should be run + to keep this file up-to-date if the repository is + published for dumb transports. `git repack` does this + by default. + +objects/info/alternates:: + This file records paths to alternate object stores that + this object store borrows objects from, one pathname per + line. Note that not only native Git tools use it locally, + but the HTTP fetcher also tries to use it remotely; this + will usually work if you have relative paths (relative + to the object database, not to the repository!) in your + alternates file, but it will not work if you use absolute + paths unless the absolute path in filesystem and web URL + is the same. See also 'objects/info/http-alternates'. + +objects/info/http-alternates:: + This file records URLs to alternate object stores that + this object store borrows objects from, to be used when + the repository is fetched over HTTP. + +refs:: + References are stored in subdirectories of this + directory. The `git prune` command knows to keep + objects reachable from refs found in this directory and + its subdirectories. + +refs/heads/`name`:: + records tip-of-the-tree commit objects of branch `name` + +refs/tags/`name`:: + records any object name (not necessarily a commit + object, or a tag object that points at a commit object). + +refs/remotes/`name`:: + records tip-of-the-tree commit objects of branches copied + from a remote repository. + +packed-refs:: + records the same information as refs/heads/, refs/tags/, + and friends record in a more efficient way. See + gitlink:git-pack-refs[1]. + +HEAD:: + A symref (see glossary) to the `refs/heads/` namespace + describing the currently active branch. It does not mean + much if the repository is not associated with any working tree + (i.e. a 'bare' repository), but a valid git repository + *must* have the HEAD file; some porcelains may use it to + guess the designated "default" branch of the repository + (usually 'master'). It is legal if the named branch + 'name' does not (yet) exist. In some legacy setups, it is + a symbolic link instead of a symref that points at the current + branch. ++ +HEAD can also record a specific commit directly, instead of +being a symref to point at the current branch. Such a state +is often called 'detached HEAD', and almost all commands work +identically as normal. See gitlink:git-checkout[1] for +details. + +branches:: + A slightly deprecated way to store shorthands to be used + to specify URL to `git fetch`, `git pull` and `git push` + commands is to store a file in `branches/'name'` and + give 'name' to these commands in place of 'repository' + argument. + +hooks:: + Hooks are customization scripts used by various git + commands. A handful of sample hooks are installed when + `git init` is run, but all of them are disabled by + default. To enable, they need to be made executable. + Read link:hooks.html[hooks] for more details about + each hook. + +index:: + The current index file for the repository. It is + usually not found in a bare repository. + +info:: + Additional information about the repository is recorded + in this directory. + +info/refs:: + This file is to help dumb transports to discover what + refs are available in this repository. Whenever you + create/delete a new branch or a new tag, `git + update-server-info` should be run to keep this file + up-to-date if the repository is published for dumb + transports. The `git-receive-pack` command, which is + run on a remote repository when you `git push` into it, + runs `hooks/update` hook to help you achieve this. + +info/grafts:: + This file records fake commit ancestry information, to + pretend the set of parents a commit has is different + from how the commit was actually created. One record + per line describes a commit and its fake parents by + listing their 40-byte hexadecimal object names separated + by a space and terminated by a newline. + +info/exclude:: + This file, by convention among Porcelains, stores the + exclude pattern list. `.gitignore` is the per-directory + ignore file. `git status`, `git add`, `git rm` and `git + clean` look at it but the core git commands do not look + at it. See also: gitlink:git-ls-files[1] `--exclude-from` + and `--exclude-per-directory`. + +remotes:: + 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`. + +shallow:: + This is similar to `info/grafts` but is internally used + and maintained by shallow clone mechanism. See `--depth` + option to gitlink:git-clone[1] and gitlink:git-fetch[1]. + diff --git a/Documentation/sort_glossary.pl b/Documentation/sort_glossary.pl new file mode 100644 index 0000000000..e0bc552a64 --- /dev/null +++ b/Documentation/sort_glossary.pl @@ -0,0 +1,69 @@ +#!/usr/bin/perl + +%terms=(); + +while(<>) { + if(/^(\S.*)::$/) { + my $term=$1; + if(defined($terms{$term})) { + die "$1 defined twice\n"; + } + $terms{$term}=""; + LOOP: while(<>) { + if(/^$/) { + last LOOP; + } + if(/^ \S/) { + $terms{$term}.=$_; + } else { + die "Error 1: $_"; + } + } + } +} + +sub format_tab_80 ($) { + my $text=$_[0]; + my $result=""; + $text=~s/\s+/ /g; + $text=~s/^\s+//; + while($text=~/^(.{1,72})(|\s+(\S.*)?)$/) { + $result.=" ".$1."\n"; + $text=$3; + } + return $result; +} + +sub no_spaces ($) { + my $result=$_[0]; + $result=~tr/ /_/; + return $result; +} + +print 'GIT Glossary +============ + +This list is sorted alphabetically: + +'; + +@keys=sort {uc($a) cmp uc($b)} keys %terms; +$pattern='(\b(?<!link:git-)'.join('\b|\b(?<!link:git-)',reverse @keys).'\b)'; +foreach $key (@keys) { + $terms{$key}=~s/$pattern/sprintf "<<ref_".no_spaces($1).",$1>>";/eg; + print '[[ref_'.no_spaces($key).']]'.$key."::\n" + .format_tab_80($terms{$key})."\n"; +} + +print ' + +Author +------ +Written by Johannes Schindelin <Johannes.Schindelin@gmx.de> and +the git-list <git@vger.kernel.org>. + +GIT +--- +Part of the link:git.html[git] suite +'; + diff --git a/Documentation/technical/pack-format.txt b/Documentation/technical/pack-format.txt new file mode 100644 index 0000000000..0e1ffb2427 --- /dev/null +++ b/Documentation/technical/pack-format.txt @@ -0,0 +1,116 @@ +GIT pack format +=============== + += pack-*.pack file has the following format: + + - The header appears at the beginning and consists of the following: + + 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 + more than 4G objects in a pack. + + - The header is followed by number of object entries, each of + which looks like this: + + (undeltified representation) + n-byte type and length (4-bit type, (n-1)*7+4-bit length) + compressed data + + (deltified representation) + n-byte type and length (4-bit type, (n-1)*7+4-bit length) + 20-byte base object name + compressed delta data + + Observation: length of each object is encoded in a variable + length format and is not constrained to 32-bit or anything. + + - The trailer records 20-byte SHA1 checksum of all of the above. + += pack-*.idx file has the following format: + + - The header consists of 256 4-byte network byte order + integers. N-th entry of this table records the number of + objects in the corresponding pack, the first byte of whose + object name are smaller than N. This is called the + 'first-level fan-out' table. + + Observation: we would need to extend this to an array of + 8-byte integers to go beyond 4G objects per pack, but it is + not strictly necessary. + + - 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 + object is stored in the packfile as the offset from the + beginning. + + 20-byte object name. + + Observation: we would definitely need to extend this to + 8-byte integer plus 20-byte object name to handle a packfile + that is larger than 4GB. + + - The file is concluded with a trailer: + + A copy of the 20-byte SHA1 checksum at the end of + corresponding packfile. + + 20-byte SHA1-checksum of all of the above. + +Pack Idx file: + + idx + +--------------------------------+ + | fanout[0] = 2 |-. + +--------------------------------+ | + | fanout[1] | | + +--------------------------------+ | + | fanout[2] | | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | + | fanout[255] | | + +--------------------------------+ | +main | offset | | +index | object name 00XXXXXXXXXXXXXXXX | | +table +--------------------------------+ | + | offset | | + | object name 00XXXXXXXXXXXXXXXX | | + +--------------------------------+ | + .-| offset |<+ + | | object name 01XXXXXXXXXXXXXXXX | + | +--------------------------------+ + | | offset | + | | object name 01XXXXXXXXXXXXXXXX | + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + | | offset | + | | object name FFXXXXXXXXXXXXXXXX | + | +--------------------------------+ +trailer | | packfile checksum | + | +--------------------------------+ + | | idxfile checksum | + | +--------------------------------+ + .-------. + | +Pack file entry: <+ + + packed object header: + 1-byte type (upper 4-bit) + size0 (lower 4-bit) + n-byte sizeN (as long as MSB is set, each 7-bit) + size0..sizeN form 4+7+7+..+7 bit integer, size0 + is the most significant part. + packed object data: + If it is not DELTA, then deflated bytes (the size above + is the size before compression). + If it is DELTA, then + 20-byte base object name SHA1 (the size above is the + size of the delta data that follows). + delta data, deflated. diff --git a/Documentation/technical/pack-heuristics.txt b/Documentation/technical/pack-heuristics.txt new file mode 100644 index 0000000000..103eb5d989 --- /dev/null +++ b/Documentation/technical/pack-heuristics.txt @@ -0,0 +1,466 @@ + Concerning Git's Packing Heuristics + =================================== + + Oh, here's a really stupid question: + + Where do I go + to learn the details + of git's packing heuristics? + +Be careful what you ask! + +Followers of the git, please open the git IRC Log and turn to +February 10, 2006. + +It's a rare occasion, and we are joined by the King Git Himself, +Linus Torvalds (linus). Nathaniel Smith, (njs`), has the floor +and seeks enlightenment. Others are present, but silent. + +Let's listen in! + + <njs`> Oh, here's a really stupid question -- where do I go to + learn the details of git's packing heuristics? google avails + me not, reading the source didn't help a lot, and wading + through the whole mailing list seems less efficient than any + of that. + +It is a bold start! A plea for help combined with a simultaneous +tri-part attack on some of the tried and true mainstays in the quest +for enlightenment. Brash accusations of google being useless. Hubris! +Maligning the source. Heresy! Disdain for the mailing list archives. +Woe. + + <pasky> yes, the packing-related delta stuff is somewhat + mysterious even for me ;) + +Ah! Modesty after all. + + <linus> njs, I don't think the docs exist. That's something where + I don't think anybody else than me even really got involved. + Most of the rest of git others have been busy with (especially + Junio), but packing nobody touched after I did it. + +It's cryptic, yet vague. Linus in style for sure. Wise men +interpret this as an apology. A few argue it is merely a +statement of fact. + + <njs`> I guess the next step is "read the source again", but I + have to build up a certain level of gumption first :-) + +Indeed! On both points. + + <linus> The packing heuristic is actually really really simple. + +Bait... + + <linus> But strange. + +And switch. That ought to do it! + + <linus> Remember: git really doesn't follow files. So what it does is + - generate a list of all objects + - sort the list according to magic heuristics + - walk the list, using a sliding window, seeing if an object + can be diffed against another object in the window + - write out the list in recency order + +The traditional understatement: + + <njs`> I suspect that what I'm missing is the precise definition of + the word "magic" + +The traditional insight: + + <pasky> yes + +And Babel-like confusion flowed. + + <njs`> oh, hmm, and I'm not sure what this sliding window means either + + <pasky> iirc, it appeared to me to be just the sha1 of the object + when reading the code casually ... + + ... which simply doesn't sound as a very good heuristics, though ;) + + <njs`> .....and recency order. okay, I think it's clear I didn't + even realize how much I wasn't realizing :-) + +Ah, grasshopper! And thus the enlightenment begins anew. + + <linus> The "magic" is actually in theory totally arbitrary. + ANY order will give you a working pack, but no, it's not + ordered by SHA1. + + Before talking about the ordering for the sliding delta + window, let's talk about the recency order. That's more + important in one way. + + <njs`> Right, but if all you want is a working way to pack things + together, you could just use cat and save yourself some + trouble... + +Waaait for it.... + + <linus> The recency ordering (which is basically: put objects + _physically_ into the pack in the order that they are + "reachable" from the head) is important. + + <njs`> okay + + <linus> It's important because that's the thing that gives packs + good locality. It keeps the objects close to the head (whether + they are old or new, but they are _reachable_ from the head) + at the head of the pack. So packs actually have absolutely + _wonderful_ IO patterns. + +Read that again, because it is important. + + <linus> But recency ordering is totally useless for deciding how + to actually generate the deltas, so the delta ordering is + something else. + + The delta ordering is (wait for it): + - first sort by the "basename" of the object, as defined by + the name the object was _first_ reached through when + generating the object list + - within the same basename, sort by size of the object + - but always sort different types separately (commits first). + + That's not exactly it, but it's very close. + + <njs`> The "_first_ reached" thing is not too important, just you + need some way to break ties since the same objects may be + reachable many ways, yes? + +And as if to clarify: + + <linus> The point is that it's all really just any random + heuristic, and the ordering is totally unimportant for + correctness, but it helps a lot if the heuristic gives + "clumping" for things that are likely to delta well against + each other. + +It is an important point, so secretly, I did my own research and have +included my results below. To be fair, it has changed some over time. +And through the magic of Revisionistic History, I draw upon this entry +from The Git IRC Logs on my father's birthday, March 1: + + <gitster> The quote from the above linus should be rewritten a + bit (wait for it): + - first sort by type. Different objects never delta with + each other. + - then sort by filename/dirname. hash of the basename + occupies the top BITS_PER_INT-DIR_BITS bits, and bottom + DIR_BITS are for the hash of leading path elements. + - then if we are doing "thin" pack, the objects we are _not_ + going to pack but we know about are sorted earlier than + other objects. + - and finally sort by size, larger to smaller. + +In one swell-foop, clarification and obscurification! Nonetheless, +authoritative. Cryptic, yet concise. It even solicits notions of +quotes from The Source Code. Clearly, more study is needed. + + <gitster> That's the sort order. What this means is: + - we do not delta different object types. + - we prefer to delta the objects with the same full path, but + allow files with the same name from different directories. + - we always prefer to delta against objects we are not going + to send, if there are some. + - we prefer to delta against larger objects, so that we have + lots of removals. + + The penultimate rule is for "thin" packs. It is used when + the other side is known to have such objects. + +There it is again. "Thin" packs. I'm thinking to myself, "What +is a 'thin' pack?" So I ask: + + <jdl> What is a "thin" pack? + + <gitster> Use of --objects-edge to rev-list as the upstream of + pack-objects. The pack transfer protocol negotiates that. + +Woo hoo! Cleared that _right_ up! + + <gitster> There are two directions - push and fetch. + +There! Did you see it? It is not '"push" and "pull"'! How often the +confusion has started here. So casually mentioned, too! + + <gitster> For push, git-send-pack invokes git-receive-pack on the + other end. The receive-pack says "I have up to these commits". + send-pack looks at them, and computes what are missing from + the other end. So "thin" could be the default there. + + In the other direction, fetch, git-fetch-pack and + git-clone-pack invokes git-upload-pack on the other end + (via ssh or by talking to the daemon). + + There are two cases: fetch-pack with -k and clone-pack is one, + fetch-pack without -k is the other. clone-pack and fetch-pack + with -k will keep the downloaded packfile without expanded, so + we do not use thin pack transfer. Otherwise, the generated + pack will have delta without base object in the same pack. + + But fetch-pack without -k will explode the received pack into + individual objects, so we automatically ask upload-pack to + give us a thin pack if upload-pack supports it. + +OK then. + +Uh. + +Let's return to the previous conversation still in progress. + + <njs`> and "basename" means something like "the tail of end of + path of file objects and dir objects, as per basename(3), and + we just declare all commit and tag objects to have the same + basename" or something? + +Luckily, that too is a point that gitster clarified for us! + +If I might add, the trick is to make files that _might_ be similar be +located close to each other in the hash buckets based on their file +names. It used to be that "foo/Makefile", "bar/baz/quux/Makefile" and +"Makefile" all landed in the same bucket due to their common basename, +"Makefile". However, now they land in "close" buckets. + +The algorithm allows not just for the _same_ bucket, but for _close_ +buckets to be considered delta candidates. The rationale is +essentially that files, like Makefiles, often have very similar +content no matter what directory they live in. + + <linus> I played around with different delta algorithms, and with + making the "delta window" bigger, but having too big of a + sliding window makes it very expensive to generate the pack: + you need to compare every object with a _ton_ of other objects. + + There are a number of other trivial heuristics too, which + basically boil down to "don't bother even trying to delta this + pair" if we can tell before-hand that the delta isn't worth it + (due to size differences, where we can take a previous delta + result into account to decide that "ok, no point in trying + that one, it will be worse"). + + End result: packing is actually very size efficient. It's + somewhat CPU-wasteful, but on the other hand, since you're + really only supposed to do it maybe once a month (and you can + do it during the night), nobody really seems to care. + +Nice Engineering Touch, there. Find when it doesn't matter, and +proclaim it a non-issue. Good style too! + + <njs`> So, just to repeat to see if I'm following, we start by + getting a list of the objects we want to pack, we sort it by + this heuristic (basically lexicographically on the tuple + (type, basename, size)). + + Then we walk through this list, and calculate a delta of + each object against the last n (tunable parameter) objects, + and pick the smallest of these deltas. + +Vastly simplified, but the essence is there! + + <linus> Correct. + + <njs`> And then once we have picked a delta or fulltext to + represent each object, we re-sort by recency, and write them + out in that order. + + <linus> Yup. Some other small details: + +And of course there is the "Other Shoe" Factor too. + + <linus> - We limit the delta depth to another magic value (right + now both the window and delta depth magic values are just "10") + + <njs`> Hrm, my intuition is that you'd end up with really _bad_ IO + patterns, because the things you want are near by, but to + actually reconstruct them you may have to jump all over in + random ways. + + <linus> - When we write out a delta, and we haven't yet written + out the object it is a delta against, we write out the base + object first. And no, when we reconstruct them, we actually + get nice IO patterns, because: + - larger objects tend to be "more recent" (Linus' law: files grow) + - 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 + (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 +important. Let's make sure we clarify some of the points here: + + <njs`> So the point is just that in practice, delta order and + recency order match each other quite well. + + <linus> Yes. There's another nice side to this (and yes, it was + designed that way ;): + - the reason we generate deltas against the larger object is + actually a big space saver too! + + <njs`> Hmm, but your last comment (if "we haven't yet written out + the object it is a delta against, we write out the base object + first"), seems like it would make these facts mostly + irrelevant because even if in practice you would not have to + wander around much, in fact you just brute-force say that in + the cases where you might have to wander, don't do that :-) + + <linus> Yes and no. Notice the rule: we only write out the base + object first if the delta against it was more recent. That + means that you can actually have deltas that refer to a base + object that is _not_ close to the delta object, but that only + happens when the delta is needed to generate an _old_ object. + + <linus> See? + +Yeah, no. I missed that on the first two or three readings myself. + + <linus> This keeps the front of the pack dense. The front of the + pack never contains data that isn't relevant to a "recent" + object. The size optimization comes from our use of xdelta + (but is true for many other delta algorithms): removing data + is cheaper (in size) than adding data. + + When you remove data, you only need to say "copy bytes n--m". + In contrast, in a delta that _adds_ data, you have to say "add + these bytes: 'actual data goes here'" + + *** njs` has quit: Read error: 104 (Connection reset by peer) + + <linus> Uhhuh. I hope I didn't blow njs` mind. + + *** njs` has joined channel #git + + <pasky> :) + +The silent observers are amused. Of course. + +And as if njs` was expected to be omniscient: + + <linus> njs - did you miss anything? + +OK, I'll spell it out. That's Geek Humor. If njs` was not actually +connected for a little bit there, how would he know if missed anything +while he was disconnected? He's a benevolent dictator with a sense of +humor! Well noted! + + <njs`> Stupid router. Or gremlins, or whatever. + +It's a cheap shot at Cisco. Take 'em when you can. + + <njs`> Yes and no. Notice the rule: we only write out the base + object first if the delta against it was more recent. + + I'm getting lost in all these orders, let me re-read :-) + So the write-out order is from most recent to least recent? + (Conceivably it could be the opposite way too, I'm not sure if + we've said) though my connection back at home is logging, so I + can just read what you said there :-) + +And for those of you paying attention, the Omniscient Trick has just +been detailed! + + <linus> Yes, we always write out most recent first + +For the other record: + + <pasky> njs`: http://pastebin.com/547965 + +The 'net never forgets, so that should be good until the end of time. + + <njs`> And, yeah, I got the part about deeper-in-history stuff + having worse IO characteristics, one sort of doesn't care. + + <linus> With the caveat that if the "most recent" needs an older + object to delta against (hey, shrinking sometimes does + happen), we write out the old object with the delta. + + <njs`> (if only it happened more...) + + <linus> Anyway, the pack-file could easily be denser still, but + because it's used both for streaming (the git protocol) and + for on-disk, it has a few pessimizations. + +Actually, it is a made-up word. But it is a made-up word being +used as setup for a later optimization, which is a real word: + + <linus> In particular, while the pack-file is then compressed, + it's compressed just one object at a time, so the actual + compression factor is less than it could be in theory. But it + means that it's all nice random-access with a simple index to + do "object name->location in packfile" translation. + + <njs`> I'm assuming the real win for delta-ing large->small is + more homogeneous statistics for gzip to run over? + + (You have to put the bytes in one place or another, but + putting them in a larger blob wins on compression) + + Actually, what is the compression strategy -- each delta + individually gzipped, the whole file gzipped, somewhere in + between, no compression at all, ....? + + Right. + +Reality IRC sets in. For example: + + <pasky> I'll read the rest in the morning, I really have to go + sleep or there's no hope whatsoever for me at the today's + exam... g'nite all. + +Heh. + + <linus> pasky: g'nite + + <njs`> pasky: 'luck + + <linus> Right: large->small matters exactly because of compression + behaviour. If it was non-compressed, it probably wouldn't make + any difference. + + <njs`> yeah + + <linus> Anyway: I'm not even trying to claim that the pack-files + are perfect, but they do tend to have a nice balance of + density vs ease-of use. + +Gasp! OK, saved. That's a fair Engineering trade off. Close call! +In fact, Linus reflects on some Basic Engineering Fundamentals, +design options, etc. + + <linus> More importantly, they allow git to still _conceptually_ + never deal with deltas at all, and be a "whole object" store. + + Which has some problems (we discussed bad huge-file + behaviour on the git lists the other day), but it does mean + that the basic git concepts are really really simple and + straightforward. + + It's all been quite stable. + + Which I think is very much a result of having very simple + basic ideas, so that there's never any confusion about what's + going on. + + Bugs happen, but they are "simple" bugs. And bugs that + actually get some object store detail wrong are almost always + so obvious that they never go anywhere. + + <njs`> Yeah. + +Nuff said. + + <linus> Anyway. I'm off for bed. It's not 6AM here, but I've got + three kids, and have to get up early in the morning to send + them off. I need my beauty sleep. + + <njs`> :-) + + <njs`> appreciate the infodump, I really was failing to find the + details on git packs :-) + +And now you know the rest of the story. diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt new file mode 100644 index 0000000000..9cd48b4859 --- /dev/null +++ b/Documentation/technical/pack-protocol.txt @@ -0,0 +1,41 @@ +Pack transfer protocols +======================= + +There are two Pack push-pull protocols. + +upload-pack (S) | fetch/clone-pack (C) protocol: + + # Tell the puller what commits we have and what their names are + S: SHA1 name + S: ... + S: SHA1 name + S: # flush -- it's your turn + # Tell the pusher what commits we want, and what we have + C: want name + C: .. + C: want name + C: have SHA1 + C: have SHA1 + C: ... + C: # flush -- occasionally ask "had enough?" + S: NAK + C: have SHA1 + C: ... + C: have SHA1 + S: ACK + C: done + S: XXXXXXX -- packfile contents. + +send-pack | receive-pack protocol. + + # Tell the pusher what commits we have and what their names are + C: SHA1 name + C: ... + C: SHA1 name + C: # flush -- it's your turn + # Tell the puller what the pusher has + S: old-SHA1 new-SHA1 name + S: old-SHA1 new-SHA1 name + S: ... + S: # flush -- done with the list + S: XXXXXXX --- packfile contents. diff --git a/Documentation/technical/racy-git.txt b/Documentation/technical/racy-git.txt new file mode 100644 index 0000000000..5030d9f2f8 --- /dev/null +++ b/Documentation/technical/racy-git.txt @@ -0,0 +1,195 @@ +Use of index and Racy git problem +================================= + +Background +---------- + +The index is one of the most important data structures in git. +It represents a virtual working tree state by recording list of +paths and their object names and serves as a staging area to +write out the next tree object to be committed. The state is +"virtual" in the sense that it does not necessarily have to, and +often does not, match the files in the working tree. + +There are cases git needs to examine the differences between the +virtual working tree state in the index and the files in the +working tree. The most obvious case is when the user asks `git +diff` (or its low level implementation, `git diff-files`) or +`git-ls-files --modified`. In addition, git internally checks +if the files in the working tree are different from what are +recorded in the index to avoid stomping on local changes in them +during patch application, switching branches, and merging. + +In order to speed up this comparison between the files in the +working tree and the index entries, the index entries record the +information obtained from the filesystem via `lstat(2)` system +call when they were last updated. When checking if they differ, +git first runs `lstat(2)` on the files and compares the result +with this information (this is what was originally done by the +`ce_match_stat()` function, but the current code does it in +`ce_match_stat_basic()` function). If some of these "cached +stat information" fields do not match, git can tell that the +files are modified without even looking at their contents. + +Note: not all members in `struct stat` obtained via `lstat(2)` +are used for this comparison. For example, `st_atime` obviously +is not useful. Currently, git compares the file type (regular +files vs symbolic links) and executable bits (only for regular +files) from `st_mode` member, `st_mtime` and `st_ctime` +timestamps, `st_uid`, `st_gid`, `st_ino`, and `st_size` members. +With a `USE_STDEV` compile-time option, `st_dev` is also +compared, but this is not enabled by default because this member +is not stable on network filesystems. With `USE_NSEC` +compile-time option, `st_mtim.tv_nsec` and `st_ctim.tv_nsec` +members are also compared, but this is not enabled by default +because the value of this member becomes meaningless once the +inode is evicted from the inode cache on filesystems that do not +store it on disk. + + +Racy git +-------- + +There is one slight problem with the optimization based on the +cached stat information. Consider this sequence: + + : modify 'foo' + $ git update-index 'foo' + : modify 'foo' again, in-place, without changing its size + +The first `update-index` computes the object name of the +contents of file `foo` and updates the index entry for `foo` +along with the `struct stat` information. If the modification +that follows it happens very fast so that the file's `st_mtime` +timestamp does not change, after this sequence, the cached stat +information the index entry records still exactly match what you +would see in the filesystem, even though the file `foo` is now +different. +This way, git can incorrectly think files in the working tree +are unmodified even though they actually are. This is called +the "racy git" problem (discovered by Pasky), and the entries +that appear clean when they may not be because of this problem +are called "racily clean". + +To avoid this problem, git does two things: + +. When the cached stat information says the file has not been + modified, and the `st_mtime` is the same as (or newer than) + the timestamp of the index file itself (which is the time `git + update-index foo` finished running in the above example), it + also compares the contents with the object registered in the + index entry to make sure they match. + +. When the index file is updated that contains racily clean + entries, cached `st_size` information is truncated to zero + before writing a new version of the index file. + +Because the index file itself is written after collecting all +the stat information from updated paths, `st_mtime` timestamp of +it is usually the same as or newer than any of the paths the +index contains. And no matter how quick the modification that +follows `git update-index foo` finishes, the resulting +`st_mtime` timestamp on `foo` cannot get a value earlier +than the index file. Therefore, index entries that can be +racily clean are limited to the ones that have the same +timestamp as the index file itself. + +The callers that want to check if an index entry matches the +corresponding file in the working tree continue to call +`ce_match_stat()`, but with this change, `ce_match_stat()` uses +`ce_modified_check_fs()` to see if racily clean ones are +actually clean after comparing the cached stat information using +`ce_match_stat_basic()`. + +The problem the latter solves is this sequence: + + $ git update-index 'foo' + : modify 'foo' in-place without changing its size + : wait for enough time + $ git update-index 'bar' + +Without the latter, the timestamp of the index file gets a newer +value, and falsely clean entry `foo` would not be caught by the +timestamp comparison check done with the former logic anymore. +The latter makes sure that the cached stat information for `foo` +would never match with the file in the working tree, so later +checks by `ce_match_stat_basic()` would report that the index entry +does not match the file and git does not have to fall back on more +expensive `ce_modified_check_fs()`. + + +Runtime penalty +--------------- + +The runtime penalty of falling back to `ce_modified_check_fs()` +from `ce_match_stat()` can be very expensive when there are many +racily clean entries. An obvious way to artificially create +this situation is to give the same timestamp to all the files in +the working tree in a large project, run `git update-index` on +them, and give the same timestamp to the index file: + + $ date >.datestamp + $ git ls-files | xargs touch -r .datestamp + $ git ls-files | git update-index --stdin + $ touch -r .datestamp .git/index + +This will make all index entries racily clean. The linux-2.6 +project, for example, there are over 20,000 files in the working +tree. On my Athron 64X2 3800+, after the above: + + $ /usr/bin/time git diff-files + 1.68user 0.54system 0:02.22elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k + 0inputs+0outputs (0major+67111minor)pagefaults 0swaps + $ git update-index MAINTAINERS + $ /usr/bin/time git diff-files + 0.02user 0.12system 0:00.14elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k + 0inputs+0outputs (0major+935minor)pagefaults 0swaps + +Running `git update-index` in the middle checked the racily +clean entries, and left the cached `st_mtime` for all the paths +intact because they were actually clean (so this step took about +the same amount of time as the first `git diff-files`). After +that, they are not racily clean anymore but are truly clean, so +the second invocation of `git diff-files` fully took advantage +of the cached stat information. + + +Avoiding runtime penalty +------------------------ + +In order to avoid the above runtime penalty, post 1.4.2 git used +to have a code that made sure the index file +got timestamp newer than the youngest files in the index when +there are many young files with the same timestamp as the +resulting index file would otherwise would have by waiting +before finishing writing the index file out. + +I suspected that in practice the situation where many paths in the +index are all racily clean was quite rare. The only code paths +that can record recent timestamp for large number of paths are: + +. Initial `git add .` of a large project. + +. `git checkout` of a large project from an empty index into an + unpopulated working tree. + +Note: switching branches with `git checkout` keeps the cached +stat information of existing working tree files that are the +same between the current branch and the new branch, which are +all older than the resulting index file, and they will not +become racily clean. Only the files that are actually checked +out can become racily clean. + +In a large project where raciness avoidance cost really matters, +however, the initial computation of all object names in the +index takes more than one second, and the index file is written +out after all that happens. Therefore the timestamp of the +index file will be more than one seconds later than the the +youngest file in the working tree. This means that in these +cases there actually will not be any racily clean entry in +the resulting index. + +Based on this discussion, the current code does not use the +"workaround" to avoid the runtime penalty that does not exist in +practice anymore. This was done with commit 0fc82cff on Aug 15, +2006. diff --git a/Documentation/technical/send-pack-pipeline.txt b/Documentation/technical/send-pack-pipeline.txt new file mode 100644 index 0000000000..681efe4219 --- /dev/null +++ b/Documentation/technical/send-pack-pipeline.txt @@ -0,0 +1,63 @@ +git-send-pack +============= + +Overall operation +----------------- + +. Connects to the remote side and invokes git-receive-pack. + +. Learns what refs the remote has and what commit they point at. + Matches them to the refspecs we are pushing. + +. Checks if there are non-fast-forwards. Unlike fetch-pack, + the repository send-pack runs in is supposed to be a superset + of the recipient in fast-forward cases, so there is no need + for want/have exchanges, and fast-forward check can be done + locally. Tell the result to the other end. + +. Calls pack_objects() which generates a packfile and sends it + over to the other end. + +. If the remote side is new enough (v1.1.0 or later), wait for + the unpack and hook status from the other end. + +. Exit with appropriate error codes. + + +Pack_objects pipeline +--------------------- + +This function gets one file descriptor (`fd`) which is either a +socket (over the network) or a pipe (local). What's written to +this fd goes to git-receive-pack to be unpacked. + + send-pack ---> fd ---> receive-pack + +The function pack_objects creates a pipe and then forks. The +forked child execs pack-objects with --revs to receive revision +parameters from its standard input. This process will write the +packfile to the other end. + + send-pack + | + pack_objects() ---> fd ---> receive-pack + | ^ (pipe) + v | + (child) + +The child dup2's to arrange its standard output to go back to +the other end, and read its standard input to come from the +pipe. After that it exec's pack-objects. On the other hand, +the parent process, before starting to feed the child pipeline, +closes the reading side of the pipe and fd to receive-pack. + + send-pack + | + pack_objects(parent) + | + v [0] + pack-objects [0] ---> receive-pack + + +[jc: the pipeline was much more complex and needed documentation before + I understood an earlier bug, but now it is trivial and straightforward.] diff --git a/Documentation/technical/trivial-merge.txt b/Documentation/technical/trivial-merge.txt new file mode 100644 index 0000000000..24c84100b0 --- /dev/null +++ b/Documentation/technical/trivial-merge.txt @@ -0,0 +1,121 @@ +Trivial merge rules +=================== + +This document describes the outcomes of the trivial merge logic in read-tree. + +One-way merge +------------- + +This replaces the index with a different tree, keeping the stat info +for entries that don't change, and allowing -u to make the minimum +required changes to the working tree to have it match. + +Entries marked '+' have stat information. Spaces marked '*' don't +affect the result. + + index tree result + ----------------------- + * (empty) (empty) + (empty) tree tree + index+ tree tree + index+ index index+ + +Two-way merge +------------- + +It is permitted for the index to lack an entry; this does not prevent +any case from applying. + +If the index exists, it is an error for it not to match either the old +or the result. + +If multiple cases apply, the one used is listed first. + +A result which changes the index is an error if the index is not empty +and not up-to-date. + +Entries marked '+' have stat information. Spaces marked '*' don't +affect the result. + + case index old new result + ------------------------------------- + 0/2 (empty) * (empty) (empty) + 1/3 (empty) * new new + 4/5 index+ (empty) (empty) index+ + 6/7 index+ (empty) index index+ + 10 index+ index (empty) (empty) + 14/15 index+ old old index+ + 18/19 index+ old index index+ + 20 index+ index new new + +Three-way merge +--------------- + +It is permitted for the index to lack an entry; this does not prevent +any case from applying. + +If the index exists, it is an error for it not to match either the +head or (if the merge is trivial) the result. + +If multiple cases apply, the one used is listed first. + +A result of "no merge" means that index is left in stage 0, ancest in +stage 1, head in stage 2, and remote in stage 3 (if any of these are +empty, no entry is left for that stage). Otherwise, the given entry is +left in stage 0, and there are no other entries. + +A result of "no merge" is an error if the index is not empty and not +up-to-date. + +*empty* means that the tree must not have a directory-file conflict + with the entry. + +For multiple ancestors, a '+' means that this case applies even if +only one ancestor or remote fits; a '^' means all of the ancestors +must be the same. + +case ancest head remote result +---------------------------------------- +1 (empty)+ (empty) (empty) (empty) +2ALT (empty)+ *empty* remote remote +2 (empty)^ (empty) remote no merge +3ALT (empty)+ head *empty* head +3 (empty)^ head (empty) no merge +4 (empty)^ head remote no merge +5ALT * head head head +6 ancest+ (empty) (empty) no merge +8 ancest^ (empty) ancest no merge +7 ancest+ (empty) remote no merge +10 ancest^ ancest (empty) no merge +9 ancest+ head (empty) no merge +16 anc1/anc2 anc1 anc2 no merge +13 ancest+ head ancest head +14 ancest+ ancest remote remote +11 ancest+ head remote no merge + +Only #2ALT and #3ALT use *empty*, because these are the only cases +where there can be conflicts that didn't exist before. Note that we +allow directory-file conflicts between things in different stages +after the trivial merge. + +A possible alternative for #6 is (empty), which would make it like +#1. This is not used, due to the likelihood that it arises due to +moving the file to multiple different locations or moving and deleting +it in different branches. + +Case #1 is included for completeness, and also in case we decide to +put on '+' markings; any path that is never mentioned at all isn't +handled. + +Note that #16 is when both #13 and #14 apply; in this case, we refuse +the trivial merge, because we can't tell from this data which is +right. This is a case of a reverted patch (in some direction, maybe +multiple times), and the right answer depends on looking at crossings +of history or common ancestors of the ancestors. + +Note that, between #6, #7, #9, and #11, all cases not otherwise +covered are handled in this table. + +For #8 and #10, there is alternative behavior, not currently +implemented, where the result is (empty). As currently implemented, +the automatic merge will generally give this effect. diff --git a/Documentation/tutorial-2.txt b/Documentation/tutorial-2.txt new file mode 100644 index 0000000000..8d89992712 --- /dev/null +++ b/Documentation/tutorial-2.txt @@ -0,0 +1,403 @@ +A tutorial introduction to git: part two +======================================== + +You should work through link:tutorial.html[A tutorial introduction to +git] before reading this tutorial. + +The goal of this tutorial is to introduce two fundamental pieces of +git's architecture--the object database and the index file--and to +provide the reader with everything necessary to understand the rest +of the git documentation. + +The git object database +----------------------- + +Let's start a new project and create a small amount of history: + +------------------------------------------------ +$ mkdir test-project +$ cd test-project +$ git init +Initialized empty Git repository in .git/ +$ echo 'hello world' > file.txt +$ git add . +$ git commit -a -m "initial commit" +Created initial commit 54196cc2703dc165cbd373a65a4dcf22d50ae7f7 + create mode 100644 file.txt +$ echo 'hello world!' >file.txt +$ git commit -a -m "add emphasis" +Created commit c4d59f390b9cfd4318117afde11d601c1085f241 +------------------------------------------------ + +What are the 40 digits of hex that git responded to the commit with? + +We saw in part one of the tutorial that commits have names like this. +It turns out that every object in the git history is stored under +such a 40-digit hex name. That name is the SHA1 hash of the object's +contents; among other things, this ensures that git will never store +the same data twice (since identical data is given an identical SHA1 +name), and that the contents of a git object will never change (since +that would change the object's name as well). + +It is expected that the content of the commit object you created while +following the example above generates a different SHA1 hash than +the one shown above because the commit object records the time when +it was created and the name of the person performing the commit. + +We can ask git about this particular object with the cat-file +command. Don't copy the 40 hex digits from this example but use those +from your own version. Note that you can shorten it to only a few +characters to save yourself typing all 40 hex digits: + +------------------------------------------------ +$ git-cat-file -t 54196cc2 +commit +$ git-cat-file commit 54196cc2 +tree 92b8b694ffb1675e5975148e1121810081dbdffe +author J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500 +committer J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500 + +initial commit +------------------------------------------------ + +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 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 92b8b694 +100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad file.txt +------------------------------------------------ + +Thus we see that this tree has one file in it. The SHA1 hash is a +reference to that file's data: + +------------------------------------------------ +$ git cat-file -t 3b18e512 +blob +------------------------------------------------ + +A "blob" is just file data, which we can also examine with cat-file: + +------------------------------------------------ +$ git cat-file blob 3b18e512 +hello world +------------------------------------------------ + +Note that this is the old file data; so the object that git named in +its response to the initial tree was a tree with a snapshot of the +directory state that was recorded by the first commit. + +All of these objects are stored under their SHA1 names inside the git +directory: + +------------------------------------------------ +$ find .git/objects/ +.git/objects/ +.git/objects/pack +.git/objects/info +.git/objects/3b +.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad +.git/objects/92 +.git/objects/92/b8b694ffb1675e5975148e1121810081dbdffe +.git/objects/54 +.git/objects/54/196cc2703dc165cbd373a65a4dcf22d50ae7f7 +.git/objects/a0 +.git/objects/a0/423896973644771497bdc03eb99d5281615b51 +.git/objects/d0 +.git/objects/d0/492b368b66bdabf2ac1fd8c92b39d3db916e59 +.git/objects/c4 +.git/objects/c4/d59f390b9cfd4318117afde11d601c1085f241 +------------------------------------------------ + +and the contents of these files is just the compressed data plus a +header identifying their length and their type. The type is either a +blob, a tree, a commit, or a tag. + +The simplest commit to find is the HEAD commit, which we can find +from .git/HEAD: + +------------------------------------------------ +$ cat .git/HEAD +ref: refs/heads/master +------------------------------------------------ + +As you can see, this tells us which branch we're currently on, and it +tells us this by naming a file under the .git directory, which itself +contains a SHA1 name referring to a commit object, which we can +examine with cat-file: + +------------------------------------------------ +$ cat .git/refs/heads/master +c4d59f390b9cfd4318117afde11d601c1085f241 +$ git cat-file -t c4d59f39 +commit +$ git cat-file commit c4d59f39 +tree d0492b368b66bdabf2ac1fd8c92b39d3db916e59 +parent 54196cc2703dc165cbd373a65a4dcf22d50ae7f7 +author J. Bruce Fields <bfields@puzzle.fieldses.org> 1143418702 -0500 +committer J. Bruce Fields <bfields@puzzle.fieldses.org> 1143418702 -0500 + +add emphasis +------------------------------------------------ + +The "tree" object here refers to the new state of the tree: + +------------------------------------------------ +$ git ls-tree d0492b36 +100644 blob a0423896973644771497bdc03eb99d5281615b51 file.txt +$ git cat-file blob a0423896 +hello world! +------------------------------------------------ + +and the "parent" object refers to the previous commit: + +------------------------------------------------ +$ git-cat-file commit 54196cc2 +tree 92b8b694ffb1675e5975148e1121810081dbdffe +author J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500 +committer J. Bruce Fields <bfields@puzzle.fieldses.org> 1143414668 -0500 + +initial commit +------------------------------------------------ + +The tree object is the tree we examined first, and this commit is +unusual in that it lacks any parent. + +Most commits have only one parent, but it is also common for a commit +to have multiple parents. In that case the commit represents a +merge, with the parent references pointing to the heads of the merged +branches. + +Besides blobs, trees, and commits, the only remaining type of object +is a "tag", which we won't discuss here; refer to gitlink:git-tag[1] +for details. + +So now we know how git uses the object database to represent a +project's history: + + * "commit" objects refer to "tree" objects representing the + snapshot of a directory tree at a particular point in the + history, and refer to "parent" commits to show how they're + connected into the project history. + * "tree" objects represent the state of a single directory, + associating directory names to "blob" objects containing file + data and "tree" objects containing subdirectory information. + * "blob" objects contain file data without any other structure. + * References to commit objects at the head of each branch are + stored in files under .git/refs/heads/. + * The name of the current branch is stored in .git/HEAD. + +Note, by the way, that lots of commands take a tree as an argument. +But as we can see above, a tree can be referred to in many different +ways--by the SHA1 name for that tree, by the name of a commit that +refers to the tree, by the name of a branch whose head refers to that +tree, etc.--and most such commands can accept any of these names. + +In command synopses, the word "tree-ish" is sometimes used to +designate such an argument. + +The index file +-------------- + +The primary tool we've been using to create commits is "git commit +-a", which creates a commit including every change you've made to +your working tree. But what if you want to commit changes only to +certain files? Or only certain changes to certain files? + +If we look at the way commits are created under the cover, we'll see +that there are more flexible ways creating commits. + +Continuing with our test-project, let's modify file.txt again: + +------------------------------------------------ +$ echo "hello world, again" >>file.txt +------------------------------------------------ + +but this time instead of immediately making the commit, let's take an +intermediate step, and ask for diffs along the way to keep track of +what's happening: + +------------------------------------------------ +$ git diff +--- a/file.txt ++++ b/file.txt +@@ -1 +1,2 @@ + hello world! ++hello world, again +$ git update-index file.txt +$ git diff +------------------------------------------------ + +The last diff is empty, but no new commits have been made, and the +head still doesn't contain the new line: + +------------------------------------------------ +$ git-diff HEAD +diff --git a/file.txt b/file.txt +index a042389..513feba 100644 +--- a/file.txt ++++ b/file.txt +@@ -1 +1,2 @@ + hello world! ++hello world, again +------------------------------------------------ + +So "git diff" is comparing against something other than the head. +The thing that it's comparing against is actually the index file, +which is stored in .git/index in a binary format, but whose contents +we can examine with ls-files: + +------------------------------------------------ +$ git ls-files --stage +100644 513feba2e53ebbd2532419ded848ba19de88ba00 0 file.txt +$ git cat-file -t 513feba2 +blob +$ git cat-file blob 513feba2 +hello world! +hello world, again +------------------------------------------------ + +So what our "git update-index" did was store a new blob and then put +a reference to it in the index file. If we modify the file again, +we'll see that the new modifications are reflected in the "git-diff" +output: + +------------------------------------------------ +$ echo 'again?' >>file.txt +$ git diff +index 513feba..ba3da7b 100644 +--- a/file.txt ++++ b/file.txt +@@ -1,2 +1,3 @@ + hello world! + hello world, again ++again? +------------------------------------------------ + +With the right arguments, git diff can also show us the difference +between the working directory and the last commit, or between the +index and the last commit: + +------------------------------------------------ +$ git diff HEAD +diff --git a/file.txt b/file.txt +index a042389..ba3da7b 100644 +--- a/file.txt ++++ b/file.txt +@@ -1 +1,3 @@ + hello world! ++hello world, again ++again? +$ git diff --cached +diff --git a/file.txt b/file.txt +index a042389..513feba 100644 +--- a/file.txt ++++ b/file.txt +@@ -1 +1,2 @@ + hello world! ++hello world, again +------------------------------------------------ + +At any time, we can create a new commit using "git commit" (without +the -a option), and verify that the state committed only includes the +changes stored in the index file, not the additional change that is +still only in our working tree: + +------------------------------------------------ +$ git commit -m "repeat" +$ git diff HEAD +diff --git a/file.txt b/file.txt +index 513feba..ba3da7b 100644 +--- a/file.txt ++++ b/file.txt +@@ -1,2 +1,3 @@ + hello world! + hello world, again ++again? +------------------------------------------------ + +So by default "git commit" uses the index to create the commit, not +the working tree; the -a option to commit tells it to first update +the index with all changes in the working tree. + +Finally, it's worth looking at the effect of "git add" on the index +file: + +------------------------------------------------ +$ echo "goodbye, world" >closing.txt +$ git add closing.txt +------------------------------------------------ + +The effect of the "git add" was to add one entry to the index file: + +------------------------------------------------ +$ git ls-files --stage +100644 8b9743b20d4b15be3955fc8d5cd2b09cd2336138 0 closing.txt +100644 513feba2e53ebbd2532419ded848ba19de88ba00 0 file.txt +------------------------------------------------ + +And, as you can see with cat-file, this new entry refers to the +current contents of the file: + +------------------------------------------------ +$ git cat-file blob 8b9743b2 +goodbye, world +------------------------------------------------ + +The "status" command is a useful way to get a quick summary of the +situation: + +------------------------------------------------ +$ git status +# On branch master +# Changes to be committed: +# (use "git reset HEAD <file>..." to unstage) +# +# new file: closing.txt +# +# Changed but not updated: +# (use "git add <file>..." to update what will be committed) +# +# modified: file.txt +# +------------------------------------------------ + +Since the current state of closing.txt is cached in the index file, +it is listed as "Changes to be committed". Since file.txt has +changes in the working directory that aren't reflected in the index, +it is marked "changed but not updated". At this point, running "git +commit" would create a commit that added closing.txt (with its new +contents), but that didn't modify file.txt. + +Also, note that a bare "git diff" shows the changes to file.txt, but +not the addition of closing.txt, because the version of closing.txt +in the index file is identical to the one in the working directory. + +In addition to being the staging area for new commits, the index file +is also populated from the object database when checking out a +branch, and is used to hold the trees involved in a merge operation. +See the link:core-tutorial.html[core tutorial] and the relevant man +pages for details. + +What next? +---------- + +At this point you should know everything necessary to read the man +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[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 +CVS-like way. + +For some interesting examples of git use, see the +link:howto-index.html[howtos]. + +For git developers, the link:core-tutorial.html[Core tutorial] goes +into detail on the lower-level git mechanisms involved in, for +example, creating a new commit. diff --git a/Documentation/tutorial.txt b/Documentation/tutorial.txt new file mode 100644 index 0000000000..129c5c5f5b --- /dev/null +++ b/Documentation/tutorial.txt @@ -0,0 +1,584 @@ +A tutorial introduction to git +============================== + +This tutorial explains how to import a new project into git, make +changes to it, and share changes with other developers. + +First, note that you can get documentation for a command such as "git +diff" with: + +------------------------------------------------ +$ man git-diff +------------------------------------------------ + +It is a good idea to introduce yourself to git with your name and +public email address before doing any operation. The easiest +way to do so is: + +------------------------------------------------ +$ git config --global user.name "Your Name Comes Here" +$ git config --global user.email you@yourdomain.example.com +------------------------------------------------ + + +Importing a new project +----------------------- + +Assume you have a tarball project.tar.gz with your initial work. You +can place it under git revision control as follows. + +------------------------------------------------ +$ tar xzf project.tar.gz +$ cd project +$ git init +------------------------------------------------ + +Git will reply + +------------------------------------------------ +Initialized empty Git repository in .git/ +------------------------------------------------ + +You've now initialized the working directory--you may notice a new +directory created, named ".git". Tell git that you want it to track +every file under the current directory (note the '.') with: + +------------------------------------------------ +$ git add . +------------------------------------------------ + +Finally, + +------------------------------------------------ +$ git commit +------------------------------------------------ + +will prompt you for a commit message, then record the current state +of all the files to the repository. + +Making changes +-------------- + +Try modifying some files, then run + +------------------------------------------------ +$ git diff +------------------------------------------------ + +to review your changes. When you're done, tell git that you +want the updated contents of these files in the commit and then +make a commit, like this: + +------------------------------------------------ +$ git add file1 file2 file3 +$ git commit +------------------------------------------------ + +This will again prompt your for a message describing the change, and then +record the new versions of the files you listed. + +Alternatively, instead of running `git add` beforehand, you can use + +------------------------------------------------ +$ git commit -a +------------------------------------------------ + +which will automatically notice modified (but not new) files. + +A note on commit messages: Though not required, it's a good idea to +begin the commit message with a single short (less than 50 character) +line summarizing the change, followed by a blank line and then a more +thorough description. Tools that turn commits into email, for +example, use the first line on the Subject: line and the rest of the +commit in the body. + + +Git tracks content not files +---------------------------- + +With git you have to explicitly "add" all the changed _content_ you +want to commit together. This can be done in a few different ways: + +1) By using 'git add <file_spec>...' + +This can be performed multiple times before a commit. Note that this +is not only for adding new files. Even modified files must be +added to the set of changes about to be committed. The "git status" +command gives you a summary of what is included so far for the +next commit. When done you should use the 'git commit' command to +make it real. + +Note: don't forget to 'add' a file again if you modified it after the +first 'add' and before 'commit'. Otherwise only the previous added +state of that file will be committed. This is because git tracks +content, so what you're really 'add'ing to the commit is the *content* +of the file in the state it is in when you 'add' it. + +2) By using 'git commit -a' directly + +This is a quick way to automatically 'add' the content from all files +that were modified since the previous commit, and perform the actual +commit without having to separately 'add' them beforehand. This will +not add content from new files i.e. files that were never added before. +Those files still have to be added explicitly before performing a +commit. + +But here's a twist. If you do 'git commit <file1> <file2> ...' then only +the changes belonging to those explicitly specified files will be +committed, entirely bypassing the current "added" changes. Those "added" +changes will still remain available for a subsequent commit though. + +However, for normal usage you only have to remember 'git add' + 'git commit' +and/or 'git commit -a'. + + +Viewing the changelog +--------------------- + +At any point you can view the history of your changes using + +------------------------------------------------ +$ git log +------------------------------------------------ + +If you also want to see complete diffs at each step, use + +------------------------------------------------ +$ git log -p +------------------------------------------------ + +Often the overview of the change is useful to get a feel of +each step + +------------------------------------------------ +$ git log --stat --summary +------------------------------------------------ + +Managing branches +----------------- + +A single git repository can maintain multiple branches of +development. To create a new branch named "experimental", use + +------------------------------------------------ +$ git branch experimental +------------------------------------------------ + +If you now run + +------------------------------------------------ +$ git branch +------------------------------------------------ + +you'll get a list of all existing branches: + +------------------------------------------------ + experimental +* master +------------------------------------------------ + +The "experimental" branch is the one you just created, and the +"master" branch is a default branch that was created for you +automatically. The asterisk marks the branch you are currently on; +type + +------------------------------------------------ +$ git checkout experimental +------------------------------------------------ + +to switch to the experimental branch. Now edit a file, commit the +change, and switch back to the master branch: + +------------------------------------------------ +(edit file) +$ git commit -a +$ git checkout master +------------------------------------------------ + +Check that the change you made is no longer visible, since it was +made on the experimental branch and you're back on the master branch. + +You can make a different change on the master branch: + +------------------------------------------------ +(edit file) +$ git commit -a +------------------------------------------------ + +at this point the two branches have diverged, with different changes +made in each. To merge the changes made in experimental into master, run + +------------------------------------------------ +$ git merge experimental +------------------------------------------------ + +If the changes don't conflict, you're done. If there are conflicts, +markers will be left in the problematic files showing the conflict; + +------------------------------------------------ +$ git diff +------------------------------------------------ + +will show this. Once you've edited the files to resolve the +conflicts, + +------------------------------------------------ +$ git commit -a +------------------------------------------------ + +will commit the result of the merge. Finally, + +------------------------------------------------ +$ gitk +------------------------------------------------ + +will show a nice graphical representation of the resulting history. + +At this point you could delete the experimental branch with + +------------------------------------------------ +$ git branch -d experimental +------------------------------------------------ + +This command ensures that the changes in the experimental branch are +already in the current branch. + +If you develop on a branch crazy-idea, then regret it, you can always +delete the branch with + +------------------------------------- +$ git branch -D crazy-idea +------------------------------------- + +Branches are cheap and easy, so this is a good way to try something +out. + +Using git for collaboration +--------------------------- + +Suppose that Alice has started a new project with a git repository in +/home/alice/project, and that Bob, who has a home directory on the +same machine, wants to contribute. + +Bob begins with: + +------------------------------------------------ +$ git clone /home/alice/project myrepo +------------------------------------------------ + +This creates a new directory "myrepo" containing a clone of Alice's +repository. The clone is on an equal footing with the original +project, possessing its own copy of the original project's history. + +Bob then makes some changes and commits them: + +------------------------------------------------ +(edit files) +$ git commit -a +(repeat as necessary) +------------------------------------------------ + +When he's ready, he tells Alice to pull changes from the repository +at /home/bob/myrepo. She does this with: + +------------------------------------------------ +$ cd /home/alice/project +$ git pull /home/bob/myrepo master +------------------------------------------------ + +This merges the changes from Bob's "master" branch into Alice's +current branch. If Alice has made her own changes in the meantime, +then she may need to manually fix any conflicts. (Note that the +"master" argument in the above command is actually unnecessary, as it +is the default.) + +The "pull" command thus performs two operations: it fetches changes +from a remote branch, then merges them into the current branch. + +When you are working in a small closely knit group, it is not +unusual to interact with the same repository over and over +again. By defining 'remote' repository shorthand, you can make +it easier: + +------------------------------------------------ +$ git remote add bob /home/bob/myrepo +------------------------------------------------ + +With this, you can perform the first operation alone using the +"git fetch" command without merging them with her own branch, +using: + +------------------------------------- +$ git fetch bob +------------------------------------- + +Unlike the longhand form, when Alice fetches from Bob using a +remote repository shorthand set up with `git remote`, what was +fetched is stored in a remote tracking branch, in this case +`bob/master`. So after this: + +------------------------------------- +$ git log -p master..bob/master +------------------------------------- + +shows a list of all the changes that Bob made since he branched from +Alice's master branch. + +After examining those changes, Alice +could merge the changes into her master branch: + +------------------------------------- +$ git merge bob/master +------------------------------------- + +This `merge` can also be done by 'pulling from her own remote +tracking branch', like this: + +------------------------------------- +$ git pull . remotes/bob/master +------------------------------------- + +Note that git pull always merges into the current branch, +regardless of what else is given on the commandline. + +Later, Bob can update his repo with Alice's latest changes using + +------------------------------------- +$ git pull +------------------------------------- + +Note that he doesn't need to give the path to Alice's repository; +when Bob cloned Alice's repository, git stored the location of her +repository in the repository configuration, and that location is +used for pulls: + +------------------------------------- +$ git config --get remote.origin.url +/home/bob/myrepo +------------------------------------- + +(The complete configuration created by git-clone is visible using +"git config -l", and the gitlink:git-config[1] man page +explains the meaning of each option.) + +Git also keeps a pristine copy of Alice's master branch under the +name "origin/master": + +------------------------------------- +$ git branch -r + origin/master +------------------------------------- + +If Bob later decides to work from a different host, he can still +perform clones and pulls using the ssh protocol: + +------------------------------------- +$ git clone alice.org:/home/alice/project myrepo +------------------------------------- + +Alternatively, git has a native protocol, or can use rsync or http; +see gitlink:git-pull[1] for details. + +Git can also be used in a CVS-like mode, with a central repository +that various users push changes to; see gitlink:git-push[1] and +link:cvs-migration.html[git for CVS users]. + +Exploring history +----------------- + +Git history is represented as a series of interrelated commits. We +have already seen that the git log command can list those commits. +Note that first line of each git log entry also gives a name for the +commit: + +------------------------------------- +$ git log +commit c82a22c39cbc32576f64f5c6b3f24b99ea8149c7 +Author: Junio C Hamano <junkio@cox.net> +Date: Tue May 16 17:18:22 2006 -0700 + + merge-base: Clarify the comments on post processing. +------------------------------------- + +We can give this name to git show to see the details about this +commit. + +------------------------------------- +$ git show c82a22c39cbc32576f64f5c6b3f24b99ea8149c7 +------------------------------------- + +But there are other ways to refer to commits. You can use any initial +part of the name that is long enough to uniquely identify the commit: + +------------------------------------- +$ git show c82a22c39c # the first few characters of the name are + # usually enough +$ git show HEAD # the tip of the current branch +$ git show experimental # the tip of the "experimental" branch +------------------------------------- + +Every commit usually has one "parent" commit +which points to the previous state of the project: + +------------------------------------- +$ git show HEAD^ # to see the parent of HEAD +$ git show HEAD^^ # to see the grandparent of HEAD +$ git show HEAD~4 # to see the great-great grandparent of HEAD +------------------------------------- + +Note that merge commits may have more than one parent: + +------------------------------------- +$ git show HEAD^1 # show the first parent of HEAD (same as HEAD^) +$ git show HEAD^2 # show the second parent of HEAD +------------------------------------- + +You can also give commits names of your own; after running + +------------------------------------- +$ git-tag v2.5 1b2e1d63ff +------------------------------------- + +you can refer to 1b2e1d63ff by the name "v2.5". If you intend to +share this name with other people (for example, to identify a release +version), you should create a "tag" object, and perhaps sign it; see +gitlink:git-tag[1] for details. + +Any git command that needs to know a commit can take any of these +names. For example: + +------------------------------------- +$ git diff v2.5 HEAD # compare the current HEAD to v2.5 +$ 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 to its state at HEAD^ +------------------------------------- + +Be careful with that last command: in addition to losing any changes +in the working directory, it will also remove all later commits from +this branch. If this branch is the only branch containing those +commits, they will be lost. Also, don't use "git reset" on a +publicly-visible branch that other developers pull from, as it will +force needless merges on other developers to clean up the history. +If you need to undo changes that you have pushed, use gitlink:git-revert[1] +instead. + +The git grep command can search for strings in any version of your +project, so + +------------------------------------- +$ git grep "hello" 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 + +------------------------------------- +$ git grep "hello" +------------------------------------- + +is a quick way to search just the files that are tracked by git. + +Many git commands also take sets of commits, which can be specified +in a number of ways. Here are some examples with git log: + +------------------------------------- +$ git log v2.5..v2.6 # commits between v2.5 and v2.6 +$ git log v2.5.. # commits since v2.5 +$ git log --since="2 weeks ago" # commits from the last 2 weeks +$ git log v2.5.. Makefile # commits since v2.5 which modify + # Makefile +------------------------------------- + +You can also give git log a "range" of commits where the first is not +necessarily an ancestor of the second; for example, if the tips of +the branches "stable-release" and "master" diverged from a common +commit some time ago, then + +------------------------------------- +$ git log stable..experimental +------------------------------------- + +will list commits made in the experimental branch but not in the +stable branch, while + +------------------------------------- +$ git log experimental..stable +------------------------------------- + +will show the list of commits made on the stable branch but not +the experimental branch. + +The "git log" command has a weakness: it must present commits in a +list. When the history has lines of development that diverged and +then merged back together, the order in which "git log" presents +those commits is meaningless. + +Most projects with multiple contributors (such as the linux kernel, +or git itself) have frequent merges, and gitk does a better job of +visualizing their history. For example, + +------------------------------------- +$ gitk --since="2 weeks ago" drivers/ +------------------------------------- + +allows you to browse any commits from the last 2 weeks of commits +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 +of the file: + +------------------------------------- +$ git diff v2.5:Makefile HEAD:Makefile.in +------------------------------------- + +You can also use "git show" to see any such file: + +------------------------------------- +$ git show v2.5:Makefile +------------------------------------- + +Next Steps +---------- + +This tutorial should be enough to perform basic distributed revision +control for your projects. However, to fully understand the depth +and power of git you need to understand two simple ideas on which it +is based: + + * The object database is the rather elegant system used to + store the history of your project--files, directories, and + commits. + + * The index file is a cache of the state of a directory tree, + used to create commits, check out working directories, and + hold the various trees involved in a merge. + +link:tutorial-2.html[Part two of this tutorial] explains the object +database, the index file, and a few other odds and ends that you'll +need to make the most of git. + +If you don't want to consider with that right away, a few other +digressions that may be interesting at this point are: + + * gitlink:git-format-patch[1], gitlink:git-am[1]: These convert + series of git commits into emailed patches, and vice versa, + useful for projects such as the linux kernel which rely heavily + on emailed patches. + + * gitlink:git-bisect[1]: When there is a regression in your + project, one way to track down the bug is by searching through + the history to find the exact commit that's to blame. Git bisect + can help you perform a binary search for that commit. It is + 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[Everyday GIT with 20 Commands Or So] + + * link:cvs-migration.html[git for CVS users]. diff --git a/Documentation/urls.txt b/Documentation/urls.txt new file mode 100644 index 0000000000..745f9677d0 --- /dev/null +++ b/Documentation/urls.txt @@ -0,0 +1,88 @@ +GIT URLS[[URLS]] +---------------- + +One of the following notations can be used +to name the remote repository: + +=============================================================== +- rsync://host.xz/path/to/repo.git/ +- http://host.xz/path/to/repo.git/ +- https://host.xz/path/to/repo.git/ +- git://host.xz/path/to/repo.git/ +- git://host.xz/~user/path/to/repo.git/ +- ssh://{startsb}user@{endsb}host.xz/path/to/repo.git/ +- ssh://{startsb}user@{endsb}host.xz/~user/path/to/repo.git/ +- ssh://{startsb}user@{endsb}host.xz/~/path/to/repo.git +=============================================================== + +SSH is the default transport protocol. You can optionally specify +which user to log-in as, and an alternate, scp-like syntax is also +supported. Both syntaxes support username expansion, +as does the native git protocol. The following three are +identical to the last three above, respectively: + +=============================================================== +- {startsb}user@{endsb}host.xz:/path/to/repo.git/ +- {startsb}user@{endsb}host.xz:~user/path/to/repo.git/ +- {startsb}user@{endsb}host.xz:path/to/repo.git +=============================================================== + +To sync with a local directory, use: + +=============================================================== +- /path/to/repo.git/ +=============================================================== + +REMOTES +------- + +In addition to the above, as a short-hand, the name of a +file in `$GIT_DIR/remotes` directory can be given; the +named file should be in the following format: + +------------ + URL: one of the above URL format + Push: <refspec> + Pull: <refspec> + +------------ + +Then such a short-hand is specified in place of +<repository> without <refspec> parameters on the command +line, <refspec> specified on `Push:` lines or `Pull:` +lines are used for `git-push` and `git-fetch`/`git-pull`, +respectively. Multiple `Push:` and `Pull:` lines may +be specified for additional branch mappings. + +Or, equivalently, in the `$GIT_DIR/config` (note the use +of `fetch` instead of `Pull:`): + +------------ + [remote "<remote>"] + url = <url> + push = <refspec> + fetch = <refspec> + +------------ + +The name of a file in `$GIT_DIR/branches` directory can be +specified as an older notation short-hand; the named +file should contain a single line, a URL in one of the +above formats, optionally followed by a hash `#` and the +name of remote head (URL fragment notation). +`$GIT_DIR/branches/<remote>` file that stores a <url> +without the fragment is equivalent to have this in the +corresponding file in the `$GIT_DIR/remotes/` directory. + +------------ + URL: <url> + Pull: refs/heads/master:<remote> + +------------ + +while having `<url>#<head>` is equivalent to + +------------ + URL: <url> + Pull: refs/heads/<head>:<remote> +------------ diff --git a/Documentation/user-manual.conf b/Documentation/user-manual.conf new file mode 100644 index 0000000000..92b01ecf71 --- /dev/null +++ b/Documentation/user-manual.conf @@ -0,0 +1,21 @@ +[titles] + underlines="__","==","--","~~","^^" + +[attributes] +caret=^ +startsb=[ +endsb=] +tilde=~ + +[gitlink-inlinemacro] +<ulink url="{target}.html">{target}{0?({0})}</ulink> + +ifdef::backend-docbook[] +# "unbreak" docbook-xsl v1.68 for manpages. v1.69 works with or without this. +[listingblock] +<example><title>{title}</title> +<literallayout> +| +</literallayout> +{title#}</example> +endif::backend-docbook[] diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt new file mode 100644 index 0000000000..c5e9ea8a42 --- /dev/null +++ b/Documentation/user-manual.txt @@ -0,0 +1,2961 @@ +Git User's Manual +_________________ + +This manual is designed to be readable by someone with basic unix +commandline skills, but no previous knowledge of git. + +Chapter 1 gives a brief overview of git commands, without any +explanation; you may prefer to skip to chapter 2 on a first reading. + +Chapters 2 and 3 explain how to fetch and study a project using +git--the tools you'd need to build and test a particular version of a +software project, to search for regressions, and so on. + +Chapter 4 explains how to do development with git, and chapter 5 how +to share that development with others. + +Further chapters cover more specialized topics. + +Comprehensive reference documentation is available through the man +pages. For a command such as "git clone", just use + +------------------------------------------------ +$ man git-clone +------------------------------------------------ + +Git Quick Start +=============== + +This is a quick summary of the major commands; the following chapters +will explain how these work in more detail. + +Creating a new repository +------------------------- + +From a tarball: + +----------------------------------------------- +$ tar xzf project.tar.gz +$ cd project +$ git init +Initialized empty Git repository in .git/ +$ git add . +$ git commit +----------------------------------------------- + +From a remote repository: + +----------------------------------------------- +$ git clone git://example.com/pub/project.git +$ cd project +----------------------------------------------- + +Managing branches +----------------- + +----------------------------------------------- +$ git branch # list all branches in this repo +$ git checkout test # switch working directory to branch "test" +$ git branch new # create branch "new" starting at current HEAD +$ git branch -d new # delete branch "new" +----------------------------------------------- + +Instead of basing new branch on current HEAD (the default), use: + +----------------------------------------------- +$ git branch new test # branch named "test" +$ git branch new v2.6.15 # tag named v2.6.15 +$ git branch new HEAD^ # commit before the most recent +$ git branch new HEAD^^ # commit before that +$ git branch new test~10 # ten commits before tip of branch "test" +----------------------------------------------- + +Create and switch to a new branch at the same time: + +----------------------------------------------- +$ git checkout -b new v2.6.15 +----------------------------------------------- + +Update and examine branches from the repository you cloned from: + +----------------------------------------------- +$ git fetch # update +$ git branch -r # list + origin/master + origin/next + ... +$ git branch checkout -b masterwork origin/master +----------------------------------------------- + +Fetch a branch from a different repository, and give it a new +name in your repository: + +----------------------------------------------- +$ git fetch git://example.com/project.git theirbranch:mybranch +$ git fetch git://example.com/project.git v2.6.15:mybranch +----------------------------------------------- + +Keep a list of repositories you work with regularly: + +----------------------------------------------- +$ git remote add example git://example.com/project.git +$ git remote # list remote repositories +example +origin +$ git remote show example # get details +* remote example + URL: git://example.com/project.git + Tracked remote branches + master next ... +$ git fetch example # update branches from example +$ git branch -r # list all remote branches +----------------------------------------------- + + +Exploring history +----------------- + +----------------------------------------------- +$ gitk # visualize and browse history +$ git log # list all commits +$ git log src/ # ...modifying src/ +$ git log v2.6.15..v2.6.16 # ...in v2.6.16, not in v2.6.15 +$ git log master..test # ...in branch test, not in branch master +$ git log test..master # ...in branch master, but not in test +$ git log test...master # ...in one branch, not in both +$ git log -S'foo()' # ...where difference contain "foo()" +$ git log --since="2 weeks ago" +$ git log -p # show patches as well +$ git show # most recent commit +$ git diff v2.6.15..v2.6.16 # diff between two tagged versions +$ git diff v2.6.15..HEAD # diff with current head +$ git grep "foo()" # search working directory for "foo()" +$ git grep v2.6.15 "foo()" # search old tree for "foo()" +$ git show v2.6.15:a.txt # look at old version of a.txt +----------------------------------------------- + +Search for regressions: + +----------------------------------------------- +$ git bisect start +$ git bisect bad # current version is bad +$ git bisect good v2.6.13-rc2 # last known good revision +Bisecting: 675 revisions left to test after this + # test here, then: +$ git bisect good # if this revision is good, or +$ git bisect bad # if this revision is bad. + # repeat until done. +----------------------------------------------- + +Making changes +-------------- + +Make sure git knows who to blame: + +------------------------------------------------ +$ cat >~/.gitconfig <<\EOF +[user] +name = Your Name Comes Here +email = you@yourdomain.example.com +EOF +------------------------------------------------ + +Select file contents to include in the next commit, then make the +commit: + +----------------------------------------------- +$ git add a.txt # updated file +$ git add b.txt # new file +$ git rm c.txt # old file +$ git commit +----------------------------------------------- + +Or, prepare and create the commit in one step: + +----------------------------------------------- +$ git commit d.txt # use latest content only of d.txt +$ git commit -a # use latest content of all tracked files +----------------------------------------------- + +Merging +------- + +----------------------------------------------- +$ git merge test # merge branch "test" into the current branch +$ git pull git://example.com/project.git master + # fetch and merge in remote branch +$ git pull . test # equivalent to git merge test +----------------------------------------------- + +Sharing your changes +-------------------- + +Importing or exporting patches: + +----------------------------------------------- +$ git format-patch origin..HEAD # format a patch for each commit + # in HEAD but not in origin +$ git-am mbox # import patches from the mailbox "mbox" +----------------------------------------------- + +Fetch a branch in a different git repository, then merge into the +current branch: + +----------------------------------------------- +$ git pull git://example.com/project.git theirbranch +----------------------------------------------- + +Store the fetched branch into a local branch before merging into the +current branch: + +----------------------------------------------- +$ git pull git://example.com/project.git theirbranch:mybranch +----------------------------------------------- + +After creating commits on a local branch, update the remote +branch with your commits: + +----------------------------------------------- +$ git push ssh://example.com/project.git mybranch:theirbranch +----------------------------------------------- + +When remote and local branch are both named "test": + +----------------------------------------------- +$ git push ssh://example.com/project.git test +----------------------------------------------- + +Shortcut version for a frequently used remote repository: + +----------------------------------------------- +$ git remote add example ssh://example.com/project.git +$ git push example test +----------------------------------------------- + +Repository maintenance +---------------------- + +Check for corruption: + +----------------------------------------------- +$ git fsck +----------------------------------------------- + +Recompress, remove unused cruft: + +----------------------------------------------- +$ git gc +----------------------------------------------- + +Repositories and Branches +========================= + +How to get a git repository +--------------------------- + +It will be useful to have a git repository to experiment with as you +read this manual. + +The best way to get one is by using the gitlink:git-clone[1] command +to download a copy of an existing repository for a project that you +are interested in. If you don't already have a project in mind, here +are some interesting examples: + +------------------------------------------------ + # git itself (approx. 10MB download): +$ git clone git://git.kernel.org/pub/scm/git/git.git + # the linux kernel (approx. 150MB download): +$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git +------------------------------------------------ + +The initial clone may be time-consuming for a large project, but you +will only need to clone once. + +The clone command creates a new directory named after the project +("git" or "linux-2.6" in the examples above). After you cd into this +directory, you will see that it contains a copy of the project files, +together with a special top-level directory named ".git", which +contains all the information about the history of the project. + +In most of the following, examples will be taken from one of the two +repositories above. + +How to check out a different version of a project +------------------------------------------------- + +Git is best thought of as a tool for storing the history of a +collection of files. It stores the history as a compressed +collection of interrelated snapshots (versions) of the project's +contents. + +A single git repository may contain multiple branches. Each branch +is a bookmark referencing a particular point in the project history. +The gitlink:git-branch[1] command shows you the list of branches: + +------------------------------------------------ +$ git branch +* master +------------------------------------------------ + +A freshly cloned repository contains a single branch, named "master", +and the working directory contains the version of the project +referred to by the master branch. + +Most projects also use tags. Tags, like branches, are references +into the project's history, and can be listed using the +gitlink:git-tag[1] command: + +------------------------------------------------ +$ git tag -l +v2.6.11 +v2.6.11-tree +v2.6.12 +v2.6.12-rc2 +v2.6.12-rc3 +v2.6.12-rc4 +v2.6.12-rc5 +v2.6.12-rc6 +v2.6.13 +... +------------------------------------------------ + +Tags are expected to always point at the same version of a project, +while branches are expected to advance as development progresses. + +Create a new branch pointing to one of these versions and check it +out using gitlink:git-checkout[1]: + +------------------------------------------------ +$ git checkout -b new v2.6.13 +------------------------------------------------ + +The working directory then reflects the contents that the project had +when it was tagged v2.6.13, and gitlink:git-branch[1] shows two +branches, with an asterisk marking the currently checked-out branch: + +------------------------------------------------ +$ git branch + master +* new +------------------------------------------------ + +If you decide that you'd rather see version 2.6.17, you can modify +the current branch to point at v2.6.17 instead, with + +------------------------------------------------ +$ git reset --hard v2.6.17 +------------------------------------------------ + +Note that if the current branch was your only reference to a +particular point in history, then resetting that branch may leave you +with no way to find the history it used to point to; so use this +command carefully. + +Understanding History: Commits +------------------------------ + +Every change in the history of a project is represented by a commit. +The gitlink:git-show[1] command shows the most recent commit on the +current branch: + +------------------------------------------------ +$ git show +commit 2b5f6dcce5bf94b9b119e9ed8d537098ec61c3d2 +Author: Jamal Hadi Salim <hadi@cyberus.ca> +Date: Sat Dec 2 22:22:25 2006 -0800 + + [XFRM]: Fix aevent structuring to be more complete. + + aevents can not uniquely identify an SA. We break the ABI with this + patch, but consensus is that since it is not yet utilized by any + (known) application then it is fine (better do it now than later). + + Signed-off-by: Jamal Hadi Salim <hadi@cyberus.ca> + Signed-off-by: David S. Miller <davem@davemloft.net> + +diff --git a/Documentation/networking/xfrm_sync.txt b/Documentation/networking/xfrm_sync.txt +index 8be626f..d7aac9d 100644 +--- a/Documentation/networking/xfrm_sync.txt ++++ b/Documentation/networking/xfrm_sync.txt +@@ -47,10 +47,13 @@ aevent_id structure looks like: + + struct xfrm_aevent_id { + struct xfrm_usersa_id sa_id; ++ xfrm_address_t saddr; + __u32 flags; ++ __u32 reqid; + }; +... +------------------------------------------------ + +As you can see, a commit shows who made the latest change, what they +did, and why. + +Every commit has a 40-hexdigit id, sometimes called the "object name" +or the "SHA1 id", shown on the first line of the "git show" output. +You can usually refer to a commit by a shorter name, such as a tag or a +branch name, but this longer name can also be useful. Most +importantly, it is a globally unique name for this commit: so if you +tell somebody else the object name (for example in email), then you are +guaranteed that name will refer to the same commit in their repository +that it does in yours (assuming their repository has that commit at +all). + +Understanding history: commits, parents, and reachability +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Every commit (except the very first commit in a project) also has a +parent commit which shows what happened before this commit. +Following the chain of parents will eventually take you back to the +beginning of the project. + +However, the commits do not form a simple list; git allows lines of +development to diverge and then reconverge, and the point where two +lines of development reconverge is called a "merge". The commit +representing a merge can therefore have more than one parent, with +each parent representing the most recent commit on one of the lines +of development leading to that point. + +The best way to see how this works is using the gitlink:gitk[1] +command; running gitk now on a git repository and looking for merge +commits will help understand how the git organizes history. + +In the following, we say that commit X is "reachable" from commit Y +if commit X is an ancestor of commit Y. Equivalently, you could say +that Y is a descendent of X, or that there is a chain of parents +leading from commit Y to commit X. + +Understanding history: History diagrams +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We will sometimes represent git history using diagrams like the one +below. Commits are shown as "o", and the links between them with +lines drawn with - / and \. Time goes left to right: + + o--o--o <-- Branch A + / + o--o--o <-- master + \ + o--o--o <-- Branch B + +If we need to talk about a particular commit, the character "o" may +be replaced with another letter or number. + +Understanding history: What is a branch? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Though we've been using the word "branch" to mean a kind of reference +to a particular commit, the word branch is also commonly used to +refer to the line of commits leading up to that point. In the +example above, git may think of the branch named "A" as just a +pointer to one particular commit, but we may refer informally to the +line of three commits leading up to that point as all being part of +"branch A". + +If we need to make it clear that we're just talking about the most +recent commit on the branch, we may refer to that commit as the +"head" of the branch. + +Manipulating branches +--------------------- + +Creating, deleting, and modifying branches is quick and easy; here's +a summary of the commands: + +git branch:: + list all branches +git branch <branch>:: + create a new branch named <branch>, referencing the same + point in history as the current branch +git branch <branch> <start-point>:: + create a new branch named <branch>, referencing + <start-point>, which may be specified any way you like, + including using a branch name or a tag name +git branch -d <branch>:: + delete the branch <branch>; if the branch you are deleting + points to a commit which is not reachable from this branch, + this command will fail with a warning. +git branch -D <branch>:: + even if the branch points to a commit not reachable + from the current branch, you may know that that commit + is still reachable from some other branch or tag. In that + case it is safe to use this command to force git to delete + the branch. +git checkout <branch>:: + make the current branch <branch>, updating the working + directory to reflect the version referenced by <branch> +git checkout -b <new> <start-point>:: + create a new branch <new> referencing <start-point>, and + check it out. + +It is also useful to know that the special symbol "HEAD" can always +be used to refer to the current branch. + +Examining branches from a remote repository +------------------------------------------- + +The "master" branch that was created at the time you cloned is a copy +of the HEAD in the repository that you cloned from. That repository +may also have had other branches, though, and your local repository +keeps branches which track each of those remote branches, which you +can view using the "-r" option to gitlink:git-branch[1]: + +------------------------------------------------ +$ git branch -r + origin/HEAD + origin/html + origin/maint + origin/man + origin/master + origin/next + origin/pu + origin/todo +------------------------------------------------ + +You cannot check out these remote-tracking branches, but you can +examine them on a branch of your own, just as you would a tag: + +------------------------------------------------ +$ git checkout -b my-todo-copy origin/todo +------------------------------------------------ + +Note that the name "origin" is just the name that git uses by default +to refer to the repository that you cloned from. + +[[how-git-stores-references]] +Naming branches, tags, and other references +------------------------------------------- + +Branches, remote-tracking branches, and tags are all references to +commits. All references are named with a slash-separated path name +starting with "refs"; the names we've been using so far are actually +shorthand: + + - The branch "test" is short for "refs/heads/test". + - The tag "v2.6.18" is short for "refs/tags/v2.6.18". + - "origin/master" is short for "refs/remotes/origin/master". + +The full name is occasionally useful if, for example, there ever +exists a tag and a branch with the same name. + +As another useful shortcut, if the repository "origin" posesses only +a single branch, you can refer to that branch as just "origin". + +More generally, if you have defined a remote repository named +"example", you can refer to the branch in that repository as +"example". And for a repository with multiple branches, this will +refer to the branch designated as the "HEAD" branch. + +For the complete list of paths which git checks for references, and +the order it uses to decide which to choose when there are multiple +references with the same shorthand name, see the "SPECIFYING +REVISIONS" section of gitlink:git-rev-parse[1]. + +[[Updating-a-repository-with-git-fetch]] +Updating a repository with git fetch +------------------------------------ + +Eventually the developer cloned from will do additional work in her +repository, creating new commits and advancing the branches to point +at the new commits. + +The command "git fetch", with no arguments, will update all of the +remote-tracking branches to the latest version found in her +repository. It will not touch any of your own branches--not even the +"master" branch that was created for you on clone. + +Fetching branches from other repositories +----------------------------------------- + +You can also track branches from repositories other than the one you +cloned from, using gitlink:git-remote[1]: + +------------------------------------------------- +$ git remote add linux-nfs git://linux-nfs.org/pub/nfs-2.6.git +$ git fetch +* refs/remotes/linux-nfs/master: storing branch 'master' ... + commit: bf81b46 +------------------------------------------------- + +New remote-tracking branches will be stored under the shorthand name +that you gave "git remote add", in this case linux-nfs: + +------------------------------------------------- +$ git branch -r +linux-nfs/master +origin/master +------------------------------------------------- + +If you run "git fetch <remote>" later, the tracking branches for the +named <remote> will be updated. + +If you examine the file .git/config, you will see that git has added +a new stanza: + +------------------------------------------------- +$ cat .git/config +... +[remote "linux-nfs"] + url = git://linux-nfs.org/~bfields/git.git + fetch = +refs/heads/*:refs/remotes/linux-nfs-read/* +... +------------------------------------------------- + +This is what causes git to track the remote's branches; you may modify +or delete these configuration options by editing .git/config with a +text editor. (See the "CONFIGURATION FILE" section of +gitlink:git-config[1] for details.) + +Exploring git history +===================== + +Git is best thought of as a tool for storing the history of a +collection of files. It does this by storing compressed snapshots of +the contents of a file heirarchy, together with "commits" which show +the relationships between these snapshots. + +Git provides extremely flexible and fast tools for exploring the +history of a project. + +We start with one specialized tool that is useful for finding the +commit that introduced a bug into a project. + +How to use bisect to find a regression +-------------------------------------- + +Suppose version 2.6.18 of your project worked, but the version at +"master" crashes. Sometimes the best way to find the cause of such a +regression is to perform a brute-force search through the project's +history to find the particular commit that caused the problem. The +gitlink:git-bisect[1] command can help you do this: + +------------------------------------------------- +$ git bisect start +$ git bisect good v2.6.18 +$ git bisect bad master +Bisecting: 3537 revisions left to test after this +[65934a9a028b88e83e2b0f8b36618fe503349f8e] BLOCK: Make USB storage depend on SCSI rather than selecting it [try #6] +------------------------------------------------- + +If you run "git branch" at this point, you'll see that git has +temporarily moved you to a new branch named "bisect". This branch +points to a commit (with commit id 65934...) that is reachable from +v2.6.19 but not from v2.6.18. Compile and test it, and see whether +it crashes. Assume it does crash. Then: + +------------------------------------------------- +$ git bisect bad +Bisecting: 1769 revisions left to test after this +[7eff82c8b1511017ae605f0c99ac275a7e21b867] i2c-core: Drop useless bitmaskings +------------------------------------------------- + +checks out an older version. Continue like this, telling git at each +stage whether the version it gives you is good or bad, and notice +that the number of revisions left to test is cut approximately in +half each time. + +After about 13 tests (in this case), it will output the commit id of +the guilty commit. You can then examine the commit with +gitlink:git-show[1], find out who wrote it, and mail them your bug +report with the commit id. Finally, run + +------------------------------------------------- +$ git bisect reset +------------------------------------------------- + +to return you to the branch you were on before and delete the +temporary "bisect" branch. + +Note that the version which git-bisect checks out for you at each +point is just a suggestion, and you're free to try a different +version if you think it would be a good idea. For example, +occasionally you may land on a commit that broke something unrelated; +run + +------------------------------------------------- +$ git bisect-visualize +------------------------------------------------- + +which will run gitk and label the commit it chose with a marker that +says "bisect". Chose a safe-looking commit nearby, note its commit +id, and check it out with: + +------------------------------------------------- +$ git reset --hard fb47ddb2db... +------------------------------------------------- + +then test, run "bisect good" or "bisect bad" as appropriate, and +continue. + +Naming commits +-------------- + +We have seen several ways of naming commits already: + + - 40-hexdigit object name + - branch name: refers to the commit at the head of the given + branch + - tag name: refers to the commit pointed to by the given tag + (we've seen branches and tags are special cases of + <<how-git-stores-references,references>>). + - HEAD: refers to the head of the current branch + +There are many more; see the "SPECIFYING REVISIONS" section of the +gitlink:git-rev-parse[1] man page for the complete list of ways to +name revisions. Some examples: + +------------------------------------------------- +$ git show fb47ddb2 # the first few characters of the object name + # are usually enough to specify it uniquely +$ git show HEAD^ # the parent of the HEAD commit +$ git show HEAD^^ # the grandparent +$ git show HEAD~4 # the great-great-grandparent +------------------------------------------------- + +Recall that merge commits may have more than one parent; by default, +^ and ~ follow the first parent listed in the commit, but you can +also choose: + +------------------------------------------------- +$ git show HEAD^1 # show the first parent of HEAD +$ git show HEAD^2 # show the second parent of HEAD +------------------------------------------------- + +In addition to HEAD, there are several other special names for +commits: + +Merges (to be discussed later), as well as operations such as +git-reset, which change the currently checked-out commit, generally +set ORIG_HEAD to the value HEAD had before the current operation. + +The git-fetch operation always stores the head of the last fetched +branch in FETCH_HEAD. For example, if you run git fetch without +specifying a local branch as the target of the operation + +------------------------------------------------- +$ git fetch git://example.com/proj.git theirbranch +------------------------------------------------- + +the fetched commits will still be available from FETCH_HEAD. + +When we discuss merges we'll also see the special name MERGE_HEAD, +which refers to the other branch that we're merging in to the current +branch. + +The gitlink:git-rev-parse[1] command is a low-level command that is +occasionally useful for translating some name for a commit to the object +name for that commit: + +------------------------------------------------- +$ git rev-parse origin +e05db0fd4f31dde7005f075a84f96b360d05984b +------------------------------------------------- + +Creating tags +------------- + +We can also create a tag to refer to a particular commit; after +running + +------------------------------------------------- +$ git-tag stable-1 1b2e1d63ff +------------------------------------------------- + +You can use stable-1 to refer to the commit 1b2e1d63ff. + +This creates a "lightweight" tag. If the tag is a tag you wish to +share with others, and possibly sign cryptographically, then you +should create a tag object instead; see the gitlink:git-tag[1] man +page for details. + +Browsing revisions +------------------ + +The gitlink:git-log[1] command can show lists of commits. On its +own, it shows all commits reachable from the parent commit; but you +can also make more specific requests: + +------------------------------------------------- +$ git log v2.5.. # commits since (not reachable from) v2.5 +$ git log test..master # commits reachable from master but not test +$ git log master..test # ...reachable from test but not master +$ git log master...test # ...reachable from either test or master, + # but not both +$ git log --since="2 weeks ago" # commits from the last 2 weeks +$ git log Makefile # commits which modify Makefile +$ git log fs/ # ... which modify any file under fs/ +$ git log -S'foo()' # commits which add or remove any file data + # matching the string 'foo()' +------------------------------------------------- + +And of course you can combine all of these; the following finds +commits since v2.5 which touch the Makefile or any file under fs: + +------------------------------------------------- +$ git log v2.5.. Makefile fs/ +------------------------------------------------- + +You can also ask git log to show patches: + +------------------------------------------------- +$ git log -p +------------------------------------------------- + +See the "--pretty" option in the gitlink:git-log[1] man page for more +display options. + +Note that git log starts with the most recent commit and works +backwards through the parents; however, since git history can contain +multiple independent lines of development, the particular order that +commits are listed in may be somewhat arbitrary. + +Generating diffs +---------------- + +You can generate diffs between any two versions using +gitlink:git-diff[1]: + +------------------------------------------------- +$ git diff master..test +------------------------------------------------- + +Sometimes what you want instead is a set of patches: + +------------------------------------------------- +$ git format-patch master..test +------------------------------------------------- + +will generate a file with a patch for each commit reachable from test +but not from master. Note that if master also has commits which are +not reachable from test, then the combined result of these patches +will not be the same as the diff produced by the git-diff example. + +Viewing old file versions +------------------------- + +You can always view an old version of a file by just checking out the +correct revision first. But sometimes it is more convenient to be +able to view an old version of a single file without checking +anything out; this command does that: + +------------------------------------------------- +$ git show v2.5:fs/locks.c +------------------------------------------------- + +Before the colon may be anything that names a commit, and after it +may be any path to a file tracked by git. + +Examples +-------- + +Check whether two branches point at the same history +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you want to check whether two branches point at the same point +in history. + +------------------------------------------------- +$ git diff origin..master +------------------------------------------------- + +will tell you whether the contents of the project are the same at the +two branches; in theory, however, it's possible that the same project +contents could have been arrived at by two different historical +routes. You could compare the object names: + +------------------------------------------------- +$ git rev-list origin +e05db0fd4f31dde7005f075a84f96b360d05984b +$ git rev-list master +e05db0fd4f31dde7005f075a84f96b360d05984b +------------------------------------------------- + +Or you could recall that the ... operator selects all commits +contained reachable from either one reference or the other but not +both: so + +------------------------------------------------- +$ git log origin...master +------------------------------------------------- + +will return no commits when the two branches are equal. + +Find first tagged version including a given fix +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you know that the commit e05db0fd fixed a certain problem. +You'd like to find the earliest tagged release that contains that +fix. + +Of course, there may be more than one answer--if the history branched +after commit e05db0fd, then there could be multiple "earliest" tagged +releases. + +You could just visually inspect the commits since e05db0fd: + +------------------------------------------------- +$ gitk e05db0fd.. +------------------------------------------------- + +Or you can use gitlink:git-name-rev[1], which will give the commit a +name based on any tag it finds pointing to one of the commit's +descendants: + +------------------------------------------------- +$ git name-rev e05db0fd +e05db0fd tags/v1.5.0-rc1^0~23 +------------------------------------------------- + +The gitlink:git-describe[1] command does the opposite, naming the +revision using a tag on which the given commit is based: + +------------------------------------------------- +$ git describe e05db0fd +v1.5.0-rc0-ge05db0f +------------------------------------------------- + +but that may sometimes help you guess which tags might come after the +given commit. + +If you just want to verify whether a given tagged version contains a +given commit, you could use gitlink:git-merge-base[1]: + +------------------------------------------------- +$ git merge-base e05db0fd v1.5.0-rc1 +e05db0fd4f31dde7005f075a84f96b360d05984b +------------------------------------------------- + +The merge-base command finds a common ancestor of the given commits, +and always returns one or the other in the case where one is a +descendant of the other; so the above output shows that e05db0fd +actually is an ancestor of v1.5.0-rc1. + +Alternatively, note that + +------------------------------------------------- +$ git log v1.5.0-rc1..e05db0fd +------------------------------------------------- + +will produce empty output if and only if v1.5.0-rc1 includes e05db0fd, +because it outputs only commits that are not reachable from v1.5.0-rc1. + +As yet another alternative, the gitlink:git-show-branch[1] command lists +the commits reachable from its arguments with a display on the left-hand +side that indicates which arguments that commit is reachable from. So, +you can run something like + +------------------------------------------------- +$ git show-branch e05db0fd v1.5.0-rc0 v1.5.0-rc1 v1.5.0-rc2 +! [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if +available + ! [v1.5.0-rc0] GIT v1.5.0 preview + ! [v1.5.0-rc1] GIT v1.5.0-rc1 + ! [v1.5.0-rc2] GIT v1.5.0-rc2 +... +------------------------------------------------- + +then search for a line that looks like + +------------------------------------------------- ++ ++ [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if +available +------------------------------------------------- + +Which shows that e05db0fd is reachable from itself, from v1.5.0-rc1, and +from v1.5.0-rc2, but not from v1.5.0-rc0. + + +Developing with git +=================== + +Telling git your name +--------------------- + +Before creating any commits, you should introduce yourself to git. The +easiest way to do so is: + +------------------------------------------------ +$ cat >~/.gitconfig <<\EOF +[user] + name = Your Name Comes Here + email = you@yourdomain.example.com +EOF +------------------------------------------------ + +(See the "CONFIGURATION FILE" section of gitlink:git-config[1] for +details on the configuration file.) + + +Creating a new repository +------------------------- + +Creating a new repository from scratch is very easy: + +------------------------------------------------- +$ mkdir project +$ cd project +$ git init +------------------------------------------------- + +If you have some initial content (say, a tarball): + +------------------------------------------------- +$ tar -xzvf project.tar.gz +$ cd project +$ git init +$ git add . # include everything below ./ in the first commit: +$ git commit +------------------------------------------------- + +[[how-to-make-a-commit]] +how to make a commit +-------------------- + +Creating a new commit takes three steps: + + 1. Making some changes to the working directory using your + favorite editor. + 2. Telling git about your changes. + 3. Creating the commit using the content you told git about + in step 2. + +In practice, you can interleave and repeat steps 1 and 2 as many +times as you want: in order to keep track of what you want committed +at step 3, git maintains a snapshot of the tree's contents in a +special staging area called "the index." + +At the beginning, the content of the index will be identical to +that of the HEAD. The command "git diff --cached", which shows +the difference between the HEAD and the index, should therefore +produce no output at that point. + +Modifying the index is easy: + +To update the index with the new contents of a modified file, use + +------------------------------------------------- +$ git add path/to/file +------------------------------------------------- + +To add the contents of a new file to the index, use + +------------------------------------------------- +$ git add path/to/file +------------------------------------------------- + +To remove a file from the index and from the working tree, + +------------------------------------------------- +$ git rm path/to/file +------------------------------------------------- + +After each step you can verify that + +------------------------------------------------- +$ git diff --cached +------------------------------------------------- + +always shows the difference between the HEAD and the index file--this +is what you'd commit if you created the commit now--and that + +------------------------------------------------- +$ git diff +------------------------------------------------- + +shows the difference between the working tree and the index file. + +Note that "git add" always adds just the current contents of a file +to the index; further changes to the same file will be ignored unless +you run git-add on the file again. + +When you're ready, just run + +------------------------------------------------- +$ git commit +------------------------------------------------- + +and git will prompt you for a commit message and then create the new +commit. Check to make sure it looks like what you expected with + +------------------------------------------------- +$ git show +------------------------------------------------- + +As a special shortcut, + +------------------------------------------------- +$ git commit -a +------------------------------------------------- + +will update the index with any files that you've modified or removed +and create a commit, all in one step. + +A number of commands are useful for keeping track of what you're +about to commit: + +------------------------------------------------- +$ git diff --cached # difference between HEAD and the index; what + # would be commited if you ran "commit" now. +$ git diff # difference between the index file and your + # working directory; changes that would not + # be included if you ran "commit" now. +$ git status # a brief per-file summary of the above. +------------------------------------------------- + +creating good commit messages +----------------------------- + +Though not required, it's a good idea to begin the commit message +with a single short (less than 50 character) line summarizing the +change, followed by a blank line and then a more thorough +description. Tools that turn commits into email, for example, use +the first line on the Subject line and the rest of the commit in the +body. + +how to merge +------------ + +You can rejoin two diverging branches of development using +gitlink:git-merge[1]: + +------------------------------------------------- +$ git merge branchname +------------------------------------------------- + +merges the development in the branch "branchname" into the current +branch. If there are conflicts--for example, if the same file is +modified in two different ways in the remote branch and the local +branch--then you are warned; the output may look something like this: + +------------------------------------------------- +$ git pull . next +Trying really trivial in-index merge... +fatal: Merge requires file-level merging +Nope. +Merging HEAD with 77976da35a11db4580b80ae27e8d65caf5208086 +Merging: +15e2162 world +77976da goodbye +found 1 common ancestor(s): +d122ed4 initial +Auto-merging file.txt +CONFLICT (content): Merge conflict in file.txt +Automatic merge failed; fix conflicts and then commit the result. +------------------------------------------------- + +Conflict markers are left in the problematic files, and after +you resolve the conflicts manually, you can update the index +with the contents and run git commit, as you normally would when +creating a new file. + +If you examine the resulting commit using gitk, you will see that it +has two parents, one pointing to the top of the current branch, and +one to the top of the other branch. + +In more detail: + +[[resolving-a-merge]] +Resolving a merge +----------------- + +When a merge isn't resolved automatically, git leaves the index and +the working tree in a special state that gives you all the +information you need to help resolve the merge. + +Files with conflicts are marked specially in the index, so until you +resolve the problem and update the index, git commit will fail: + +------------------------------------------------- +$ git commit +file.txt: needs merge +------------------------------------------------- + +Also, git status will list those files as "unmerged". + +All of the changes that git was able to merge automatically are +already added to the index file, so gitlink:git-diff[1] shows only +the conflicts. Also, it uses a somewhat unusual syntax: + +------------------------------------------------- +$ git diff +diff --cc file.txt +index 802992c,2b60207..0000000 +--- a/file.txt ++++ b/file.txt +@@@ -1,1 -1,1 +1,5 @@@ +++<<<<<<< HEAD:file.txt + +Hello world +++======= ++ Goodbye +++>>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt +------------------------------------------------- + +Recall that the commit which will be commited after we resolve this +conflict will have two parents instead of the usual one: one parent +will be HEAD, the tip of the current branch; the other will be the +tip of the other branch, which is stored temporarily in MERGE_HEAD. + +The diff above shows the differences between the working-tree version +of file.txt and two previous version: one version from HEAD, and one +from MERGE_HEAD. So instead of preceding each line by a single "+" +or "-", it now uses two columns: the first column is used for +differences between the first parent and the working directory copy, +and the second for differences between the second parent and the +working directory copy. Thus after resolving the conflict in the +obvious way, the diff will look like: + +------------------------------------------------- +$ git diff +diff --cc file.txt +index 802992c,2b60207..0000000 +--- a/file.txt ++++ b/file.txt +@@@ -1,1 -1,1 +1,1 @@@ +- Hello world + -Goodbye +++Goodbye world +------------------------------------------------- + +This shows that our resolved version deleted "Hello world" from the +first parent, deleted "Goodbye" from the second parent, and added +"Goodbye world", which was previously absent from both. + +The gitlink:git-log[1] command also provides special help for merges: + +------------------------------------------------- +$ git log --merge +------------------------------------------------- + +This will list all commits which exist only on HEAD or on MERGE_HEAD, +and which touch an unmerged file. + +We can now add the resolved version to the index and commit: + +------------------------------------------------- +$ git add file.txt +$ git commit +------------------------------------------------- + +Note that the commit message will already be filled in for you with +some information about the merge. Normally you can just use this +default message unchanged, but you may add additional commentary of +your own if desired. + +[[undoing-a-merge]] +undoing a merge +--------------- + +If you get stuck and decide to just give up and throw the whole mess +away, you can always return to the pre-merge state with + +------------------------------------------------- +$ git reset --hard HEAD +------------------------------------------------- + +Or, if you've already commited the merge that you want to throw away, + +------------------------------------------------- +$ git reset --hard HEAD^ +------------------------------------------------- + +However, this last command can be dangerous in some cases--never +throw away a commit you have already committed if that commit may +itself have been merged into another branch, as doing so may confuse +further merges. + +Fast-forward merges +------------------- + +There is one special case not mentioned above, which is treated +differently. Normally, a merge results in a merge commit, with two +parents, one pointing at each of the two lines of development that +were merged. + +However, if one of the two lines of development is completely +contained within the other--so every commit present in the one is +already contained in the other--then git just performs a +<<fast-forwards,fast forward>>; the head of the current branch is +moved forward to point at the head of the merged-in branch, without +any new commits being created. + +Fixing mistakes +--------------- + +If you've messed up the working tree, but haven't yet committed your +mistake, you can return the entire working tree to the last committed +state with + +------------------------------------------------- +$ git reset --hard HEAD +------------------------------------------------- + +If you make a commit that you later wish you hadn't, there are two +fundamentally different ways to fix the problem: + + 1. You can create a new commit that undoes whatever was done + by the previous commit. This is the correct thing if your + mistake has already been made public. + + 2. You can go back and modify the old commit. You should + never do this if you have already made the history public; + git does not normally expect the "history" of a project to + change, and cannot correctly perform repeated merges from + a branch that has had its history changed. + +Fixing a mistake with a new commit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating a new commit that reverts an earlier change is very easy; +just pass the gitlink:git-revert[1] command a reference to the bad +commit; for example, to revert the most recent commit: + +------------------------------------------------- +$ git revert HEAD +------------------------------------------------- + +This will create a new commit which undoes the change in HEAD. You +will be given a chance to edit the commit message for the new commit. + +You can also revert an earlier change, for example, the next-to-last: + +------------------------------------------------- +$ git revert HEAD^ +------------------------------------------------- + +In this case git will attempt to undo the old change while leaving +intact any changes made since then. If more recent changes overlap +with the changes to be reverted, then you will be asked to fix +conflicts manually, just as in the case of <<resolving-a-merge, +resolving a merge>>. + +Fixing a mistake by editing history +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the problematic commit is the most recent commit, and you have not +yet made that commit public, then you may just +<<undoing-a-merge,destroy it using git-reset>>. + +Alternatively, you +can edit the working directory and update the index to fix your +mistake, just as if you were going to <<how-to-make-a-commit,create a +new commit>>, then run + +------------------------------------------------- +$ git commit --amend +------------------------------------------------- + +which will replace the old commit by a new commit incorporating your +changes, giving you a chance to edit the old commit message first. + +Again, you should never do this to a commit that may already have +been merged into another branch; use gitlink:git-revert[1] instead in +that case. + +It is also possible to edit commits further back in the history, but +this is an advanced topic to be left for +<<cleaning-up-history,another chapter>>. + +Checking out an old version of a file +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the process of undoing a previous bad change, you may find it +useful to check out an older version of a particular file using +gitlink:git-checkout[1]. We've used git checkout before to switch +branches, but it has quite different behavior if it is given a path +name: the command + +------------------------------------------------- +$ git checkout HEAD^ path/to/file +------------------------------------------------- + +replaces path/to/file by the contents it had in the commit HEAD^, and +also updates the index to match. It does not change branches. + +If you just want to look at an old version of the file, without +modifying the working directory, you can do that with +gitlink:git-show[1]: + +------------------------------------------------- +$ git show HEAD^ path/to/file +------------------------------------------------- + +which will display the given version of the file. + +Ensuring good performance +------------------------- + +On large repositories, git depends on compression to keep the history +information from taking up to much space on disk or in memory. + +This compression is not performed automatically. Therefore you +should occasionally run gitlink:git-gc[1]: + +------------------------------------------------- +$ git gc +------------------------------------------------- + +to recompress the archive. This can be very time-consuming, so +you may prefer to run git-gc when you are not doing other work. + +Ensuring reliability +-------------------- + +Checking the repository for corruption +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The gitlink:git-fsck[1] command runs a number of self-consistency checks +on the repository, and reports on any problems. This may take some +time. The most common warning by far is about "dangling" objects: + +------------------------------------------------- +$ git fsck +dangling commit 7281251ddd2a61e38657c827739c57015671a6b3 +dangling commit 2706a059f258c6b245f298dc4ff2ccd30ec21a63 +dangling commit 13472b7c4b80851a1bc551779171dcb03655e9b5 +dangling blob 218761f9d90712d37a9c5e36f406f92202db07eb +dangling commit bf093535a34a4d35731aa2bd90fe6b176302f14f +dangling commit 8e4bec7f2ddaa268bef999853c25755452100f8e +dangling tree d50bb86186bf27b681d25af89d3b5b68382e4085 +dangling tree b24c2473f1fd3d91352a624795be026d64c8841f +... +------------------------------------------------- + +Dangling objects are objects that are harmless, but also unnecessary; +you can remove them at any time with gitlink:git-prune[1] or the --prune +option to gitlink:git-gc[1]: + +------------------------------------------------- +$ git gc --prune +------------------------------------------------- + +This may be time-consuming. Unlike most other git operations (including +git-gc when run without any options), it is not safe to prune while +other git operations are in progress in the same repository. + +For more about dangling objects, see <<dangling-objects>>. + + +Recovering lost changes +~~~~~~~~~~~~~~~~~~~~~~~ + +Reflogs +^^^^^^^ + +Say you modify a branch with gitlink:git-reset[1] --hard, and then +realize that the branch was the only reference you had to that point in +history. + +Fortunately, git also keeps a log, called a "reflog", of all the +previous values of each branch. So in this case you can still find the +old history using, for example, + +------------------------------------------------- +$ git log master@{1} +------------------------------------------------- + +This lists the commits reachable from the previous version of the head. +This syntax can be used to with any git command that accepts a commit, +not just with git log. Some other examples: + +------------------------------------------------- +$ git show master@{2} # See where the branch pointed 2, +$ git show master@{3} # 3, ... changes ago. +$ gitk master@{yesterday} # See where it pointed yesterday, +$ gitk master@{"1 week ago"} # ... or last week +------------------------------------------------- + +The reflogs are kept by default for 30 days, after which they may be +pruned. See gitlink:git-reflog[1] and gitlink:git-gc[1] to learn +how to control this pruning, and see the "SPECIFYING REVISIONS" +section of gitlink:git-rev-parse[1] for details. + +Note that the reflog history is very different from normal git history. +While normal history is shared by every repository that works on the +same project, the reflog history is not shared: it tells you only about +how the branches in your local repository have changed over time. + +Examining dangling objects +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In some situations the reflog may not be able to save you. For +example, suppose you delete a branch, then realize you need the history +it pointed you. The reflog is also deleted; however, if you have not +yet pruned the repository, then you may still be able to find +the lost commits; run git-fsck and watch for output that mentions +"dangling commits": + +------------------------------------------------- +$ git fsck +dangling commit 7281251ddd2a61e38657c827739c57015671a6b3 +dangling commit 2706a059f258c6b245f298dc4ff2ccd30ec21a63 +dangling commit 13472b7c4b80851a1bc551779171dcb03655e9b5 +... +------------------------------------------------- + +You can examine +one of those dangling commits with, for example, + +------------------------------------------------ +$ gitk 7281251ddd --not --all +------------------------------------------------ + +which does what it sounds like: it says that you want to see the commit +history that is described by the dangling commit(s), but not the +history that is described by all your existing branches and tags. Thus +you get exactly the history reachable from that commit that is lost. +(And notice that it might not be just one commit: we only report the +"tip of the line" as being dangling, but there might be a whole deep +and complex commit history that was gotten dropped.) + +If you decide you want the history back, you can always create a new +reference pointing to it, for example, a new branch: + +------------------------------------------------ +$ git branch recovered-branch 7281251ddd +------------------------------------------------ + + +Sharing development with others +=============================== + +[[getting-updates-with-git-pull]] +Getting updates with git pull +----------------------------- + +After you clone a repository and make a few changes of your own, you +may wish to check the original repository for updates and merge them +into your own work. + +We have already seen <<Updating-a-repository-with-git-fetch,how to +keep remote tracking branches up to date>> with gitlink:git-fetch[1], +and how to merge two branches. So you can merge in changes from the +original repository's master branch with: + +------------------------------------------------- +$ git fetch +$ git merge origin/master +------------------------------------------------- + +However, the gitlink:git-pull[1] command provides a way to do this in +one step: + +------------------------------------------------- +$ git pull origin master +------------------------------------------------- + +In fact, "origin" is normally the default repository to pull from, +and the default branch is normally the HEAD of the remote repository, +so often you can accomplish the above with just + +------------------------------------------------- +$ git pull +------------------------------------------------- + +See the descriptions of the branch.<name>.remote and +branch.<name>.merge options in gitlink:git-config[1] to learn +how to control these defaults depending on the current branch. + +In addition to saving you keystrokes, "git pull" also helps you by +producing a default commit message documenting the branch and +repository that you pulled from. + +(But note that no such commit will be created in the case of a +<<fast-forwards,fast forward>>; instead, your branch will just be +updated to point to the latest commit from the upstream branch). + +The git-pull command can also be given "." as the "remote" repository, +in which case it just merges in a branch from the current repository; so +the commands + +------------------------------------------------- +$ git pull . branch +$ git merge branch +------------------------------------------------- + +are roughly equivalent. The former is actually very commonly used. + +Submitting patches to a project +------------------------------- + +If you just have a few changes, the simplest way to submit them may +just be to send them as patches in email: + +First, use gitlink:git-format-patch[1]; for example: + +------------------------------------------------- +$ git format-patch origin +------------------------------------------------- + +will produce a numbered series of files in the current directory, one +for each patch in the current branch but not in origin/HEAD. + +You can then import these into your mail client and send them by +hand. However, if you have a lot to send at once, you may prefer to +use the gitlink:git-send-email[1] script to automate the process. +Consult the mailing list for your project first to determine how they +prefer such patches be handled. + +Importing patches to a project +------------------------------ + +Git also provides a tool called gitlink:git-am[1] (am stands for +"apply mailbox"), for importing such an emailed series of patches. +Just save all of the patch-containing messages, in order, into a +single mailbox file, say "patches.mbox", then run + +------------------------------------------------- +$ git am -3 patches.mbox +------------------------------------------------- + +Git will apply each patch in order; if any conflicts are found, it +will stop, and you can fix the conflicts as described in +"<<resolving-a-merge,Resolving a merge>>". (The "-3" option tells +git to perform a merge; if you would prefer it just to abort and +leave your tree and index untouched, you may omit that option.) + +Once the index is updated with the results of the conflict +resolution, instead of creating a new commit, just run + +------------------------------------------------- +$ git am --resolved +------------------------------------------------- + +and git will create the commit for you and continue applying the +remaining patches from the mailbox. + +The final result will be a series of commits, one for each patch in +the original mailbox, with authorship and commit log message each +taken from the message containing each patch. + +[[setting-up-a-public-repository]] +Setting up a public repository +------------------------------ + +Another way to submit changes to a project is to simply tell the +maintainer of that project to pull from your repository, exactly as +you did in the section "<<getting-updates-with-git-pull, Getting +updates with git pull>>". + +If you and maintainer both have accounts on the same machine, then +then you can just pull changes from each other's repositories +directly; note that all of the command (gitlink:git-clone[1], +git-fetch[1], git-pull[1], etc.) which accept a URL as an argument +will also accept a local file patch; so, for example, you can +use + +------------------------------------------------- +$ git clone /path/to/repository +$ git pull /path/to/other/repository +------------------------------------------------- + +If this sort of setup is inconvenient or impossible, another (more +common) option is to set up a public repository on a public server. +This also allows you to cleanly separate private work in progress +from publicly visible work. + +You will continue to do your day-to-day work in your personal +repository, but periodically "push" changes from your personal +repository into your public repository, allowing other developers to +pull from that repository. So the flow of changes, in a situation +where there is one other developer with a public repository, looks +like this: + + you push + your personal repo ------------------> your public repo + ^ | + | | + | you pull | they pull + | | + | | + | they push V + their public repo <------------------- their repo + +Now, assume your personal repository is in the directory ~/proj. We +first create a new clone of the repository: + +------------------------------------------------- +$ git clone --bare proj-clone.git +------------------------------------------------- + +The resulting directory proj-clone.git will contains a "bare" git +repository--it is just the contents of the ".git" directory, without +a checked-out copy of a working directory. + +Next, copy proj-clone.git to the server where you plan to host the +public repository. You can use scp, rsync, or whatever is most +convenient. + +If somebody else maintains the public server, they may already have +set up a git service for you, and you may skip to the section +"<<pushing-changes-to-a-public-repository,Pushing changes to a public +repository>>", below. + +Otherwise, the following sections explain how to export your newly +created public repository: + +[[exporting-via-http]] +Exporting a git repository via http +----------------------------------- + +The git protocol gives better performance and reliability, but on a +host with a web server set up, http exports may be simpler to set up. + +All you need to do is place the newly created bare git repository in +a directory that is exported by the web server, and make some +adjustments to give web clients some extra information they need: + +------------------------------------------------- +$ mv proj.git /home/you/public_html/proj.git +$ cd proj.git +$ git update-server-info +$ chmod a+x hooks/post-update +------------------------------------------------- + +(For an explanation of the last two lines, see +gitlink:git-update-server-info[1], and the documentation +link:hooks.txt[Hooks used by git].) + +Advertise the url of proj.git. Anybody else should then be able to +clone or pull from that url, for example with a commandline like: + +------------------------------------------------- +$ git clone http://yourserver.com/~you/proj.git +------------------------------------------------- + +(See also +link:howto/setup-git-server-over-http.txt[setup-git-server-over-http] +for a slightly more sophisticated setup using WebDAV which also +allows pushing over http.) + +[[exporting-via-git]] +Exporting a git repository via the git protocol +----------------------------------------------- + +This is the preferred method. + +For now, we refer you to the gitlink:git-daemon[1] man page for +instructions. (See especially the examples section.) + +[[pushing-changes-to-a-public-repository]] +Pushing changes to a public repository +-------------------------------------- + +Note that the two techniques outline above (exporting via +<<exporting-via-http,http>> or <<exporting-via-git,git>>) allow other +maintainers to fetch your latest changes, but they do not allow write +access, which you will need to update the public repository with the +latest changes created in your private repository. + +The simplest way to do this is using gitlink:git-push[1] and ssh; to +update the remote branch named "master" with the latest state of your +branch named "master", run + +------------------------------------------------- +$ git push ssh://yourserver.com/~you/proj.git master:master +------------------------------------------------- + +or just + +------------------------------------------------- +$ git push ssh://yourserver.com/~you/proj.git master +------------------------------------------------- + +As with git-fetch, git-push will complain if this does not result in +a <<fast-forwards,fast forward>>. Normally this is a sign of +something wrong. However, if you are sure you know what you're +doing, you may force git-push to perform the update anyway by +proceeding the branch name by a plus sign: + +------------------------------------------------- +$ git push ssh://yourserver.com/~you/proj.git +master +------------------------------------------------- + +As with git-fetch, you may also set up configuration options to +save typing; so, for example, after + +------------------------------------------------- +$ cat >.git/config <<EOF +[remote "public-repo"] + url = ssh://yourserver.com/~you/proj.git +EOF +------------------------------------------------- + +you should be able to perform the above push with just + +------------------------------------------------- +$ git push public-repo master +------------------------------------------------- + +See the explanations of the remote.<name>.url, branch.<name>.remote, +and remote.<name>.push options in gitlink:git-config[1] for +details. + +Setting up a shared repository +------------------------------ + +Another way to collaborate is by using a model similar to that +commonly used in CVS, where several developers with special rights +all push to and pull from a single shared repository. See +link:cvs-migration.txt[git for CVS users] for instructions on how to +set this up. + +Allow web browsing of a repository +---------------------------------- + +The gitweb cgi script provides users an easy way to browse your +project's files and history without having to install git; see the file +gitweb/README in the git source tree for instructions on setting it up. + +Examples +-------- + +TODO: topic branches, typical roles as in everyday.txt, ? + + +[[cleaning-up-history]] +Rewriting history and maintaining patch series +============================================== + +Normally commits are only added to a project, never taken away or +replaced. Git is designed with this assumption, and violating it will +cause git's merge machinery (for example) to do the wrong thing. + +However, there is a situation in which it can be useful to violate this +assumption. + +Creating the perfect patch series +--------------------------------- + +Suppose you are a contributor to a large project, and you want to add a +complicated feature, and to present it to the other developers in a way +that makes it easy for them to read your changes, verify that they are +correct, and understand why you made each change. + +If you present all of your changes as a single patch (or commit), they +may find it is too much to digest all at once. + +If you present them with the entire history of your work, complete with +mistakes, corrections, and dead ends, they may be overwhelmed. + +So the ideal is usually to produce a series of patches such that: + + 1. Each patch can be applied in order. + + 2. Each patch includes a single logical change, together with a + message explaining the change. + + 3. No patch introduces a regression: after applying any initial + part of the series, the resulting project still compiles and + works, and has no bugs that it didn't have before. + + 4. The complete series produces the same end result as your own + (probably much messier!) development process did. + +We will introduce some tools that can help you do this, explain how to +use them, and then explain some of the problems that can arise because +you are rewriting history. + +Keeping a patch series up to date using git-rebase +-------------------------------------------------- + +Suppose you have a series of commits in a branch "mywork", which +originally branched off from "origin". + +Suppose you create a branch "mywork" on a remote-tracking branch +"origin", and created some commits on top of it: + +------------------------------------------------- +$ git checkout -b mywork origin +$ vi file.txt +$ git commit +$ vi otherfile.txt +$ git commit +... +------------------------------------------------- + +You have performed no merges into mywork, so it is just a simple linear +sequence of patches on top of "origin": + + + o--o--o <-- origin + \ + o--o--o <-- mywork + +Some more interesting work has been done in the upstream project, and +"origin" has advanced: + + o--o--O--o--o--o <-- origin + \ + a--b--c <-- mywork + +At this point, you could use "pull" to merge your changes back in; +the result would create a new merge commit, like this: + + + o--o--O--o--o--o <-- origin + \ \ + a--b--c--m <-- mywork + +However, if you prefer to keep the history in mywork a simple series of +commits without any merges, you may instead choose to use +gitlink:git-rebase[1]: + +------------------------------------------------- +$ git checkout mywork +$ git rebase origin +------------------------------------------------- + +This will remove each of your commits from mywork, temporarily saving +them as patches (in a directory named ".dotest"), update mywork to +point at the latest version of origin, then apply each of the saved +patches to the new mywork. The result will look like: + + + o--o--O--o--o--o <-- origin + \ + a'--b'--c' <-- mywork + +In the process, it may discover conflicts. In that case it will stop +and allow you to fix the conflicts; after fixing conflicts, use "git +add" to update the index with those contents, and then, instead of +running git-commit, just run + +------------------------------------------------- +$ git rebase --continue +------------------------------------------------- + +and git will continue applying the rest of the patches. + +At any point you may use the --abort option to abort this process and +return mywork to the state it had before you started the rebase: + +------------------------------------------------- +$ git rebase --abort +------------------------------------------------- + +Reordering or selecting from a patch series +------------------------------------------- + +Given one existing commit, the gitlink:git-cherry-pick[1] command +allows you to apply the change introduced by that commit and create a +new commit that records it. So, for example, if "mywork" points to a +series of patches on top of "origin", you might do something like: + +------------------------------------------------- +$ git checkout -b mywork-new origin +$ gitk origin..mywork & +------------------------------------------------- + +And browse through the list of patches in the mywork branch using gitk, +applying them (possibly in a different order) to mywork-new using +cherry-pick, and possibly modifying them as you go using commit +--amend. + +Another technique is to use git-format-patch to create a series of +patches, then reset the state to before the patches: + +------------------------------------------------- +$ git format-patch origin +$ git reset --hard origin +------------------------------------------------- + +Then modify, reorder, or eliminate patches as preferred before applying +them again with gitlink:git-am[1]. + +Other tools +----------- + +There are numerous other tools, such as stgit, which exist for the +purpose of maintaining a patch series. These are out of the scope of +this manual. + +Problems with rewriting history +------------------------------- + +The primary problem with rewriting the history of a branch has to do +with merging. Suppose somebody fetches your branch and merges it into +their branch, with a result something like this: + + o--o--O--o--o--o <-- origin + \ \ + t--t--t--m <-- their branch: + +Then suppose you modify the last three commits: + + o--o--o <-- new head of origin + / + o--o--O--o--o--o <-- old head of origin + +If we examined all this history together in one repository, it will +look like: + + o--o--o <-- new head of origin + / + o--o--O--o--o--o <-- old head of origin + \ \ + t--t--t--m <-- their branch: + +Git has no way of knowing that the new head is an updated version of +the old head; it treats this situation exactly the same as it would if +two developers had independently done the work on the old and new heads +in parallel. At this point, if someone attempts to merge the new head +in to their branch, git will attempt to merge together the two (old and +new) lines of development, instead of trying to replace the old by the +new. The results are likely to be unexpected. + +You may still choose to publish branches whose history is rewritten, +and it may be useful for others to be able to fetch those branches in +order to examine or test them, but they should not attempt to pull such +branches into their own work. + +For true distributed development that supports proper merging, +published branches should never be rewritten. + +Advanced branch management +========================== + +Fetching individual branches +---------------------------- + +Instead of using gitlink:git-remote[1], you can also choose just +to update one branch at a time, and to store it locally under an +arbitrary name: + +------------------------------------------------- +$ git fetch origin todo:my-todo-work +------------------------------------------------- + +The first argument, "origin", just tells git to fetch from the +repository you originally cloned from. The second argument tells git +to fetch the branch named "todo" from the remote repository, and to +store it locally under the name refs/heads/my-todo-work. + +You can also fetch branches from other repositories; so + +------------------------------------------------- +$ git fetch git://example.com/proj.git master:example-master +------------------------------------------------- + +will create a new branch named "example-master" and store in it the +branch named "master" from the repository at the given URL. If you +already have a branch named example-master, it will attempt to +"fast-forward" to the commit given by example.com's master branch. So +next we explain what a fast-forward is: + +[[fast-forwards]] +Understanding git history: fast-forwards +---------------------------------------- + +In the previous example, when updating an existing branch, "git +fetch" checks to make sure that the most recent commit on the remote +branch is a descendant of the most recent commit on your copy of the +branch before updating your copy of the branch to point at the new +commit. Git calls this process a "fast forward". + +A fast forward looks something like this: + + o--o--o--o <-- old head of the branch + \ + o--o--o <-- new head of the branch + + +In some cases it is possible that the new head will *not* actually be +a descendant of the old head. For example, the developer may have +realized she made a serious mistake, and decided to backtrack, +resulting in a situation like: + + o--o--o--o--a--b <-- old head of the branch + \ + o--o--o <-- new head of the branch + + + +In this case, "git fetch" will fail, and print out a warning. + +In that case, you can still force git to update to the new head, as +described in the following section. However, note that in the +situation above this may mean losing the commits labeled "a" and "b", +unless you've already created a reference of your own pointing to +them. + +Forcing git fetch to do non-fast-forward updates +------------------------------------------------ + +If git fetch fails because the new head of a branch is not a +descendant of the old head, you may force the update with: + +------------------------------------------------- +$ git fetch git://example.com/proj.git +master:refs/remotes/example/master +------------------------------------------------- + +Note the addition of the "+" sign. Be aware that commits which the +old version of example/master pointed at may be lost, as we saw in +the previous section. + +Configuring remote branches +--------------------------- + +We saw above that "origin" is just a shortcut to refer to the +repository which you originally cloned from. This information is +stored in git configuration variables, which you can see using +gitlink:git-config[1]: + +------------------------------------------------- +$ git config -l +core.repositoryformatversion=0 +core.filemode=true +core.logallrefupdates=true +remote.origin.url=git://git.kernel.org/pub/scm/git/git.git +remote.origin.fetch=+refs/heads/*:refs/remotes/origin/* +branch.master.remote=origin +branch.master.merge=refs/heads/master +------------------------------------------------- + +If there are other repositories that you also use frequently, you can +create similar configuration options to save typing; for example, +after + +------------------------------------------------- +$ git config remote.example.url git://example.com/proj.git +------------------------------------------------- + +then the following two commands will do the same thing: + +------------------------------------------------- +$ git fetch git://example.com/proj.git master:refs/remotes/example/master +$ git fetch example master:refs/remotes/example/master +------------------------------------------------- + +Even better, if you add one more option: + +------------------------------------------------- +$ git config remote.example.fetch master:refs/remotes/example/master +------------------------------------------------- + +then the following commands will all do the same thing: + +------------------------------------------------- +$ git fetch git://example.com/proj.git master:ref/remotes/example/master +$ git fetch example master:ref/remotes/example/master +$ git fetch example example/master +$ git fetch example +------------------------------------------------- + +You can also add a "+" to force the update each time: + +------------------------------------------------- +$ git config remote.example.fetch +master:ref/remotes/example/master +------------------------------------------------- + +Don't do this unless you're sure you won't mind "git fetch" possibly +throwing away commits on mybranch. + +Also note that all of the above configuration can be performed by +directly editing the file .git/config instead of using +gitlink:git-config[1]. + +See gitlink:git-config[1] for more details on the configuration +options mentioned above. + + +Git internals +============= + +There are two object abstractions: the "object database", and the +"current directory cache" aka "index". + +The Object Database +------------------- + +The object database is literally just a content-addressable collection +of objects. All objects are named by their content, which is +approximated by the SHA1 hash of the object itself. Objects may refer +to other objects (by referencing their SHA1 hash), and so you can +build up a hierarchy of objects. + +All objects have a statically determined "type" aka "tag", which is +determined at object creation time, and which identifies the format of +the object (i.e. how it is used, and how it can refer to other +objects). There are currently four different object types: "blob", +"tree", "commit" and "tag". + +A "blob" object cannot refer to any other object, and is, like the type +implies, a pure storage object containing some user data. It is used to +actually store the file data, i.e. a blob object is associated with some +particular version of some file. + +A "tree" object is an object that ties one or more "blob" objects into a +directory structure. In addition, a tree object can refer to other tree +objects, thus creating a directory hierarchy. + +A "commit" object ties such directory hierarchies together into +a DAG of revisions - each "commit" is associated with exactly one tree +(the directory hierarchy at the time of the commit). In addition, a +"commit" refers to one or more "parent" commit objects that describe the +history of how we arrived at that directory hierarchy. + +As a special case, a commit object with no parents is called the "root" +object, and is the point of an initial project commit. Each project +must have at least one root, and while you can tie several different +root objects together into one project by creating a commit object which +has two or more separate roots as its ultimate parents, that's probably +just going to confuse people. So aim for the notion of "one root object +per project", even if git itself does not enforce that. + +A "tag" object symbolically identifies and can be used to sign other +objects. It contains the identifier and type of another object, a +symbolic name (of course!) and, optionally, a signature. + +Regardless of object type, all objects share the following +characteristics: they are all deflated with zlib, and have a header +that not only specifies their type, but also provides size information +about the data in the object. It's worth noting that the SHA1 hash +that is used to name the object is the hash of the original data +plus this header, so `sha1sum` 'file' does not match the object name +for 'file'. +(Historical note: in the dawn of the age of git the hash +was the sha1 of the 'compressed' object.) + +As a result, the general consistency of an object can always be tested +independently of the contents or the type of the object: all objects can +be validated by verifying that (a) their hashes match the content of the +file and (b) the object successfully inflates to a stream of bytes that +forms a sequence of <ascii type without space> + <space> + <ascii decimal +size> + <byte\0> + <binary object data>. + +The structured objects can further have their structure and +connectivity to other objects verified. This is generally done with +the `git-fsck` program, which generates a full dependency graph +of all objects, and verifies their internal consistency (in addition +to just verifying their superficial consistency through the hash). + +The object types in some more detail: + +Blob Object +----------- + +A "blob" object is nothing but a binary blob of data, and doesn't +refer to anything else. There is no signature or any other +verification of the data, so while the object is consistent (it 'is' +indexed by its sha1 hash, so the data itself is certainly correct), it +has absolutely no other attributes. No name associations, no +permissions. It is purely a blob of data (i.e. normally "file +contents"). + +In particular, since the blob is entirely defined by its data, if two +files in a directory tree (or in multiple different versions of the +repository) have the same contents, they will share the same blob +object. The object is totally independent of its location in the +directory tree, and renaming a file does not change the object that +file is associated with in any way. + +A blob is typically created when gitlink:git-update-index[1] +is run, and its data can be accessed by gitlink:git-cat-file[1]. + +Tree Object +----------- + +The next hierarchical object type is the "tree" object. A tree object +is a list of mode/name/blob data, sorted by name. Alternatively, the +mode data may specify a directory mode, in which case instead of +naming a blob, that name is associated with another TREE object. + +Like the "blob" object, a tree object is uniquely determined by the +set contents, and so two separate but identical trees will always +share the exact same object. This is true at all levels, i.e. it's +true for a "leaf" tree (which does not refer to any other trees, only +blobs) as well as for a whole subdirectory. + +For that reason a "tree" object is just a pure data abstraction: it +has no history, no signatures, no verification of validity, except +that since the contents are again protected by the hash itself, we can +trust that the tree is immutable and its contents never change. + +So you can trust the contents of a tree to be valid, the same way you +can trust the contents of a blob, but you don't know where those +contents 'came' from. + +Side note on trees: since a "tree" object is a sorted list of +"filename+content", you can create a diff between two trees without +actually having to unpack two trees. Just ignore all common parts, +and your diff will look right. In other words, you can effectively +(and efficiently) tell the difference between any two random trees by +O(n) where "n" is the size of the difference, rather than the size of +the tree. + +Side note 2 on trees: since the name of a "blob" depends entirely and +exclusively on its contents (i.e. there are no names or permissions +involved), you can see trivial renames or permission changes by +noticing that the blob stayed the same. However, renames with data +changes need a smarter "diff" implementation. + +A tree is created with gitlink:git-write-tree[1] and +its data can be accessed by gitlink:git-ls-tree[1]. +Two trees can be compared with gitlink:git-diff-tree[1]. + +Commit Object +------------- + +The "commit" object is an object that introduces the notion of +history into the picture. In contrast to the other objects, it +doesn't just describe the physical state of a tree, it describes how +we got there, and why. + +A "commit" is defined by the tree-object that it results in, the +parent commits (zero, one or more) that led up to that point, and a +comment on what happened. Again, a commit is not trusted per se: +the contents are well-defined and "safe" due to the cryptographically +strong signatures at all levels, but there is no reason to believe +that the tree is "good" or that the merge information makes sense. +The parents do not have to actually have any relationship with the +result, for example. + +Note on commits: unlike real SCM's, commits do not contain +rename information or file mode change information. All of that is +implicit in the trees involved (the result tree, and the result trees +of the parents), and describing that makes no sense in this idiotic +file manager. + +A commit is created with gitlink:git-commit-tree[1] and +its data can be accessed by gitlink:git-cat-file[1]. + +Trust +----- + +An aside on the notion of "trust". Trust is really outside the scope +of "git", but it's worth noting a few things. First off, since +everything is hashed with SHA1, you 'can' trust that an object is +intact and has not been messed with by external sources. So the name +of an object uniquely identifies a known state - just not a state that +you may want to trust. + +Furthermore, since the SHA1 signature of a commit refers to the +SHA1 signatures of the tree it is associated with and the signatures +of the parent, a single named commit specifies uniquely a whole set +of history, with full contents. You can't later fake any step of the +way once you have the name of a commit. + +So to introduce some real trust in the system, the only thing you need +to do is to digitally sign just 'one' special note, which includes the +name of a top-level commit. Your digital signature shows others +that you trust that commit, and the immutability of the history of +commits tells others that they can trust the whole history. + +In other words, you can easily validate a whole archive by just +sending out a single email that tells the people the name (SHA1 hash) +of the top commit, and digitally sign that email using something +like GPG/PGP. + +To assist in this, git also provides the tag object... + +Tag Object +---------- + +Git provides the "tag" object to simplify creating, managing and +exchanging symbolic and signed tokens. The "tag" object at its +simplest simply symbolically identifies another object by containing +the sha1, type and symbolic name. + +However it can optionally contain additional signature information +(which git doesn't care about as long as there's less than 8k of +it). This can then be verified externally to git. + +Note that despite the tag features, "git" itself only handles content +integrity; the trust framework (and signature provision and +verification) has to come from outside. + +A tag is created with gitlink:git-mktag[1], +its data can be accessed by gitlink:git-cat-file[1], +and the signature can be verified by +gitlink:git-verify-tag[1]. + + +The "index" aka "Current Directory Cache" +----------------------------------------- + +The index is a simple binary file, which contains an efficient +representation of a virtual directory content at some random time. It +does so by a simple array that associates a set of names, dates, +permissions and content (aka "blob") objects together. The cache is +always kept ordered by name, and names are unique (with a few very +specific rules) at any point in time, but the cache has no long-term +meaning, and can be partially updated at any time. + +In particular, the index certainly does not need to be consistent with +the current directory contents (in fact, most operations will depend on +different ways to make the index 'not' be consistent with the directory +hierarchy), but it has three very important attributes: + +'(a) it can re-generate the full state it caches (not just the +directory structure: it contains pointers to the "blob" objects so +that it can regenerate the data too)' + +As a special case, there is a clear and unambiguous one-way mapping +from a current directory cache to a "tree object", which can be +efficiently created from just the current directory cache without +actually looking at any other data. So a directory cache at any one +time uniquely specifies one and only one "tree" object (but has +additional data to make it easy to match up that tree object with what +has happened in the directory) + +'(b) it has efficient methods for finding inconsistencies between that +cached state ("tree object waiting to be instantiated") and the +current state.' + +'(c) it can additionally efficiently represent information about merge +conflicts between different tree objects, allowing each pathname to be +associated with sufficient information about the trees involved that +you can create a three-way merge between them.' + +Those are the three ONLY things that the directory cache does. It's a +cache, and the normal operation is to re-generate it completely from a +known tree object, or update/compare it with a live tree that is being +developed. If you blow the directory cache away entirely, you generally +haven't lost any information as long as you have the name of the tree +that it described. + +At the same time, the index is at the same time also the +staging area for creating new trees, and creating a new tree always +involves a controlled modification of the index file. In particular, +the index file can have the representation of an intermediate tree that +has not yet been instantiated. So the index can be thought of as a +write-back cache, which can contain dirty information that has not yet +been written back to the backing store. + + + +The Workflow +------------ + +Generally, all "git" operations work on the index file. Some operations +work *purely* on the index file (showing the current state of the +index), but most operations move data to and from the index file. Either +from the database or from the working directory. Thus there are four +main combinations: + +working directory -> index +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You update the index with information from the working directory with +the gitlink:git-update-index[1] command. You +generally update the index information by just specifying the filename +you want to update, like so: + +------------------------------------------------- +$ git-update-index filename +------------------------------------------------- + +but to avoid common mistakes with filename globbing etc, the command +will not normally add totally new entries or remove old entries, +i.e. it will normally just update existing cache entries. + +To tell git that yes, you really do realize that certain files no +longer exist, or that new files should be added, you +should use the `--remove` and `--add` flags respectively. + +NOTE! A `--remove` flag does 'not' mean that subsequent filenames will +necessarily be removed: if the files still exist in your directory +structure, the index will be updated with their new status, not +removed. The only thing `--remove` means is that update-cache will be +considering a removed file to be a valid thing, and if the file really +does not exist any more, it will update the index accordingly. + +As a special case, you can also do `git-update-index --refresh`, which +will refresh the "stat" information of each index to match the current +stat information. It will 'not' update the object status itself, and +it will only update the fields that are used to quickly test whether +an object still matches its old backing store object. + +index -> object database +~~~~~~~~~~~~~~~~~~~~~~~~ + +You write your current index file to a "tree" object with the program + +------------------------------------------------- +$ git-write-tree +------------------------------------------------- + +that doesn't come with any options - it will just write out the +current index into the set of tree objects that describe that state, +and it will return the name of the resulting top-level tree. You can +use that tree to re-generate the index at any time by going in the +other direction: + +object database -> index +~~~~~~~~~~~~~~~~~~~~~~~~ + +You read a "tree" file from the object database, and use that to +populate (and overwrite - don't do this if your index contains any +unsaved state that you might want to restore later!) your current +index. Normal operation is just + +------------------------------------------------- +$ git-read-tree <sha1 of tree> +------------------------------------------------- + +and your index file will now be equivalent to the tree that you saved +earlier. However, that is only your 'index' file: your working +directory contents have not been modified. + +index -> working directory +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You update your working directory from the index by "checking out" +files. This is not a very common operation, since normally you'd just +keep your files updated, and rather than write to your working +directory, you'd tell the index files about the changes in your +working directory (i.e. `git-update-index`). + +However, if you decide to jump to a new version, or check out somebody +else's version, or just restore a previous tree, you'd populate your +index file with read-tree, and then you need to check out the result +with + +------------------------------------------------- +$ git-checkout-index filename +------------------------------------------------- + +or, if you want to check out all of the index, use `-a`. + +NOTE! git-checkout-index normally refuses to overwrite old files, so +if you have an old version of the tree already checked out, you will +need to use the "-f" flag ('before' the "-a" flag or the filename) to +'force' the checkout. + + +Finally, there are a few odds and ends which are not purely moving +from one representation to the other: + +Tying it all together +~~~~~~~~~~~~~~~~~~~~~ + +To commit a tree you have instantiated with "git-write-tree", you'd +create a "commit" object that refers to that tree and the history +behind it - most notably the "parent" commits that preceded it in +history. + +Normally a "commit" has one parent: the previous state of the tree +before a certain change was made. However, sometimes it can have two +or more parent commits, in which case we call it a "merge", due to the +fact that such a commit brings together ("merges") two or more +previous states represented by other commits. + +In other words, while a "tree" represents a particular directory state +of a working directory, a "commit" represents that state in "time", +and explains how we got there. + +You create a commit object by giving it the tree that describes the +state at the time of the commit, and a list of parents: + +------------------------------------------------- +$ git-commit-tree <tree> -p <parent> [-p <parent2> ..] +------------------------------------------------- + +and then giving the reason for the commit on stdin (either through +redirection from a pipe or file, or by just typing it at the tty). + +git-commit-tree will return the name of the object that represents +that commit, and you should save it away for later use. Normally, +you'd commit a new `HEAD` state, and while git doesn't care where you +save the note about that state, in practice we tend to just write the +result to the file pointed at by `.git/HEAD`, so that we can always see +what the last committed state was. + +Here is an ASCII art by Jon Loeliger that illustrates how +various pieces fit together. + +------------ + + commit-tree + commit obj + +----+ + | | + | | + V V + +-----------+ + | Object DB | + | Backing | + | Store | + +-----------+ + ^ + write-tree | | + tree obj | | + | | read-tree + | | tree obj + V + +-----------+ + | Index | + | "cache" | + +-----------+ + update-index ^ + blob obj | | + | | + checkout-index -u | | checkout-index + stat | | blob obj + V + +-----------+ + | Working | + | Directory | + +-----------+ + +------------ + + +Examining the data +------------------ + +You can examine the data represented in the object database and the +index with various helper tools. For every object, you can use +gitlink:git-cat-file[1] to examine details about the +object: + +------------------------------------------------- +$ git-cat-file -t <objectname> +------------------------------------------------- + +shows the type of the object, and once you have the type (which is +usually implicit in where you find the object), you can use + +------------------------------------------------- +$ git-cat-file blob|tree|commit|tag <objectname> +------------------------------------------------- + +to show its contents. NOTE! Trees have binary content, and as a result +there is a special helper for showing that content, called +`git-ls-tree`, which turns the binary content into a more easily +readable form. + +It's especially instructive to look at "commit" objects, since those +tend to be small and fairly self-explanatory. In particular, if you +follow the convention of having the top commit name in `.git/HEAD`, +you can do + +------------------------------------------------- +$ git-cat-file commit HEAD +------------------------------------------------- + +to see what the top commit was. + +Merging multiple trees +---------------------- + +Git helps you do a three-way merge, which you can expand to n-way by +repeating the merge procedure arbitrary times until you finally +"commit" the state. The normal situation is that you'd only do one +three-way merge (two parents), and commit it, but if you like to, you +can do multiple parents in one go. + +To do a three-way merge, you need the two sets of "commit" objects +that you want to merge, use those to find the closest common parent (a +third "commit" object), and then use those commit objects to find the +state of the directory ("tree" object) at these points. + +To get the "base" for the merge, you first look up the common parent +of two commits with + +------------------------------------------------- +$ git-merge-base <commit1> <commit2> +------------------------------------------------- + +which will return you the commit they are both based on. You should +now look up the "tree" objects of those commits, which you can easily +do with (for example) + +------------------------------------------------- +$ git-cat-file commit <commitname> | head -1 +------------------------------------------------- + +since the tree object information is always the first line in a commit +object. + +Once you know the three trees you are going to merge (the one "original" +tree, aka the common case, and the two "result" trees, aka the branches +you want to merge), you do a "merge" read into the index. This will +complain if it has to throw away your old index contents, so you should +make sure that you've committed those - in fact you would normally +always do a merge against your last commit (which should thus match what +you have in your current index anyway). + +To do the merge, do + +------------------------------------------------- +$ git-read-tree -m -u <origtree> <yourtree> <targettree> +------------------------------------------------- + +which will do all trivial merge operations for you directly in the +index file, and you can just write the result out with +`git-write-tree`. + + +Merging multiple trees, continued +--------------------------------- + +Sadly, many merges aren't trivial. If there are files that have +been added.moved or removed, or if both branches have modified the +same file, you will be left with an index tree that contains "merge +entries" in it. Such an index tree can 'NOT' be written out to a tree +object, and you will have to resolve any such merge clashes using +other tools before you can write out the result. + +You can examine such index state with `git-ls-files --unmerged` +command. An example: + +------------------------------------------------ +$ git-read-tree -m $orig HEAD $target +$ git-ls-files --unmerged +100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1 hello.c +100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2 hello.c +100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello.c +------------------------------------------------ + +Each line of the `git-ls-files --unmerged` output begins with +the blob mode bits, blob SHA1, 'stage number', and the +filename. The 'stage number' is git's way to say which tree it +came from: stage 1 corresponds to `$orig` tree, stage 2 `HEAD` +tree, and stage3 `$target` tree. + +Earlier we said that trivial merges are done inside +`git-read-tree -m`. For example, if the file did not change +from `$orig` to `HEAD` nor `$target`, or if the file changed +from `$orig` to `HEAD` and `$orig` to `$target` the same way, +obviously the final outcome is what is in `HEAD`. What the +above example shows is that file `hello.c` was changed from +`$orig` to `HEAD` and `$orig` to `$target` in a different way. +You could resolve this by running your favorite 3-way merge +program, e.g. `diff3` or `merge`, on the blob objects from +these three stages yourself, like this: + +------------------------------------------------ +$ git-cat-file blob 263414f... >hello.c~1 +$ git-cat-file blob 06fa6a2... >hello.c~2 +$ git-cat-file blob cc44c73... >hello.c~3 +$ merge hello.c~2 hello.c~1 hello.c~3 +------------------------------------------------ + +This would leave the merge result in `hello.c~2` file, along +with conflict markers if there are conflicts. After verifying +the merge result makes sense, you can tell git what the final +merge result for this file is by: + +------------------------------------------------- +$ mv -f hello.c~2 hello.c +$ git-update-index hello.c +------------------------------------------------- + +When a path is in unmerged state, running `git-update-index` for +that path tells git to mark the path resolved. + +The above is the description of a git merge at the lowest level, +to help you understand what conceptually happens under the hood. +In practice, nobody, not even git itself, uses three `git-cat-file` +for this. There is `git-merge-index` program that extracts the +stages to temporary files and calls a "merge" script on it: + +------------------------------------------------- +$ git-merge-index git-merge-one-file hello.c +------------------------------------------------- + +and that is what higher level `git resolve` is implemented with. + +How git stores objects efficiently: pack files +---------------------------------------------- + +We've seen how git stores each object in a file named after the +object's SHA1 hash. + +Unfortunately this system becomes inefficient once a project has a +lot of objects. Try this on an old project: + +------------------------------------------------ +$ git count-objects +6930 objects, 47620 kilobytes +------------------------------------------------ + +The first number is the number of objects which are kept in +individual files. The second is the amount of space taken up by +those "loose" objects. + +You can save space and make git faster by moving these loose objects in +to a "pack file", which stores a group of objects in an efficient +compressed format; the details of how pack files are formatted can be +found in link:technical/pack-format.txt[technical/pack-format.txt]. + +To put the loose objects into a pack, just run git repack: + +------------------------------------------------ +$ git repack +Generating pack... +Done counting 6020 objects. +Deltifying 6020 objects. + 100% (6020/6020) done +Writing 6020 objects. + 100% (6020/6020) done +Total 6020, written 6020 (delta 4070), reused 0 (delta 0) +Pack pack-3e54ad29d5b2e05838c75df582c65257b8d08e1c created. +------------------------------------------------ + +You can then run + +------------------------------------------------ +$ git prune +------------------------------------------------ + +to remove any of the "loose" objects that are now contained in the +pack. This will also remove any unreferenced objects (which may be +created when, for example, you use "git reset" to remove a commit). +You can verify that the loose objects are gone by looking at the +.git/objects directory or by running + +------------------------------------------------ +$ git count-objects +0 objects, 0 kilobytes +------------------------------------------------ + +Although the object files are gone, any commands that refer to those +objects will work exactly as they did before. + +The gitlink:git-gc[1] command performs packing, pruning, and more for +you, so is normally the only high-level command you need. + +[[dangling-objects]] +Dangling objects +---------------- + +The gitlink:git-fsck[1] command will sometimes complain about dangling +objects. They are not a problem. + +The most common cause of dangling objects is that you've rebased a +branch, or you have pulled from somebody else who rebased a branch--see +<<cleaning-up-history>>. In that case, the old head of the original +branch still exists, as does obviously everything it pointed to. The +branch pointer itself just doesn't, since you replaced it with another +one. + +There are also other situations too that cause dangling objects. For +example, a "dangling blob" may arise because you did a "git add" of a +file, but then, before you actually committed it and made it part of the +bigger picture, you changed something else in that file and committed +that *updated* thing - the old state that you added originally ends up +not being pointed to by any commit or tree, so it's now a dangling blob +object. + +Similarly, when the "recursive" merge strategy runs, and finds that +there are criss-cross merges and thus more than one merge base (which is +fairly unusual, but it does happen), it will generate one temporary +midway tree (or possibly even more, if you had lots of criss-crossing +merges and more than two merge bases) as a temporary internal merge +base, and again, those are real objects, but the end result will not end +up pointing to them, so they end up "dangling" in your repository. + +Generally, dangling objects aren't anything to worry about. They can +even be very useful: if you screw something up, the dangling objects can +be how you recover your old tree (say, you did a rebase, and realized +that you really didn't want to - you can look at what dangling objects +you have, and decide to reset your head to some old dangling state). + +For commits, the most useful thing to do with dangling objects tends to +be to do a simple + +------------------------------------------------ +$ gitk <dangling-commit-sha-goes-here> --not --all +------------------------------------------------ + +For blobs and trees, you can't do the same, but you can examine them. +You can just do + +------------------------------------------------ +$ git show <dangling-blob/tree-sha-goes-here> +------------------------------------------------ + +to show what the contents of the blob were (or, for a tree, basically +what the "ls" for that directory was), and that may give you some idea +of what the operation was that left that dangling object. + +Usually, dangling blobs and trees aren't very interesting. They're +almost always the result of either being a half-way mergebase (the blob +will often even have the conflict markers from a merge in it, if you +have had conflicting merges that you fixed up by hand), or simply +because you interrupted a "git fetch" with ^C or something like that, +leaving _some_ of the new objects in the object database, but just +dangling and useless. + +Anyway, once you are sure that you're not interested in any dangling +state, you can just prune all unreachable objects: + +------------------------------------------------ +$ git prune +------------------------------------------------ + +and they'll be gone. But you should only run "git prune" on a quiescent +repository - it's kind of like doing a filesystem fsck recovery: you +don't want to do that while the filesystem is mounted. + +(The same is true of "git-fsck" itself, btw - but since +git-fsck never actually *changes* the repository, it just reports +on what it found, git-fsck itself is never "dangerous" to run. +Running it while somebody is actually changing the repository can cause +confusing and scary messages, but it won't actually do anything bad. In +contrast, running "git prune" while somebody is actively changing the +repository is a *BAD* idea). + +Glossary of git terms +===================== + +include::glossary.txt[] + +Notes and todo list for this manual +=================================== + +This is a work in progress. + +The basic requirements: + - It must be readable in order, from beginning to end, by + someone intelligent with a basic grasp of the unix + commandline, but without any special knowledge of git. If + necessary, any other prerequisites should be specifically + mentioned as they arise. + - Whenever possible, section headings should clearly describe + the task they explain how to do, in language that requires + no more knowledge than necessary: for example, "importing + patches into a project" rather than "the git-am command" + +Think about how to create a clear chapter dependency graph that will +allow people to get to important topics without necessarily reading +everything in between. + +Say something about .gitignore. + +Scan Documentation/ for other stuff left out; in particular: + howto's + some of technical/? + hooks + list of commands in gitlink:git[1] + +Scan email archives for other stuff left out + +Scan man pages to see if any assume more background than this manual +provides. + +Simplify beginning by suggesting disconnected head instead of +temporary branch creation? + +Explain how to refer to file stages in the "how to resolve a merge" +section: diff -1, -2, -3, --ours, --theirs :1:/path notation. The +"git ls-files --unmerged --stage" thing is sorta useful too, +actually. And note gitk --merge. + +Add more good examples. Entire sections of just cookbook examples +might be a good idea; maybe make an "advanced examples" section a +standard end-of-chapter section? + +Include cross-references to the glossary, where appropriate. + +Document shallow clones? See draft 1.5.0 release notes for some +documentation. + +Add a section on working with other version control systems, including +CVS, Subversion, and just imports of series of release tarballs. + +More details on gitweb? + +Write a chapter on using plumbing and writing scripts. diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 79f1c527ff..7a10b6097f 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v0.5.GIT +DEF_VER=v1.5.0-rc4.GIT LF=' ' diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000000..361c65bacc --- /dev/null +++ b/INSTALL @@ -0,0 +1,114 @@ + + Git installation + +Normally you can just do "make" followed by "make install", and that +will install the git programs in your own ~/bin/ directory. If you want +to do a global install, you can do + + $ make prefix=/usr all doc ;# as yourself + # make prefix=/usr install install-doc ;# as root + +(or prefix=/usr/local, of course). Just like any program suite +that uses $prefix, the built results have some paths encoded, +which are derived from $prefix, so "make all; make prefix=/usr +install" would not work. + +Alternatively you can use autoconf generated ./configure script to +set up install paths (via config.mak.autogen), so you can write instead + + $ make configure ;# as yourself + $ ./configure --prefix=/usr ;# as yourself + $ make all doc ;# as yourself + # make install install-doc ;# as root + + +Issues of note: + + - git normally installs a helper script wrapper called "git", which + conflicts with a similarly named "GNU interactive tools" program. + + Tough. Either don't use the wrapper script, or delete the old GNU + interactive tools. None of the core git stuff needs the wrapper, + it's just a convenient shorthand and while it is documented in some + places, you can always replace "git commit" with "git-commit" + instead. + + But let's face it, most of us don't have GNU interactive tools, and + even if we had it, we wouldn't know what it does. I don't think it + has been actively developed since 1997, and people have moved over to + graphical file managers. + + - You can use git after building but without installing if you + wanted to. Various git commands need to find other git + commands and scripts to do their work, so you would need to + arrange a few environment variables to tell them that their + friends will be found in your built source area instead of at + their standard installation area. Something like this works + for me: + + GIT_EXEC_PATH=`pwd` + PATH=`pwd`:$PATH + GITPERLLIB=`pwd`/perl/blib/lib + export GIT_EXEC_PATH PATH GITPERLLIB + + - Git is reasonably self-sufficient, but does depend on a few external + programs and libraries: + + - "zlib", the compression library. Git won't build without it. + + - "openssl". The git-rev-list program uses bignum support from + openssl, and unless you specify otherwise, you'll also get the + SHA1 library from here. + + If you don't have openssl, you can use one of the SHA1 libraries + that come with git (git includes the one from Mozilla, and has + its own PowerPC and ARM optimized ones too - see the Makefile). + + - "libcurl" and "curl" executable. git-http-fetch and + git-fetch use them. If you do not use http + transfer, you are probably OK if you do not have + them. + + - expat library; git-http-push uses it for remote lock + management over DAV. Similar to "curl" above, this is optional. + + - "wish", the Tcl/Tk windowing shell is used in gitk to show the + history graphically + + - "ssh" is used to push and pull over the net + + - "perl" and POSIX-compliant shells are needed to use most of + the barebone Porcelainish scripts. + + - Some platform specific issues are dealt with Makefile rules, + but depending on your specific installation, you may not + have all the libraries/tools needed, or you may have + necessary libraries at unusual locations. Please look at the + top of the Makefile to see what can be adjusted for your needs. + You can place local settings in config.mak and the Makefile + will include them. Note that config.mak is not distributed; + the name is reserved for local settings. + + - To build and install documentation suite, you need to have the + asciidoc/xmlto toolchain. Alternatively, pre-formatted + documentation are available in "html" and "man" branches of the git + repository itself. For example, you could: + + $ mkdir manual && cd manual + $ git init + $ git fetch-pack git://git.kernel.org/pub/scm/git/git.git man html | + while read a b + do + echo $a >.git/$b + done + $ cp .git/refs/heads/man .git/refs/heads/master + $ git checkout + + to checkout the pre-built man pages. Also in this repository: + + $ git checkout html + + would instead give you a copy of what you see at: + + http://www.kernel.org/pub/software/scm/git/docs/ + @@ -1,48 +1,950 @@ +# The default target of this Makefile is... all:: +# Define NO_OPENSSL environment variable if you do not have OpenSSL. +# This also implies MOZILLA_SHA1. +# +# Define NO_CURL if you do not have curl installed. git-http-pull and +# git-http-push are not built, and you cannot use http:// and https:// +# transports. +# +# Define CURLDIR=/foo/bar if your curl header and library files are in +# /foo/bar/include and /foo/bar/lib directories. +# +# Define NO_EXPAT if you do not have expat installed. git-http-push is +# not built, and you cannot push using http:// and https:// transports. +# +# Define NO_D_INO_IN_DIRENT if you don't have d_ino in your struct dirent. +# +# Define NO_D_TYPE_IN_DIRENT if your platform defines DT_UNKNOWN but lacks +# d_type in struct dirent (latest Cygwin -- will be fixed soonish). +# +# Define NO_C99_FORMAT if your formatted IO functions (printf/scanf et.al.) +# do not support the 'size specifiers' introduced by C99, namely ll, hh, +# j, z, t. (representing long long int, char, intmax_t, size_t, ptrdiff_t). +# some C compilers supported these specifiers prior to C99 as an extension. +# +# Define NO_STRCASESTR if you don't have strcasestr. +# +# Define NO_STRLCPY if you don't have strlcpy. +# +# Define NO_SETENV if you don't have setenv in the C library. +# +# Define NO_SYMLINK_HEAD if you never want .git/HEAD to be a symbolic link. +# Enable it on Windows. By default, symrefs are still used. +# +# Define NO_SVN_TESTS if you want to skip time-consuming SVN interoperability +# tests. These tests take up a significant amount of the total test time +# but are not needed unless you plan to talk to SVN repos. +# +# Define NO_FINK if you are building on Darwin/Mac OS X, have Fink +# installed in /sw, but don't want GIT to link against any libraries +# installed there. If defined you may specify your own (or Fink's) +# include directories and library directories by defining CFLAGS +# and LDFLAGS appropriately. +# +# Define NO_DARWIN_PORTS if you are building on Darwin/Mac OS X, +# have DarwinPorts installed in /opt/local, but don't want GIT to +# link against any libraries installed there. If defined you may +# specify your own (or DarwinPort's) include directories and +# library directories by defining CFLAGS and LDFLAGS appropriately. +# +# Define PPC_SHA1 environment variable when running make to make use of +# a bundled SHA1 routine optimized for PowerPC. +# +# Define ARM_SHA1 environment variable when running make to make use of +# a bundled SHA1 routine optimized for ARM. +# +# Define MOZILLA_SHA1 environment variable when running make to make use of +# a bundled SHA1 routine coming from Mozilla. It is GPL'd and should be fast +# on non-x86 architectures (e.g. PowerPC), while the OpenSSL version (default +# choice) has very fast version optimized for i586. +# +# Define NEEDS_SSL_WITH_CRYPTO if you need -lcrypto with -lssl (Darwin). +# +# Define NEEDS_LIBICONV if linking with libc is not enough (Darwin). +# +# Define NEEDS_SOCKET if linking with libc is not enough (SunOS, +# Patrick Mauritz). +# +# Define NO_MMAP if you want to avoid mmap. +# +# Define NO_PREAD if you have a problem with pread() system call (e.g. +# cygwin.dll before v1.5.22). +# +# Define NO_FAST_WORKING_DIRECTORY if accessing objects in pack files is +# generally faster on your platform than accessing the working directory. +# +# Define NO_TRUSTABLE_FILEMODE if your filesystem may claim to support +# the executable mode bit, but doesn't really do so. +# +# Define NO_IPV6 if you lack IPv6 support and getaddrinfo(). +# +# Define NO_SOCKADDR_STORAGE if your platform does not have struct +# sockaddr_storage. +# +# Define NO_ICONV if your libc does not properly support iconv. +# +# Define NO_R_TO_GCC if your gcc does not like "-R/path/lib" that +# tells runtime paths to dynamic libraries; "-Wl,-rpath=/path/lib" +# is used instead. +# +# Define USE_NSEC below if you want git to care about sub-second file mtimes +# and ctimes. Note that you need recent glibc (at least 2.2.4) for this, and +# it will BREAK YOUR LOCAL DIFFS! show-diff and anything using it will likely +# randomly break unless your underlying filesystem supports those sub-second +# times (my ext3 doesn't). +# +# Define USE_STDEV below if you want git to care about the underlying device +# change being considered an inode change from the update-cache perspective. +# +# Define NO_PERL_MAKEMAKER if you cannot use Makefiles generated by perl's +# MakeMaker (e.g. using ActiveState under Cygwin). +# + GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE @$(SHELL_PATH) ./GIT-VERSION-GEN -include GIT-VERSION-FILE -SCRIPT_SH = git-gui.sh -GITGUI_BUILT_INS = git-citool -ALL_PROGRAMS = $(GITGUI_BUILT_INS) $(patsubst %.sh,%,$(SCRIPT_SH)) +uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') +uname_M := $(shell sh -c 'uname -m 2>/dev/null || echo not') +uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not') +uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not') +uname_P := $(shell sh -c 'uname -p 2>/dev/null || echo not') + +# CFLAGS and LDFLAGS are for the users to override from the command line. + +CFLAGS = -g -O2 -Wall +LDFLAGS = +ALL_CFLAGS = $(CFLAGS) +ALL_LDFLAGS = $(LDFLAGS) +STRIP ?= strip + +prefix = $(HOME) +bindir = $(prefix)/bin +gitexecdir = $(bindir) +template_dir = $(prefix)/share/git-core/templates/ +# DESTDIR= + +# default configuration for gitweb +GITWEB_CONFIG = gitweb_config.perl +GITWEB_HOME_LINK_STR = projects +GITWEB_SITENAME = +GITWEB_PROJECTROOT = /pub/git +GITWEB_EXPORT_OK = +GITWEB_STRICT_EXPORT = +GITWEB_BASE_URL = +GITWEB_LIST = +GITWEB_HOMETEXT = indextext.html +GITWEB_CSS = gitweb.css +GITWEB_LOGO = git-logo.png +GITWEB_FAVICON = git-favicon.png +GITWEB_SITE_HEADER = +GITWEB_SITE_FOOTER = + +export prefix bindir gitexecdir template_dir + +CC = gcc +AR = ar +TAR = tar +INSTALL = install +RPMBUILD = rpmbuild + +# sparse is architecture-neutral, which means that we need to tell it +# explicitly what architecture to check for. Fix this up for yours.. +SPARSE_FLAGS = -D__BIG_ENDIAN__ -D__powerpc__ + + + +### --- END CONFIGURATION SECTION --- + +# Those must not be GNU-specific; they are shared with perl/ which may +# be built by a different compiler. (Note that this is an artifact now +# but it still might be nice to keep that distinction.) +BASIC_CFLAGS = +BASIC_LDFLAGS = + +SCRIPT_SH = \ + git-bisect.sh git-checkout.sh \ + git-clean.sh git-clone.sh git-commit.sh \ + git-fetch.sh git-gc.sh \ + git-ls-remote.sh \ + git-merge-one-file.sh git-parse-remote.sh \ + git-pull.sh git-rebase.sh \ + git-repack.sh git-request-pull.sh git-reset.sh \ + git-resolve.sh git-revert.sh git-sh-setup.sh \ + git-tag.sh git-verify-tag.sh \ + git-applymbox.sh git-applypatch.sh git-am.sh \ + git-merge.sh git-merge-stupid.sh git-merge-octopus.sh \ + git-merge-resolve.sh git-merge-ours.sh \ + git-lost-found.sh git-quiltimport.sh + +SCRIPT_PERL = \ + git-add--interactive.perl \ + git-archimport.perl git-cvsimport.perl git-relink.perl \ + git-cvsserver.perl git-remote.perl \ + git-svnimport.perl git-cvsexportcommit.perl \ + git-send-email.perl git-svn.perl + +SCRIPTS = $(patsubst %.sh,%,$(SCRIPT_SH)) \ + $(patsubst %.perl,%,$(SCRIPT_PERL)) \ + git-cherry-pick git-status git-instaweb + +# ... and all the rest that could be moved out of bindir to gitexecdir +PROGRAMS = \ + git-convert-objects$X git-fetch-pack$X git-fsck$X \ + git-hash-object$X git-index-pack$X git-local-fetch$X \ + git-fast-import$X \ + git-merge-base$X \ + git-daemon$X \ + git-merge-index$X git-mktag$X git-mktree$X git-patch-id$X \ + git-peek-remote$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 \ + git-update-server-info$X \ + git-upload-pack$X git-verify-pack$X \ + git-pack-redundant$X git-var$X \ + git-merge-tree$X git-imap-send$X \ + git-merge-recursive$X \ + $(EXTRA_PROGRAMS) + +# Empty... +EXTRA_PROGRAMS = + +BUILT_INS = \ + git-format-patch$X git-show$X git-whatchanged$X git-cherry$X \ + git-get-tar-commit-id$X git-init$X git-repo-config$X \ + git-fsck-objects$X \ + $(patsubst builtin-%.o,git-%$X,$(BUILTIN_OBJS)) + +# what 'all' will build and 'install' will install, in gitexecdir +ALL_PROGRAMS = $(PROGRAMS) $(SCRIPTS) + +# Backward compatibility -- to be removed after 1.0 +PROGRAMS += git-ssh-pull$X git-ssh-push$X +# Set paths to tools early so that they can be used for version tests. ifndef SHELL_PATH SHELL_PATH = /bin/sh endif +ifndef PERL_PATH + PERL_PATH = /usr/bin/perl +endif -gitexecdir := $(shell git --exec-path) -INSTALL = install +export PERL_PATH + +LIB_FILE=libgit.a +XDIFF_LIB=xdiff/lib.a + +LIB_H = \ + archive.h blob.h cache.h commit.h csum-file.h delta.h grep.h \ + diff.h object.h pack.h pkt-line.h quote.h refs.h list-objects.h sideband.h \ + run-command.h strbuf.h tag.h tree.h git-compat-util.h revision.h \ + tree-walk.h log-tree.h dir.h path-list.h unpack-trees.h builtin.h \ + utf8.h reflog-walk.h + +DIFF_OBJS = \ + diff.o diff-lib.o diffcore-break.o diffcore-order.o \ + diffcore-pickaxe.o diffcore-rename.o tree-diff.o combine-diff.o \ + diffcore-delta.o log-tree.o + +LIB_OBJS = \ + 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 \ + interpolate.o \ + lockfile.o \ + object.o pack-check.o patch-delta.o path.o pkt-line.o sideband.o \ + reachable.o reflog-walk.o \ + quote.o read-cache.o refs.o run-command.o dir.o object-refs.o \ + server-info.o setup.o sha1_file.o sha1_name.o strbuf.o \ + tag.o tree.o usage.o config.o environment.o ctype.o copy.o \ + revision.o pager.o tree-walk.o xdiff-interface.o \ + write_or_die.o trace.o list-objects.o grep.o \ + alloc.o merge-file.o path-list.o help.o unpack-trees.o $(DIFF_OBJS) \ + color.o wt-status.o archive-zip.o archive-tar.o shallow.o utf8.o + +BUILTIN_OBJS = \ + builtin-add.o \ + builtin-annotate.o \ + builtin-apply.o \ + builtin-archive.o \ + builtin-blame.o \ + builtin-branch.o \ + builtin-cat-file.o \ + builtin-checkout-index.o \ + builtin-check-ref-format.o \ + builtin-commit-tree.o \ + builtin-count-objects.o \ + builtin-describe.o \ + builtin-diff.o \ + builtin-diff-files.o \ + builtin-diff-index.o \ + builtin-diff-stages.o \ + builtin-diff-tree.o \ + builtin-fmt-merge-msg.o \ + builtin-for-each-ref.o \ + builtin-fsck.o \ + builtin-grep.o \ + builtin-init-db.o \ + builtin-log.o \ + builtin-ls-files.o \ + builtin-ls-tree.o \ + builtin-mailinfo.o \ + builtin-mailsplit.o \ + builtin-merge-file.o \ + builtin-mv.o \ + builtin-name-rev.o \ + builtin-pack-objects.o \ + builtin-prune.o \ + builtin-prune-packed.o \ + builtin-push.o \ + builtin-read-tree.o \ + builtin-reflog.o \ + builtin-config.o \ + builtin-rerere.o \ + builtin-rev-list.o \ + builtin-rev-parse.o \ + builtin-rm.o \ + builtin-runstatus.o \ + builtin-shortlog.o \ + builtin-show-branch.o \ + builtin-stripspace.o \ + builtin-symbolic-ref.o \ + builtin-tar-tree.o \ + builtin-unpack-objects.o \ + builtin-update-index.o \ + builtin-update-ref.o \ + builtin-upload-archive.o \ + builtin-verify-pack.o \ + builtin-write-tree.o \ + builtin-show-ref.o \ + builtin-pack-refs.o + +GITLIBS = $(LIB_FILE) $(XDIFF_LIB) +EXTLIBS = -lz + +# +# Platform specific tweaks +# + +# We choose to avoid "if .. else if .. else .. endif endif" +# because maintaining the nesting to match is a pain. If +# we had "elif" things would have been much nicer... + +ifeq ($(uname_S),Linux) + NO_STRLCPY = YesPlease +endif +ifeq ($(uname_S),GNU/kFreeBSD) + NO_STRLCPY = YesPlease +endif +ifeq ($(uname_S),Darwin) + NEEDS_SSL_WITH_CRYPTO = YesPlease + NEEDS_LIBICONV = YesPlease + NO_STRLCPY = YesPlease +endif +ifeq ($(uname_S),SunOS) + NEEDS_SOCKET = YesPlease + NEEDS_NSL = YesPlease + SHELL_PATH = /bin/bash + NO_STRCASESTR = YesPlease + ifeq ($(uname_R),5.8) + NEEDS_LIBICONV = YesPlease + NO_UNSETENV = YesPlease + NO_SETENV = YesPlease + NO_C99_FORMAT = YesPlease + endif + ifeq ($(uname_R),5.9) + NO_UNSETENV = YesPlease + NO_SETENV = YesPlease + NO_C99_FORMAT = YesPlease + endif + INSTALL = ginstall + TAR = gtar + BASIC_CFLAGS += -D__EXTENSIONS__ +endif +ifeq ($(uname_O),Cygwin) + NO_D_TYPE_IN_DIRENT = YesPlease + NO_D_INO_IN_DIRENT = YesPlease + NO_STRCASESTR = YesPlease + NO_SYMLINK_HEAD = YesPlease + NEEDS_LIBICONV = YesPlease + NO_C99_FORMAT = YesPlease + NO_FAST_WORKING_DIRECTORY = UnfortunatelyYes + NO_TRUSTABLE_FILEMODE = UnfortunatelyYes + # There are conflicting reports about this. + # On some boxes NO_MMAP is needed, and not so elsewhere. + # Try commenting this out if you suspect MMAP is more efficient + NO_MMAP = YesPlease + NO_IPV6 = YesPlease + X = .exe +endif +ifeq ($(uname_S),FreeBSD) + NEEDS_LIBICONV = YesPlease + BASIC_CFLAGS += -I/usr/local/include + BASIC_LDFLAGS += -L/usr/local/lib +endif +ifeq ($(uname_S),OpenBSD) + NO_STRCASESTR = YesPlease + NEEDS_LIBICONV = YesPlease + BASIC_CFLAGS += -I/usr/local/include + BASIC_LDFLAGS += -L/usr/local/lib +endif +ifeq ($(uname_S),NetBSD) + ifeq ($(shell expr "$(uname_R)" : '[01]\.'),2) + NEEDS_LIBICONV = YesPlease + endif + BASIC_CFLAGS += -I/usr/pkg/include + BASIC_LDFLAGS += -L/usr/pkg/lib + ALL_LDFLAGS += -Wl,-rpath,/usr/pkg/lib +endif +ifeq ($(uname_S),AIX) + NO_STRCASESTR=YesPlease + NO_STRLCPY = YesPlease + NEEDS_LIBICONV=YesPlease +endif +ifeq ($(uname_S),IRIX64) + NO_IPV6=YesPlease + NO_SETENV=YesPlease + NO_STRCASESTR=YesPlease + NO_STRLCPY = YesPlease + NO_SOCKADDR_STORAGE=YesPlease + SHELL_PATH=/usr/gnu/bin/bash + BASIC_CFLAGS += -DPATH_MAX=1024 + # for now, build 32-bit version + BASIC_LDFLAGS += -L/usr/lib32 +endif +ifneq (,$(findstring arm,$(uname_M))) + ARM_SHA1 = YesPlease +endif + +-include config.mak.autogen +-include config.mak + +ifeq ($(uname_S),Darwin) + ifndef NO_FINK + ifeq ($(shell test -d /sw/lib && echo y),y) + BASIC_CFLAGS += -I/sw/include + BASIC_LDFLAGS += -L/sw/lib + endif + endif + ifndef NO_DARWIN_PORTS + ifeq ($(shell test -d /opt/local/lib && echo y),y) + BASIC_CFLAGS += -I/opt/local/include + BASIC_LDFLAGS += -L/opt/local/lib + endif + endif +endif + +ifdef NO_R_TO_GCC_LINKER + # Some gcc does not accept and pass -R to the linker to specify + # the runtime dynamic library path. + CC_LD_DYNPATH = -Wl,-rpath= +else + CC_LD_DYNPATH = -R +endif + +ifndef NO_CURL + ifdef CURLDIR + # Try "-Wl,-rpath=$(CURLDIR)/lib" in such a case. + BASIC_CFLAGS += -I$(CURLDIR)/include + CURL_LIBCURL = -L$(CURLDIR)/lib $(CC_LD_DYNPATH)$(CURLDIR)/lib -lcurl + else + CURL_LIBCURL = -lcurl + endif + PROGRAMS += git-http-fetch$X + curl_check := $(shell (echo 070908; curl-config --vernum) | sort -r | sed -ne 2p) + ifeq "$(curl_check)" "070908" + ifndef NO_EXPAT + PROGRAMS += git-http-push$X + endif + endif + ifndef NO_EXPAT + EXPAT_LIBEXPAT = -lexpat + endif +endif + +ifndef NO_OPENSSL + OPENSSL_LIBSSL = -lssl + ifdef OPENSSLDIR + BASIC_CFLAGS += -I$(OPENSSLDIR)/include + OPENSSL_LINK = -L$(OPENSSLDIR)/lib $(CC_LD_DYNPATH)$(OPENSSLDIR)/lib + else + OPENSSL_LINK = + endif +else + BASIC_CFLAGS += -DNO_OPENSSL + MOZILLA_SHA1 = 1 + OPENSSL_LIBSSL = +endif +ifdef NEEDS_SSL_WITH_CRYPTO + LIB_4_CRYPTO = $(OPENSSL_LINK) -lcrypto -lssl +else + LIB_4_CRYPTO = $(OPENSSL_LINK) -lcrypto +endif +ifdef NEEDS_LIBICONV + ifdef ICONVDIR + BASIC_CFLAGS += -I$(ICONVDIR)/include + ICONV_LINK = -L$(ICONVDIR)/lib $(CC_LD_DYNPATH)$(ICONVDIR)/lib + else + ICONV_LINK = + endif + EXTLIBS += $(ICONV_LINK) -liconv +endif +ifdef NEEDS_SOCKET + EXTLIBS += -lsocket +endif +ifdef NEEDS_NSL + EXTLIBS += -lnsl +endif +ifdef NO_D_TYPE_IN_DIRENT + BASIC_CFLAGS += -DNO_D_TYPE_IN_DIRENT +endif +ifdef NO_D_INO_IN_DIRENT + BASIC_CFLAGS += -DNO_D_INO_IN_DIRENT +endif +ifdef NO_C99_FORMAT + BASIC_CFLAGS += -DNO_C99_FORMAT +endif +ifdef NO_SYMLINK_HEAD + BASIC_CFLAGS += -DNO_SYMLINK_HEAD +endif +ifdef NO_STRCASESTR + COMPAT_CFLAGS += -DNO_STRCASESTR + COMPAT_OBJS += compat/strcasestr.o +endif +ifdef NO_STRLCPY + COMPAT_CFLAGS += -DNO_STRLCPY + COMPAT_OBJS += compat/strlcpy.o +endif +ifdef NO_SETENV + COMPAT_CFLAGS += -DNO_SETENV + COMPAT_OBJS += compat/setenv.o +endif +ifdef NO_UNSETENV + COMPAT_CFLAGS += -DNO_UNSETENV + COMPAT_OBJS += compat/unsetenv.o +endif +ifdef NO_MMAP + COMPAT_CFLAGS += -DNO_MMAP + COMPAT_OBJS += compat/mmap.o +endif +ifdef NO_PREAD + COMPAT_CFLAGS += -DNO_PREAD + COMPAT_OBJS += compat/pread.o +endif +ifdef NO_FAST_WORKING_DIRECTORY + BASIC_CFLAGS += -DNO_FAST_WORKING_DIRECTORY +endif +ifdef NO_TRUSTABLE_FILEMODE + BASIC_CFLAGS += -DNO_TRUSTABLE_FILEMODE +endif +ifdef NO_IPV6 + BASIC_CFLAGS += -DNO_IPV6 +endif +ifdef NO_SOCKADDR_STORAGE +ifdef NO_IPV6 + BASIC_CFLAGS += -Dsockaddr_storage=sockaddr_in +else + BASIC_CFLAGS += -Dsockaddr_storage=sockaddr_in6 +endif +endif +ifdef NO_INET_NTOP + LIB_OBJS += compat/inet_ntop.o +endif +ifdef NO_INET_PTON + LIB_OBJS += compat/inet_pton.o +endif + +ifdef NO_ICONV + BASIC_CFLAGS += -DNO_ICONV +endif + +ifdef PPC_SHA1 + SHA1_HEADER = "ppc/sha1.h" + LIB_OBJS += ppc/sha1.o ppc/sha1ppc.o +else +ifdef ARM_SHA1 + SHA1_HEADER = "arm/sha1.h" + LIB_OBJS += arm/sha1.o arm/sha1_arm.o +else +ifdef MOZILLA_SHA1 + SHA1_HEADER = "mozilla-sha1/sha1.h" + LIB_OBJS += mozilla-sha1/sha1.o +else + SHA1_HEADER = <openssl/sha.h> + EXTLIBS += $(LIB_4_CRYPTO) +endif +endif +endif +ifdef NO_PERL_MAKEMAKER + export NO_PERL_MAKEMAKER +endif + +# Shell quote (do not use $(call) to accommodate ancient setups); + +SHA1_HEADER_SQ = $(subst ','\'',$(SHA1_HEADER)) DESTDIR_SQ = $(subst ','\'',$(DESTDIR)) +bindir_SQ = $(subst ','\'',$(bindir)) gitexecdir_SQ = $(subst ','\'',$(gitexecdir)) +template_dir_SQ = $(subst ','\'',$(template_dir)) +prefix_SQ = $(subst ','\'',$(prefix)) SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) +PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH)) + +LIBS = $(GITLIBS) $(EXTLIBS) + +BASIC_CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER_SQ)' $(COMPAT_CFLAGS) +LIB_OBJS += $(COMPAT_OBJS) + +ALL_CFLAGS += $(BASIC_CFLAGS) +ALL_LDFLAGS += $(BASIC_LDFLAGS) + +export prefix TAR INSTALL DESTDIR SHELL_PATH template_dir + + +### Build rules + +all:: $(ALL_PROGRAMS) $(BUILT_INS) git$X gitk gitweb/gitweb.cgi +ifneq (,$X) + $(foreach p,$(patsubst %$X,%,$(filter %$X,$(ALL_PROGRAMS) $(BUILT_INS) git$X)), rm -f '$p';) +endif + +all:: + $(MAKE) -C perl PERL_PATH='$(PERL_PATH_SQ)' prefix='$(prefix_SQ)' all + $(MAKE) -C templates + +strip: $(PROGRAMS) git$X + $(STRIP) $(STRIP_OPTS) $(PROGRAMS) git$X + +git$X: git.c common-cmds.h $(BUILTIN_OBJS) $(GITLIBS) GIT-CFLAGS + $(CC) -DGIT_VERSION='"$(GIT_VERSION)"' \ + $(ALL_CFLAGS) -o $@ $(filter %.c,$^) \ + $(BUILTIN_OBJS) $(ALL_LDFLAGS) $(LIBS) + +help.o: common-cmds.h + +$(BUILT_INS): git$X + rm -f $@ && ln git$X $@ + +common-cmds.h: Documentation/git-*.txt + ./generate-cmdlist.sh > $@+ + mv $@+ $@ $(patsubst %.sh,%,$(SCRIPT_SH)) : % : %.sh rm -f $@ $@+ sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ - -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ - $@.sh >$@+ + -e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \ + -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ + -e 's/@@NO_CURL@@/$(NO_CURL)/g' \ + $@.sh >$@+ chmod +x $@+ mv $@+ $@ -$(GITGUI_BUILT_INS): git-gui - rm -f $@ && ln git-gui $@ +$(patsubst %.perl,%,$(SCRIPT_PERL)): perl/perl.mak + +perl/perl.mak: GIT-CFLAGS + $(MAKE) -C perl PERL_PATH='$(PERL_PATH_SQ)' prefix='$(prefix_SQ)' $(@F) + +$(patsubst %.perl,%,$(SCRIPT_PERL)): % : %.perl + rm -f $@ $@+ + INSTLIBDIR=`$(MAKE) -C perl -s --no-print-directory instlibdir` && \ + sed -e '1{' \ + -e ' s|#!.*perl|#!$(PERL_PATH_SQ)|' \ + -e ' h' \ + -e ' s=.*=use lib (split(/:/, $$ENV{GITPERLLIB} || "@@INSTLIBDIR@@"));=' \ + -e ' H' \ + -e ' x' \ + -e '}' \ + -e 's|@@INSTLIBDIR@@|'"$$INSTLIBDIR"'|g' \ + -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ + $@.perl >$@+ + chmod +x $@+ + mv $@+ $@ + +git-cherry-pick: git-revert + cp $< $@+ + mv $@+ $@ + +git-status: git-commit + cp $< $@+ + mv $@+ $@ + +gitweb/gitweb.cgi: gitweb/gitweb.perl + rm -f $@ $@+ + sed -e '1s|#!.*perl|#!$(PERL_PATH_SQ)|' \ + -e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \ + -e 's|++GIT_BINDIR++|$(bindir)|g' \ + -e 's|++GITWEB_CONFIG++|$(GITWEB_CONFIG)|g' \ + -e 's|++GITWEB_HOME_LINK_STR++|$(GITWEB_HOME_LINK_STR)|g' \ + -e 's|++GITWEB_SITENAME++|$(GITWEB_SITENAME)|g' \ + -e 's|++GITWEB_PROJECTROOT++|$(GITWEB_PROJECTROOT)|g' \ + -e 's|++GITWEB_EXPORT_OK++|$(GITWEB_EXPORT_OK)|g' \ + -e 's|++GITWEB_STRICT_EXPORT++|$(GITWEB_STRICT_EXPORT)|g' \ + -e 's|++GITWEB_BASE_URL++|$(GITWEB_BASE_URL)|g' \ + -e 's|++GITWEB_LIST++|$(GITWEB_LIST)|g' \ + -e 's|++GITWEB_HOMETEXT++|$(GITWEB_HOMETEXT)|g' \ + -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \ + -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \ + -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \ + -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \ + -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \ + $< >$@+ + chmod +x $@+ + mv $@+ $@ + +git-instaweb: git-instaweb.sh gitweb/gitweb.cgi gitweb/gitweb.css + rm -f $@ $@+ + sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ + -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ + -e 's/@@NO_CURL@@/$(NO_CURL)/g' \ + -e '/@@GITWEB_CGI@@/r gitweb/gitweb.cgi' \ + -e '/@@GITWEB_CGI@@/d' \ + -e '/@@GITWEB_CSS@@/r gitweb/gitweb.css' \ + -e '/@@GITWEB_CSS@@/d' \ + $@.sh > $@+ + chmod +x $@+ + mv $@+ $@ + +configure: configure.ac + rm -f $@ $<+ + sed -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ + $< > $<+ + autoconf -o $@ $<+ + rm -f $<+ # These can record GIT_VERSION -$(patsubst %.sh,%,$(SCRIPT_SH)): GIT-VERSION-FILE +git$X git.spec \ + $(patsubst %.sh,%,$(SCRIPT_SH)) \ + $(patsubst %.perl,%,$(SCRIPT_PERL)) \ + : GIT-VERSION-FILE + +%.o: %.c GIT-CFLAGS + $(CC) -o $*.o -c $(ALL_CFLAGS) $< +%.o: %.S + $(CC) -o $*.o -c $(ALL_CFLAGS) $< + +exec_cmd.o: exec_cmd.c GIT-CFLAGS + $(CC) -o $*.o -c $(ALL_CFLAGS) '-DGIT_EXEC_PATH="$(gitexecdir_SQ)"' $< +builtin-init-db.o: builtin-init-db.c GIT-CFLAGS + $(CC) -o $*.o -c $(ALL_CFLAGS) -DDEFAULT_GIT_TEMPLATE_DIR='"$(template_dir_SQ)"' $< + +http.o: http.c GIT-CFLAGS + $(CC) -o $*.o -c $(ALL_CFLAGS) -DGIT_USER_AGENT='"git/$(GIT_VERSION)"' $< + +ifdef NO_EXPAT +http-fetch.o: http-fetch.c http.h GIT-CFLAGS + $(CC) -o $*.o -c $(ALL_CFLAGS) -DNO_EXPAT $< +endif + +git-%$X: %.o $(GITLIBS) + $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS) -all:: $(ALL_PROGRAMS) +ssh-pull.o: ssh-fetch.c +ssh-push.o: ssh-upload.c +git-local-fetch$X: fetch.o +git-ssh-fetch$X: rsh.o fetch.o +git-ssh-upload$X: rsh.o +git-ssh-pull$X: rsh.o fetch.o +git-ssh-push$X: rsh.o + +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 $(GITLIBS) + $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ + $(LIBS) $(CURL_LIBCURL) $(EXPAT_LIBEXPAT) + +git-http-push$X: revision.o http.o http-push.o $(GITLIBS) + $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ + $(LIBS) $(CURL_LIBCURL) $(EXPAT_LIBEXPAT) + +$(LIB_OBJS) $(BUILTIN_OBJS): $(LIB_H) +$(patsubst git-%$X,%.o,$(PROGRAMS)): $(LIB_H) $(wildcard */*.h) +$(DIFF_OBJS): diffcore.h + +$(LIB_FILE): $(LIB_OBJS) + rm -f $@ && $(AR) rcs $@ $(LIB_OBJS) + +XDIFF_OBJS=xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o \ + xdiff/xmerge.o +$(XDIFF_OBJS): xdiff/xinclude.h xdiff/xmacros.h xdiff/xdiff.h xdiff/xtypes.h \ + xdiff/xutils.h xdiff/xprepare.h xdiff/xdiffi.h xdiff/xemit.h + +$(XDIFF_LIB): $(XDIFF_OBJS) + rm -f $@ && $(AR) rcs $@ $(XDIFF_OBJS) + + +perl/Makefile: perl/Git.pm perl/Makefile.PL GIT-CFLAGS + (cd perl && $(PERL_PATH) Makefile.PL \ + PREFIX='$(prefix_SQ)') + +doc: + $(MAKE) -C Documentation all + +TAGS: + rm -f TAGS + find . -name '*.[hcS]' -print | xargs etags -a + +tags: + rm -f tags + find . -name '*.[hcS]' -print | xargs ctags -a + +### Detect prefix changes +TRACK_CFLAGS = $(subst ','\'',$(ALL_CFLAGS)):\ + $(bindir_SQ):$(gitexecdir_SQ):$(template_dir_SQ):$(prefix_SQ) + +GIT-CFLAGS: .FORCE-GIT-CFLAGS + @FLAGS='$(TRACK_CFLAGS)'; \ + if test x"$$FLAGS" != x"`cat GIT-CFLAGS 2>/dev/null`" ; then \ + echo 1>&2 " * new build flags or prefix"; \ + echo "$$FLAGS" >GIT-CFLAGS; \ + fi + +### Testing rules + +# GNU make supports exporting all variables by "export" without parameters. +# However, the environment gets quite big, and some programs have problems +# with that. + +export NO_SVN_TESTS + +test: all + $(MAKE) -C t/ all + +test-date$X: test-date.c date.o ctype.o + $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) test-date.c date.o ctype.o + +test-delta$X: test-delta.o diff-delta.o patch-delta.o $(GITLIBS) + $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS) + +test-dump-cache-tree$X: dump-cache-tree.o $(GITLIBS) + $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS) + +test-sha1$X: test-sha1.o $(GITLIBS) + $(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS) + +check-sha1:: test-sha1$X + ./test-sha1.sh + +check: common-cmds.h + for i in *.c; do sparse $(ALL_CFLAGS) $(SPARSE_FLAGS) $$i || exit; done + + + +### Installation rules install: all + $(INSTALL) -d -m755 '$(DESTDIR_SQ)$(bindir_SQ)' $(INSTALL) -d -m755 '$(DESTDIR_SQ)$(gitexecdir_SQ)' - $(INSTALL) git-gui '$(DESTDIR_SQ)$(gitexecdir_SQ)' - $(foreach p,$(GITGUI_BUILT_INS), rm -f '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' && ln '$(DESTDIR_SQ)$(gitexecdir_SQ)/git-gui' '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' ;) + $(INSTALL) $(ALL_PROGRAMS) '$(DESTDIR_SQ)$(gitexecdir_SQ)' + $(INSTALL) git$X gitk '$(DESTDIR_SQ)$(bindir_SQ)' + $(MAKE) -C templates DESTDIR='$(DESTDIR_SQ)' install + $(MAKE) -C perl prefix='$(prefix_SQ)' install + if test 'z$(bindir_SQ)' != 'z$(gitexecdir_SQ)'; \ + then \ + ln -f '$(DESTDIR_SQ)$(bindir_SQ)/git$X' \ + '$(DESTDIR_SQ)$(gitexecdir_SQ)/git$X' || \ + cp '$(DESTDIR_SQ)$(bindir_SQ)/git$X' \ + '$(DESTDIR_SQ)$(gitexecdir_SQ)/git$X'; \ + fi + $(foreach p,$(BUILT_INS), rm -f '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' && ln '$(DESTDIR_SQ)$(gitexecdir_SQ)/git$X' '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' ;) +ifneq (,$X) + $(foreach p,$(patsubst %$X,%,$(filter %$X,$(ALL_PROGRAMS) $(BUILT_INS) git$X)), rm -f '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p';) +endif + +install-doc: + $(MAKE) -C Documentation install + +quick-install-doc: + $(MAKE) -C Documentation quick-install + + + +### Maintainer's dist rules + +git.spec: git.spec.in + sed -e 's/@@VERSION@@/$(GIT_VERSION)/g' < $< > $@+ + mv $@+ $@ + +GIT_TARNAME=git-$(GIT_VERSION) +dist: git.spec git-archive + ./git-archive --format=tar \ + --prefix=$(GIT_TARNAME)/ HEAD^{tree} > $(GIT_TARNAME).tar + @mkdir -p $(GIT_TARNAME) + @cp git.spec $(GIT_TARNAME) + @echo $(GIT_VERSION) > $(GIT_TARNAME)/version + $(TAR) rf $(GIT_TARNAME).tar \ + $(GIT_TARNAME)/git.spec $(GIT_TARNAME)/version + @rm -rf $(GIT_TARNAME) + gzip -f -9 $(GIT_TARNAME).tar + +rpm: dist + $(RPMBUILD) -ta $(GIT_TARNAME).tar.gz + +htmldocs = git-htmldocs-$(GIT_VERSION) +manpages = git-manpages-$(GIT_VERSION) +dist-doc: + rm -fr .doc-tmp-dir + mkdir .doc-tmp-dir + $(MAKE) -C Documentation WEBDOC_DEST=../.doc-tmp-dir install-webdoc + cd .doc-tmp-dir && $(TAR) cf ../$(htmldocs).tar . + gzip -n -9 -f $(htmldocs).tar + : + rm -fr .doc-tmp-dir + mkdir .doc-tmp-dir .doc-tmp-dir/man1 .doc-tmp-dir/man7 + $(MAKE) -C Documentation DESTDIR=./ \ + man1dir=../.doc-tmp-dir/man1 \ + man7dir=../.doc-tmp-dir/man7 \ + install + cd .doc-tmp-dir && $(TAR) cf ../$(manpages).tar . + gzip -n -9 -f $(manpages).tar + rm -fr .doc-tmp-dir + +### Cleaning rules + +clean: + rm -f *.o mozilla-sha1/*.o arm/*.o ppc/*.o compat/*.o xdiff/*.o \ + $(LIB_FILE) $(XDIFF_LIB) + rm -f $(ALL_PROGRAMS) $(BUILT_INS) git$X + rm -f *.spec *.pyc *.pyo */*.pyc */*.pyo common-cmds.h TAGS tags + rm -rf autom4te.cache + rm -f configure config.log config.mak.autogen config.mak.append config.status config.cache + rm -rf $(GIT_TARNAME) .doc-tmp-dir + rm -f $(GIT_TARNAME).tar.gz git-core_$(GIT_VERSION)-*.tar.gz + rm -f $(htmldocs).tar.gz $(manpages).tar.gz + rm -f gitweb/gitweb.cgi + $(MAKE) -C Documentation/ clean + $(MAKE) -C perl clean + $(MAKE) -C templates/ clean + $(MAKE) -C t/ clean + rm -f GIT-VERSION-FILE GIT-CFLAGS + +.PHONY: all install clean strip +.PHONY: .FORCE-GIT-VERSION-FILE TAGS tags .FORCE-GIT-CFLAGS -clean:: - rm -f $(ALL_PROGRAMS) GIT-VERSION-FILE +### Check documentation +# +check-docs:: + @for v in $(ALL_PROGRAMS) $(BUILT_INS) git$X gitk; \ + do \ + case "$$v" in \ + git-merge-octopus | git-merge-ours | git-merge-recursive | \ + git-merge-resolve | git-merge-stupid | \ + git-ssh-pull | git-ssh-push ) continue ;; \ + esac ; \ + test -f "Documentation/$$v.txt" || \ + echo "no doc: $$v"; \ + grep -q "^gitlink:$$v\[[0-9]\]::" Documentation/git.txt || \ + case "$$v" in \ + git) ;; \ + *) echo "no link: $$v";; \ + esac ; \ + done | sort -.PHONY: all install clean -.PHONY: .FORCE-GIT-VERSION-FILE +### Make sure built-ins do not have dups and listed in git.c +# +check-builtins:: + ./check-builtins.sh diff --git a/README b/README new file mode 100644 index 0000000000..441167cb8a --- /dev/null +++ b/README @@ -0,0 +1,40 @@ +//////////////////////////////////////////////////////////////// + + GIT - the stupid content tracker + +//////////////////////////////////////////////////////////////// + +"git" can mean anything, depending on your mood. + + - random three-letter combination that is pronounceable, and not + actually used by any common UNIX command. The fact that it is a + mispronunciation of "get" may or may not be relevant. + - stupid. contemptible and despicable. simple. Take your pick from the + dictionary of slang. + - "global information tracker": you're in a good mood, and it actually + works for you. Angels sing, and a light suddenly fills the room. + - "goddamn idiotic truckload of sh*t": when it breaks + +Git is a fast, scalable, distributed revision control system with an +unusually rich command set that provides both high-level operations +and full access to internals. + +Git is an Open Source project covered by the GNU General Public License. +It was originally written by Linus Torvalds with help of a group of +hackers around the net. It is currently maintained by Junio C Hamano. + +Please read the file INSTALL for installation instructions. +See Documentation/tutorial.txt to get started, then see +Documentation/everyday.txt for a useful minimum set of commands, +and "man git-commandname" for documentation of each command. +CVS users may also want to read Documentation/cvs-migration.txt. + +Many Git online resources are accessible from http://git.or.cz/ +including full documentation and Git related tools. + +The user discussion and development of Git take place on the Git +mailing list -- everyone is welcome to post bug reports, feature +requests, comments and patches to git@vger.kernel.org. To subscribe +to the list, send an email with just "subscribe git" in the body to +majordomo@vger.kernel.org. The mailing list archives are available at +http://marc.theaimsgroup.com/?l=git and other archival sites. diff --git a/alloc.c b/alloc.c new file mode 100644 index 0000000000..460db192d5 --- /dev/null +++ b/alloc.c @@ -0,0 +1,64 @@ +/* + * alloc.c - specialized allocator for internal objects + * + * Copyright (C) 2006 Linus Torvalds + * + * The standard malloc/free wastes too much space for objects, partly because + * it maintains all the allocation infrastructure (which isn't needed, since + * we never free an object descriptor anyway), but even more because it ends + * up with maximal alignment because it doesn't know what the object alignment + * for the new allocation is. + */ +#include "cache.h" +#include "object.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "tag.h" + +#define BLOCKING 1024 + +#define DEFINE_ALLOCATOR(name) \ +static unsigned int name##_allocs; \ +struct name *alloc_##name##_node(void) \ +{ \ + static int nr; \ + static struct name *block; \ + \ + if (!nr) { \ + nr = BLOCKING; \ + block = xcalloc(BLOCKING, sizeof(struct name)); \ + } \ + nr--; \ + name##_allocs++; \ + return block++; \ +} + +DEFINE_ALLOCATOR(blob) +DEFINE_ALLOCATOR(tree) +DEFINE_ALLOCATOR(commit) +DEFINE_ALLOCATOR(tag) + +#ifdef NO_C99_FORMAT +#define SZ_FMT "%u" +#else +#define SZ_FMT "%zu" +#endif + +static void report(const char* name, unsigned int count, size_t size) +{ + fprintf(stderr, "%10s: %8u (" SZ_FMT " kB)\n", name, count, size); +} + +#undef SZ_FMT + +#define REPORT(name) \ + report(#name, name##_allocs, name##_allocs*sizeof(struct name) >> 10) + +void alloc_report(void) +{ + REPORT(blob); + REPORT(tree); + REPORT(commit); + REPORT(tag); +} diff --git a/archive-tar.c b/archive-tar.c new file mode 100644 index 0000000000..7d52a061f4 --- /dev/null +++ b/archive-tar.c @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2005, 2006 Rene Scharfe + */ +#include "cache.h" +#include "commit.h" +#include "strbuf.h" +#include "tar.h" +#include "builtin.h" +#include "archive.h" + +#define RECORDSIZE (512) +#define BLOCKSIZE (RECORDSIZE * 20) + +static char block[BLOCKSIZE]; +static unsigned long offset; + +static time_t archive_time; +static int tar_umask = 002; +static int verbose; + +/* writes out the whole block, but only if it is full */ +static void write_if_needed(void) +{ + if (offset == BLOCKSIZE) { + write_or_die(1, block, BLOCKSIZE); + offset = 0; + } +} + +/* + * queues up writes, so that all our write(2) calls write exactly one + * full block; pads writes to RECORDSIZE + */ +static void write_blocked(const void *data, unsigned long size) +{ + const char *buf = data; + unsigned long tail; + + if (offset) { + unsigned long chunk = BLOCKSIZE - offset; + if (size < chunk) + chunk = size; + memcpy(block + offset, buf, chunk); + size -= chunk; + offset += chunk; + buf += chunk; + write_if_needed(); + } + while (size >= BLOCKSIZE) { + write_or_die(1, buf, BLOCKSIZE); + size -= BLOCKSIZE; + buf += BLOCKSIZE; + } + if (size) { + memcpy(block + offset, buf, size); + offset += size; + } + tail = offset % RECORDSIZE; + if (tail) { + memset(block + offset, 0, RECORDSIZE - tail); + offset += RECORDSIZE - tail; + } + write_if_needed(); +} + +/* + * The end of tar archives is marked by 2*512 nul bytes and after that + * follows the rest of the block (if any). + */ +static void write_trailer(void) +{ + int tail = BLOCKSIZE - offset; + memset(block + offset, 0, tail); + write_or_die(1, block, BLOCKSIZE); + if (tail < 2 * RECORDSIZE) { + memset(block, 0, offset); + write_or_die(1, block, BLOCKSIZE); + } +} + +static void strbuf_append_string(struct strbuf *sb, const char *s) +{ + int slen = strlen(s); + int total = sb->len + slen; + if (total > sb->alloc) { + sb->buf = xrealloc(sb->buf, total); + sb->alloc = total; + } + memcpy(sb->buf + sb->len, s, slen); + sb->len = total; +} + +/* + * pax extended header records have the format "%u %s=%s\n". %u contains + * the size of the whole string (including the %u), the first %s is the + * keyword, the second one is the value. This function constructs such a + * string and appends it to a struct strbuf. + */ +static void strbuf_append_ext_header(struct strbuf *sb, const char *keyword, + const char *value, unsigned int valuelen) +{ + char *p; + int len, total, tmp; + + /* "%u %s=%s\n" */ + len = 1 + 1 + strlen(keyword) + 1 + valuelen + 1; + for (tmp = len; tmp > 9; tmp /= 10) + len++; + + total = sb->len + len; + if (total > sb->alloc) { + sb->buf = xrealloc(sb->buf, total); + sb->alloc = total; + } + + p = sb->buf; + p += sprintf(p, "%u %s=", len, keyword); + memcpy(p, value, valuelen); + p += valuelen; + *p = '\n'; + sb->len = total; +} + +static unsigned int ustar_header_chksum(const struct ustar_header *header) +{ + char *p = (char *)header; + unsigned int chksum = 0; + while (p < header->chksum) + chksum += *p++; + chksum += sizeof(header->chksum) * ' '; + p += sizeof(header->chksum); + while (p < (char *)header + sizeof(struct ustar_header)) + chksum += *p++; + return chksum; +} + +static int get_path_prefix(const struct strbuf *path, int maxlen) +{ + int i = path->len; + if (i > maxlen) + i = maxlen; + do { + i--; + } while (i > 0 && path->buf[i] != '/'); + return i; +} + +static void write_entry(const unsigned char *sha1, struct strbuf *path, + unsigned int mode, void *buffer, unsigned long size) +{ + struct ustar_header header; + struct strbuf ext_header; + + memset(&header, 0, sizeof(header)); + ext_header.buf = NULL; + ext_header.len = ext_header.alloc = 0; + + if (!sha1) { + *header.typeflag = TYPEFLAG_GLOBAL_HEADER; + mode = 0100666; + strcpy(header.name, "pax_global_header"); + } else if (!path) { + *header.typeflag = TYPEFLAG_EXT_HEADER; + mode = 0100666; + sprintf(header.name, "%s.paxheader", sha1_to_hex(sha1)); + } else { + if (verbose) + fprintf(stderr, "%.*s\n", path->len, path->buf); + if (S_ISDIR(mode)) { + *header.typeflag = TYPEFLAG_DIR; + mode = (mode | 0777) & ~tar_umask; + } else if (S_ISLNK(mode)) { + *header.typeflag = TYPEFLAG_LNK; + mode |= 0777; + } else if (S_ISREG(mode)) { + *header.typeflag = TYPEFLAG_REG; + mode = (mode | ((mode & 0100) ? 0777 : 0666)) & ~tar_umask; + } else { + error("unsupported file mode: 0%o (SHA1: %s)", + mode, sha1_to_hex(sha1)); + return; + } + if (path->len > sizeof(header.name)) { + int plen = get_path_prefix(path, sizeof(header.prefix)); + int rest = path->len - plen - 1; + if (plen > 0 && rest <= sizeof(header.name)) { + memcpy(header.prefix, path->buf, plen); + memcpy(header.name, path->buf + plen + 1, rest); + } else { + sprintf(header.name, "%s.data", + sha1_to_hex(sha1)); + strbuf_append_ext_header(&ext_header, "path", + path->buf, path->len); + } + } else + memcpy(header.name, path->buf, path->len); + } + + if (S_ISLNK(mode) && buffer) { + if (size > sizeof(header.linkname)) { + sprintf(header.linkname, "see %s.paxheader", + sha1_to_hex(sha1)); + strbuf_append_ext_header(&ext_header, "linkpath", + buffer, size); + } else + memcpy(header.linkname, buffer, size); + } + + sprintf(header.mode, "%07o", mode & 07777); + sprintf(header.size, "%011lo", S_ISREG(mode) ? size : 0); + sprintf(header.mtime, "%011lo", archive_time); + + sprintf(header.uid, "%07o", 0); + sprintf(header.gid, "%07o", 0); + strlcpy(header.uname, "root", sizeof(header.uname)); + strlcpy(header.gname, "root", sizeof(header.gname)); + sprintf(header.devmajor, "%07o", 0); + sprintf(header.devminor, "%07o", 0); + + memcpy(header.magic, "ustar", 6); + memcpy(header.version, "00", 2); + + sprintf(header.chksum, "%07o", ustar_header_chksum(&header)); + + if (ext_header.len > 0) { + write_entry(sha1, NULL, 0, ext_header.buf, ext_header.len); + free(ext_header.buf); + } + write_blocked(&header, sizeof(header)); + if (S_ISREG(mode) && buffer && size > 0) + write_blocked(buffer, size); +} + +static void write_global_extended_header(const unsigned char *sha1) +{ + struct strbuf ext_header; + ext_header.buf = NULL; + ext_header.len = ext_header.alloc = 0; + strbuf_append_ext_header(&ext_header, "comment", sha1_to_hex(sha1), 40); + write_entry(NULL, NULL, 0, ext_header.buf, ext_header.len); + free(ext_header.buf); +} + +static int git_tar_config(const char *var, const char *value) +{ + if (!strcmp(var, "tar.umask")) { + if (!strcmp(value, "user")) { + tar_umask = umask(0); + umask(tar_umask); + } else { + tar_umask = git_config_int(var, value); + } + return 0; + } + return git_default_config(var, value); +} + +static int write_tar_entry(const unsigned char *sha1, + const char *base, int baselen, + const char *filename, unsigned mode, int stage) +{ + static struct strbuf path; + int filenamelen = strlen(filename); + void *buffer; + char type[20]; + unsigned long size; + + if (!path.alloc) { + path.buf = xmalloc(PATH_MAX); + path.alloc = PATH_MAX; + path.len = path.eof = 0; + } + if (path.alloc < baselen + filenamelen) { + free(path.buf); + path.buf = xmalloc(baselen + filenamelen); + path.alloc = baselen + filenamelen; + } + memcpy(path.buf, base, baselen); + memcpy(path.buf + baselen, filename, filenamelen); + path.len = baselen + filenamelen; + if (S_ISDIR(mode)) { + strbuf_append_string(&path, "/"); + buffer = NULL; + size = 0; + } else { + buffer = read_sha1_file(sha1, type, &size); + if (!buffer) + die("cannot read %s", sha1_to_hex(sha1)); + } + + write_entry(sha1, &path, mode, buffer, size); + free(buffer); + + return READ_TREE_RECURSIVE; +} + +int write_tar_archive(struct archiver_args *args) +{ + int plen = args->base ? strlen(args->base) : 0; + + git_config(git_tar_config); + + archive_time = args->time; + verbose = args->verbose; + + if (args->commit_sha1) + write_global_extended_header(args->commit_sha1); + + if (args->base && plen > 0 && args->base[plen - 1] == '/') { + char *base = xstrdup(args->base); + int baselen = strlen(base); + + while (baselen > 0 && base[baselen - 1] == '/') + base[--baselen] = '\0'; + write_tar_entry(args->tree->object.sha1, "", 0, base, 040777, 0); + free(base); + } + read_tree_recursive(args->tree, args->base, plen, 0, + args->pathspec, write_tar_entry); + write_trailer(); + + return 0; +} diff --git a/archive-zip.c b/archive-zip.c new file mode 100644 index 0000000000..f31b8ed823 --- /dev/null +++ b/archive-zip.c @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2006 Rene Scharfe + */ +#include "cache.h" +#include "commit.h" +#include "blob.h" +#include "tree.h" +#include "quote.h" +#include "builtin.h" +#include "archive.h" + +static int verbose; +static int zip_date; +static int zip_time; + +static unsigned char *zip_dir; +static unsigned int zip_dir_size; + +static unsigned int zip_offset; +static unsigned int zip_dir_offset; +static unsigned int zip_dir_entries; + +#define ZIP_DIRECTORY_MIN_SIZE (1024 * 1024) + +struct zip_local_header { + unsigned char magic[4]; + unsigned char version[2]; + unsigned char flags[2]; + unsigned char compression_method[2]; + unsigned char mtime[2]; + unsigned char mdate[2]; + unsigned char crc32[4]; + unsigned char compressed_size[4]; + unsigned char size[4]; + unsigned char filename_length[2]; + unsigned char extra_length[2]; + unsigned char _end[1]; +}; + +struct zip_dir_header { + unsigned char magic[4]; + unsigned char creator_version[2]; + unsigned char version[2]; + unsigned char flags[2]; + unsigned char compression_method[2]; + unsigned char mtime[2]; + unsigned char mdate[2]; + unsigned char crc32[4]; + unsigned char compressed_size[4]; + unsigned char size[4]; + unsigned char filename_length[2]; + unsigned char extra_length[2]; + unsigned char comment_length[2]; + unsigned char disk[2]; + unsigned char attr1[2]; + unsigned char attr2[4]; + unsigned char offset[4]; + unsigned char _end[1]; +}; + +struct zip_dir_trailer { + unsigned char magic[4]; + unsigned char disk[2]; + unsigned char directory_start_disk[2]; + unsigned char entries_on_this_disk[2]; + unsigned char entries[2]; + unsigned char size[4]; + unsigned char offset[4]; + unsigned char comment_length[2]; + unsigned char _end[1]; +}; + +/* + * On ARM, padding is added at the end of the struct, so a simple + * sizeof(struct ...) reports two bytes more than the payload size + * we're interested in. + */ +#define ZIP_LOCAL_HEADER_SIZE offsetof(struct zip_local_header, _end) +#define ZIP_DIR_HEADER_SIZE offsetof(struct zip_dir_header, _end) +#define ZIP_DIR_TRAILER_SIZE offsetof(struct zip_dir_trailer, _end) + +static void copy_le16(unsigned char *dest, unsigned int n) +{ + dest[0] = 0xff & n; + dest[1] = 0xff & (n >> 010); +} + +static void copy_le32(unsigned char *dest, unsigned int n) +{ + dest[0] = 0xff & n; + dest[1] = 0xff & (n >> 010); + dest[2] = 0xff & (n >> 020); + dest[3] = 0xff & (n >> 030); +} + +static void *zlib_deflate(void *data, unsigned long size, + unsigned long *compressed_size) +{ + z_stream stream; + unsigned long maxsize; + void *buffer; + int result; + + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, zlib_compression_level); + maxsize = deflateBound(&stream, size); + buffer = xmalloc(maxsize); + + stream.next_in = data; + stream.avail_in = size; + stream.next_out = buffer; + stream.avail_out = maxsize; + + do { + result = deflate(&stream, Z_FINISH); + } while (result == Z_OK); + + if (result != Z_STREAM_END) { + free(buffer); + return NULL; + } + + deflateEnd(&stream); + *compressed_size = stream.total_out; + + return buffer; +} + +static char *construct_path(const char *base, int baselen, + const char *filename, int isdir, int *pathlen) +{ + int filenamelen = strlen(filename); + int len = baselen + filenamelen; + char *path, *p; + + if (isdir) + len++; + p = path = xmalloc(len + 1); + + memcpy(p, base, baselen); + p += baselen; + memcpy(p, filename, filenamelen); + p += filenamelen; + if (isdir) + *p++ = '/'; + *p = '\0'; + + *pathlen = len; + + return path; +} + +static int write_zip_entry(const unsigned char *sha1, + const char *base, int baselen, + const char *filename, unsigned mode, int stage) +{ + struct zip_local_header header; + struct zip_dir_header dirent; + unsigned long attr2; + unsigned long compressed_size; + unsigned long uncompressed_size; + unsigned long crc; + unsigned long direntsize; + unsigned long size; + int method; + int result = -1; + int pathlen; + unsigned char *out; + char *path; + char type[20]; + void *buffer = NULL; + void *deflated = NULL; + + crc = crc32(0, NULL, 0); + + path = construct_path(base, baselen, filename, S_ISDIR(mode), &pathlen); + if (verbose) + fprintf(stderr, "%s\n", path); + if (pathlen > 0xffff) { + error("path too long (%d chars, SHA1: %s): %s", pathlen, + sha1_to_hex(sha1), path); + goto out; + } + + if (S_ISDIR(mode)) { + method = 0; + attr2 = 16; + result = READ_TREE_RECURSIVE; + out = NULL; + uncompressed_size = 0; + compressed_size = 0; + } else if (S_ISREG(mode) || S_ISLNK(mode)) { + method = 0; + attr2 = S_ISLNK(mode) ? ((mode | 0777) << 16) : 0; + if (S_ISREG(mode) && zlib_compression_level != 0) + method = 8; + result = 0; + buffer = read_sha1_file(sha1, type, &size); + if (!buffer) + die("cannot read %s", sha1_to_hex(sha1)); + crc = crc32(crc, buffer, size); + out = buffer; + uncompressed_size = size; + compressed_size = size; + } else { + error("unsupported file mode: 0%o (SHA1: %s)", mode, + sha1_to_hex(sha1)); + goto out; + } + + if (method == 8) { + deflated = zlib_deflate(buffer, size, &compressed_size); + if (deflated && compressed_size - 6 < size) { + /* ZLIB --> raw compressed data (see RFC 1950) */ + /* CMF and FLG ... */ + out = (unsigned char *)deflated + 2; + compressed_size -= 6; /* ... and ADLER32 */ + } else { + method = 0; + compressed_size = size; + } + } + + /* make sure we have enough free space in the dictionary */ + direntsize = ZIP_DIR_HEADER_SIZE + pathlen; + while (zip_dir_size < zip_dir_offset + direntsize) { + zip_dir_size += ZIP_DIRECTORY_MIN_SIZE; + zip_dir = xrealloc(zip_dir, zip_dir_size); + } + + copy_le32(dirent.magic, 0x02014b50); + copy_le16(dirent.creator_version, S_ISLNK(mode) ? 0x0317 : 0); + copy_le16(dirent.version, 10); + copy_le16(dirent.flags, 0); + copy_le16(dirent.compression_method, method); + copy_le16(dirent.mtime, zip_time); + copy_le16(dirent.mdate, zip_date); + copy_le32(dirent.crc32, crc); + copy_le32(dirent.compressed_size, compressed_size); + copy_le32(dirent.size, uncompressed_size); + copy_le16(dirent.filename_length, pathlen); + copy_le16(dirent.extra_length, 0); + copy_le16(dirent.comment_length, 0); + copy_le16(dirent.disk, 0); + copy_le16(dirent.attr1, 0); + copy_le32(dirent.attr2, attr2); + copy_le32(dirent.offset, zip_offset); + memcpy(zip_dir + zip_dir_offset, &dirent, ZIP_DIR_HEADER_SIZE); + zip_dir_offset += ZIP_DIR_HEADER_SIZE; + memcpy(zip_dir + zip_dir_offset, path, pathlen); + zip_dir_offset += pathlen; + zip_dir_entries++; + + copy_le32(header.magic, 0x04034b50); + copy_le16(header.version, 10); + copy_le16(header.flags, 0); + copy_le16(header.compression_method, method); + copy_le16(header.mtime, zip_time); + copy_le16(header.mdate, zip_date); + copy_le32(header.crc32, crc); + copy_le32(header.compressed_size, compressed_size); + copy_le32(header.size, uncompressed_size); + copy_le16(header.filename_length, pathlen); + copy_le16(header.extra_length, 0); + write_or_die(1, &header, ZIP_LOCAL_HEADER_SIZE); + zip_offset += ZIP_LOCAL_HEADER_SIZE; + write_or_die(1, path, pathlen); + zip_offset += pathlen; + if (compressed_size > 0) { + write_or_die(1, out, compressed_size); + zip_offset += compressed_size; + } + +out: + free(buffer); + free(deflated); + free(path); + + return result; +} + +static void write_zip_trailer(const unsigned char *sha1) +{ + struct zip_dir_trailer trailer; + + copy_le32(trailer.magic, 0x06054b50); + copy_le16(trailer.disk, 0); + copy_le16(trailer.directory_start_disk, 0); + copy_le16(trailer.entries_on_this_disk, zip_dir_entries); + copy_le16(trailer.entries, zip_dir_entries); + copy_le32(trailer.size, zip_dir_offset); + copy_le32(trailer.offset, zip_offset); + copy_le16(trailer.comment_length, sha1 ? 40 : 0); + + write_or_die(1, zip_dir, zip_dir_offset); + write_or_die(1, &trailer, ZIP_DIR_TRAILER_SIZE); + if (sha1) + write_or_die(1, sha1_to_hex(sha1), 40); +} + +static void dos_time(time_t *time, int *dos_date, int *dos_time) +{ + struct tm *t = localtime(time); + + *dos_date = t->tm_mday + (t->tm_mon + 1) * 32 + + (t->tm_year + 1900 - 1980) * 512; + *dos_time = t->tm_sec / 2 + t->tm_min * 32 + t->tm_hour * 2048; +} + +int write_zip_archive(struct archiver_args *args) +{ + int plen = strlen(args->base); + + dos_time(&args->time, &zip_date, &zip_time); + + zip_dir = xmalloc(ZIP_DIRECTORY_MIN_SIZE); + zip_dir_size = ZIP_DIRECTORY_MIN_SIZE; + verbose = args->verbose; + + if (args->base && plen > 0 && args->base[plen - 1] == '/') { + char *base = xstrdup(args->base); + int baselen = strlen(base); + + while (baselen > 0 && base[baselen - 1] == '/') + base[--baselen] = '\0'; + write_zip_entry(args->tree->object.sha1, "", 0, base, 040777, 0); + free(base); + } + read_tree_recursive(args->tree, args->base, plen, 0, + args->pathspec, write_zip_entry); + write_zip_trailer(args->commit_sha1); + + free(zip_dir); + + return 0; +} + +void *parse_extra_zip_args(int argc, const char **argv) +{ + for (; argc > 0; argc--, argv++) { + const char *arg = argv[0]; + + if (arg[0] == '-' && isdigit(arg[1]) && arg[2] == '\0') + zlib_compression_level = arg[1] - '0'; + else + die("Unknown argument for zip format: %s", arg); + } + return NULL; +} diff --git a/archive.h b/archive.h new file mode 100644 index 0000000000..6838dc788f --- /dev/null +++ b/archive.h @@ -0,0 +1,45 @@ +#ifndef ARCHIVE_H +#define ARCHIVE_H + +#define MAX_EXTRA_ARGS 32 +#define MAX_ARGS (MAX_EXTRA_ARGS + 32) + +struct archiver_args { + const char *base; + struct tree *tree; + const unsigned char *commit_sha1; + time_t time; + const char **pathspec; + unsigned int verbose : 1; + void *extra; +}; + +typedef int (*write_archive_fn_t)(struct archiver_args *); + +typedef void *(*parse_extra_args_fn_t)(int argc, const char **argv); + +struct archiver { + const char *name; + struct archiver_args args; + write_archive_fn_t write_archive; + parse_extra_args_fn_t parse_extra; +}; + +extern int parse_archive_args(int argc, + const char **argv, + struct archiver *ar); + +extern void parse_treeish_arg(const char **treeish, + struct archiver_args *ar_args, + const char *prefix); + +extern void parse_pathspec_arg(const char **pathspec, + struct archiver_args *args); +/* + * Archive-format specific backends. + */ +extern int write_tar_archive(struct archiver_args *); +extern int write_zip_archive(struct archiver_args *); +extern void *parse_extra_zip_args(int argc, const char **argv); + +#endif /* ARCHIVE_H */ diff --git a/arm/sha1.c b/arm/sha1.c new file mode 100644 index 0000000000..11b1a048b4 --- /dev/null +++ b/arm/sha1.c @@ -0,0 +1,82 @@ +/* + * SHA-1 implementation optimized for ARM + * + * Copyright: (C) 2005 by Nicolas Pitre <nico@cam.org> + * Created: September 17, 2005 + */ + +#include <string.h> +#include "sha1.h" + +extern void sha_transform(uint32_t *hash, const unsigned char *data, uint32_t *W); + +void SHA1_Init(SHA_CTX *c) +{ + c->len = 0; + c->hash[0] = 0x67452301; + c->hash[1] = 0xefcdab89; + c->hash[2] = 0x98badcfe; + c->hash[3] = 0x10325476; + c->hash[4] = 0xc3d2e1f0; +} + +void SHA1_Update(SHA_CTX *c, const void *p, unsigned long n) +{ + uint32_t workspace[80]; + unsigned int partial; + unsigned long done; + + partial = c->len & 0x3f; + c->len += n; + if ((partial + n) >= 64) { + if (partial) { + done = 64 - partial; + memcpy(c->buffer + partial, p, done); + sha_transform(c->hash, c->buffer, workspace); + partial = 0; + } else + done = 0; + while (n >= done + 64) { + sha_transform(c->hash, p + done, workspace); + done += 64; + } + } else + done = 0; + if (n - done) + memcpy(c->buffer + partial, p + done, n - done); +} + +void SHA1_Final(unsigned char *hash, SHA_CTX *c) +{ + uint64_t bitlen; + uint32_t bitlen_hi, bitlen_lo; + unsigned int i, offset, padlen; + unsigned char bits[8]; + static const unsigned char padding[64] = { 0x80, }; + + bitlen = c->len << 3; + offset = c->len & 0x3f; + padlen = ((offset < 56) ? 56 : (64 + 56)) - offset; + SHA1_Update(c, padding, padlen); + + bitlen_hi = bitlen >> 32; + bitlen_lo = bitlen & 0xffffffff; + bits[0] = bitlen_hi >> 24; + bits[1] = bitlen_hi >> 16; + bits[2] = bitlen_hi >> 8; + bits[3] = bitlen_hi; + bits[4] = bitlen_lo >> 24; + bits[5] = bitlen_lo >> 16; + bits[6] = bitlen_lo >> 8; + bits[7] = bitlen_lo; + SHA1_Update(c, bits, 8); + + for (i = 0; i < 5; i++) { + uint32_t v = c->hash[i]; + hash[0] = v >> 24; + hash[1] = v >> 16; + hash[2] = v >> 8; + hash[3] = v; + hash += 4; + } +} diff --git a/arm/sha1.h b/arm/sha1.h new file mode 100644 index 0000000000..3952646349 --- /dev/null +++ b/arm/sha1.h @@ -0,0 +1,18 @@ +/* + * SHA-1 implementation optimized for ARM + * + * Copyright: (C) 2005 by Nicolas Pitre <nico@cam.org> + * Created: September 17, 2005 + */ + +#include <stdint.h> + +typedef struct sha_context { + uint64_t len; + uint32_t hash[5]; + unsigned char buffer[64]; +} SHA_CTX; + +void SHA1_Init(SHA_CTX *c); +void SHA1_Update(SHA_CTX *c, const void *p, unsigned long n); +void SHA1_Final(unsigned char *hash, SHA_CTX *c); diff --git a/arm/sha1_arm.S b/arm/sha1_arm.S new file mode 100644 index 0000000000..da92d20e84 --- /dev/null +++ b/arm/sha1_arm.S @@ -0,0 +1,184 @@ +/* + * SHA transform optimized for ARM + * + * Copyright: (C) 2005 by Nicolas Pitre <nico@cam.org> + * Created: September 17, 2005 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + + .text + .globl sha_transform + +/* + * void sha_transform(uint32_t *hash, const unsigned char *data, uint32_t *W); + * + * note: the "data" pointer may be unaligned. + */ + +sha_transform: + + stmfd sp!, {r4 - r8, lr} + + @ for (i = 0; i < 16; i++) + @ W[i] = ntohl(((uint32_t *)data)[i]); */ + +#ifdef __ARMEB__ + mov r4, r0 + mov r0, r2 + mov r2, #64 + bl memcpy + mov r2, r0 + mov r0, r4 +#else + mov r3, r2 + mov lr, #16 +1: ldrb r4, [r1], #1 + ldrb r5, [r1], #1 + ldrb r6, [r1], #1 + ldrb r7, [r1], #1 + subs lr, lr, #1 + orr r5, r5, r4, lsl #8 + orr r6, r6, r5, lsl #8 + orr r7, r7, r6, lsl #8 + str r7, [r3], #4 + bne 1b +#endif + + @ for (i = 0; i < 64; i++) + @ W[i+16] = ror(W[i+13] ^ W[i+8] ^ W[i+2] ^ W[i], 31); + + sub r3, r2, #4 + mov lr, #64 +2: ldr r4, [r3, #4]! + subs lr, lr, #1 + ldr r5, [r3, #8] + ldr r6, [r3, #32] + ldr r7, [r3, #52] + eor r4, r4, r5 + eor r4, r4, r6 + eor r4, r4, r7 + mov r4, r4, ror #31 + str r4, [r3, #64] + bne 2b + + /* + * The SHA functions are: + * + * f1(B,C,D) = (D ^ (B & (C ^ D))) + * f2(B,C,D) = (B ^ C ^ D) + * f3(B,C,D) = ((B & C) | (D & (B | C))) + * + * Then the sub-blocks are processed as follows: + * + * A' = ror(A, 27) + f(B,C,D) + E + K + *W++ + * B' = A + * C' = ror(B, 2) + * D' = C + * E' = D + * + * We therefore unroll each loop 5 times to avoid register shuffling. + * Also the ror for C (and also D and E which are successivelyderived + * from it) is applied in place to cut on an additional mov insn for + * each round. + */ + + .macro sha_f1, A, B, C, D, E + ldr r3, [r2], #4 + eor ip, \C, \D + add \E, r1, \E, ror #2 + and ip, \B, ip, ror #2 + add \E, \E, \A, ror #27 + eor ip, ip, \D, ror #2 + add \E, \E, r3 + add \E, \E, ip + .endm + + .macro sha_f2, A, B, C, D, E + ldr r3, [r2], #4 + add \E, r1, \E, ror #2 + eor ip, \B, \C, ror #2 + add \E, \E, \A, ror #27 + eor ip, ip, \D, ror #2 + add \E, \E, r3 + add \E, \E, ip + .endm + + .macro sha_f3, A, B, C, D, E + ldr r3, [r2], #4 + add \E, r1, \E, ror #2 + orr ip, \B, \C, ror #2 + add \E, \E, \A, ror #27 + and ip, ip, \D, ror #2 + add \E, \E, r3 + and r3, \B, \C, ror #2 + orr ip, ip, r3 + add \E, \E, ip + .endm + + ldmia r0, {r4 - r8} + + mov lr, #4 + ldr r1, .L_sha_K + 0 + + /* adjust initial values */ + mov r6, r6, ror #30 + mov r7, r7, ror #30 + mov r8, r8, ror #30 + +3: subs lr, lr, #1 + sha_f1 r4, r5, r6, r7, r8 + sha_f1 r8, r4, r5, r6, r7 + sha_f1 r7, r8, r4, r5, r6 + sha_f1 r6, r7, r8, r4, r5 + sha_f1 r5, r6, r7, r8, r4 + bne 3b + + ldr r1, .L_sha_K + 4 + mov lr, #4 + +4: subs lr, lr, #1 + sha_f2 r4, r5, r6, r7, r8 + sha_f2 r8, r4, r5, r6, r7 + sha_f2 r7, r8, r4, r5, r6 + sha_f2 r6, r7, r8, r4, r5 + sha_f2 r5, r6, r7, r8, r4 + bne 4b + + ldr r1, .L_sha_K + 8 + mov lr, #4 + +5: subs lr, lr, #1 + sha_f3 r4, r5, r6, r7, r8 + sha_f3 r8, r4, r5, r6, r7 + sha_f3 r7, r8, r4, r5, r6 + sha_f3 r6, r7, r8, r4, r5 + sha_f3 r5, r6, r7, r8, r4 + bne 5b + + ldr r1, .L_sha_K + 12 + mov lr, #4 + +6: subs lr, lr, #1 + sha_f2 r4, r5, r6, r7, r8 + sha_f2 r8, r4, r5, r6, r7 + sha_f2 r7, r8, r4, r5, r6 + sha_f2 r6, r7, r8, r4, r5 + sha_f2 r5, r6, r7, r8, r4 + bne 6b + + ldmia r0, {r1, r2, r3, ip, lr} + add r4, r1, r4 + add r5, r2, r5 + add r6, r3, r6, ror #2 + add r7, ip, r7, ror #2 + add r8, lr, r8, ror #2 + stmia r0, {r4 - r8} + + ldmfd sp!, {r4 - r8, pc} + +.L_sha_K: + .word 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6 + diff --git a/base85.c b/base85.c new file mode 100644 index 0000000000..a9e97f89d9 --- /dev/null +++ b/base85.c @@ -0,0 +1,140 @@ +#include "cache.h" + +#undef DEBUG_85 + +#ifdef DEBUG_85 +#define say(a) fprintf(stderr, a) +#define say1(a,b) fprintf(stderr, a, b) +#define say2(a,b,c) fprintf(stderr, a, b, c) +#else +#define say(a) do {} while(0) +#define say1(a,b) do {} while(0) +#define say2(a,b,c) do {} while(0) +#endif + +static const char en85[] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', + '!', '#', '$', '%', '&', '(', ')', '*', '+', '-', + ';', '<', '=', '>', '?', '@', '^', '_', '`', '{', + '|', '}', '~' +}; + +static char de85[256]; +static void prep_base85(void) +{ + int i; + if (de85['Z']) + return; + for (i = 0; i < ARRAY_SIZE(en85); i++) { + int ch = en85[i]; + de85[ch] = i + 1; + } +} + +int decode_85(char *dst, char *buffer, int len) +{ + prep_base85(); + + say2("decode 85 <%.*s>", len/4*5, buffer); + while (len) { + unsigned acc = 0; + int de, cnt = 4; + unsigned char ch; + do { + ch = *buffer++; + de = de85[ch]; + if (--de < 0) + return error("invalid base85 alphabet %c", ch); + acc = acc * 85 + de; + } while (--cnt); + ch = *buffer++; + de = de85[ch]; + if (--de < 0) + return error("invalid base85 alphabet %c", ch); + /* + * Detect overflow. The largest + * 5-letter possible is "|NsC0" to + * encode 0xffffffff, and "|NsC" gives + * 0x03030303 at this point (i.e. + * 0xffffffff = 0x03030303 * 85). + */ + if (0x03030303 < acc || + 0xffffffff - de < (acc *= 85)) + error("invalid base85 sequence %.5s", buffer-5); + acc += de; + say1(" %08x", acc); + + cnt = (len < 4) ? len : 4; + len -= cnt; + do { + acc = (acc << 8) | (acc >> 24); + *dst++ = acc; + } while (--cnt); + } + say("\n"); + + return 0; +} + +void encode_85(char *buf, unsigned char *data, int bytes) +{ + prep_base85(); + + say("encode 85"); + while (bytes) { + unsigned acc = 0; + int cnt; + for (cnt = 24; cnt >= 0; cnt -= 8) { + int ch = *data++; + acc |= ch << cnt; + if (--bytes == 0) + break; + } + say1(" %08x", acc); + for (cnt = 4; cnt >= 0; cnt--) { + int val = acc % 85; + acc /= 85; + buf[cnt] = en85[val]; + } + buf += 5; + } + say("\n"); + + *buf = 0; +} + +#ifdef DEBUG_85 +int main(int ac, char **av) +{ + char buf[1024]; + + if (!strcmp(av[1], "-e")) { + int len = strlen(av[2]); + encode_85(buf, av[2], len); + if (len <= 26) len = len + 'A' - 1; + else len = len + 'a' - 26 + 1; + printf("encoded: %c%s\n", len, buf); + return 0; + } + if (!strcmp(av[1], "-d")) { + int len = *av[2]; + if ('A' <= len && len <= 'Z') len = len - 'A' + 1; + else len = len - 'a' + 26 + 1; + decode_85(buf, av[2]+1, len); + printf("decoded: %.*s\n", len, buf); + return 0; + } + if (!strcmp(av[1], "-t")) { + char t[4] = { -1,-1,-1,-1 }; + encode_85(buf, t, 4); + printf("encoded: D%s\n", buf); + return 0; + } +} +#endif diff --git a/blob.c b/blob.c new file mode 100644 index 0000000000..9776beac58 --- /dev/null +++ b/blob.c @@ -0,0 +1,50 @@ +#include "cache.h" +#include "blob.h" + +const char *blob_type = "blob"; + +struct blob *lookup_blob(const unsigned char *sha1) +{ + struct object *obj = lookup_object(sha1); + if (!obj) { + struct blob *ret = alloc_blob_node(); + created_object(sha1, &ret->object); + ret->object.type = OBJ_BLOB; + return ret; + } + if (!obj->type) + obj->type = OBJ_BLOB; + if (obj->type != OBJ_BLOB) { + error("Object %s is a %s, not a blob", + sha1_to_hex(sha1), typename(obj->type)); + return NULL; + } + return (struct blob *) obj; +} + +int parse_blob_buffer(struct blob *item, void *buffer, unsigned long size) +{ + item->object.parsed = 1; + return 0; +} + +int parse_blob(struct blob *item) +{ + char type[20]; + void *buffer; + unsigned long size; + int ret; + + if (item->object.parsed) + return 0; + buffer = read_sha1_file(item->object.sha1, type, &size); + if (!buffer) + return error("Could not read %s", + sha1_to_hex(item->object.sha1)); + if (strcmp(type, blob_type)) + return error("Object %s not a blob", + sha1_to_hex(item->object.sha1)); + ret = parse_blob_buffer(item, buffer, size); + free(buffer); + return ret; +} diff --git a/blob.h b/blob.h new file mode 100644 index 0000000000..ea5d9e9f8b --- /dev/null +++ b/blob.h @@ -0,0 +1,18 @@ +#ifndef BLOB_H +#define BLOB_H + +#include "object.h" + +extern const char *blob_type; + +struct blob { + struct object object; +}; + +struct blob *lookup_blob(const unsigned char *sha1); + +int parse_blob_buffer(struct blob *item, void *buffer, unsigned long size); + +int parse_blob(struct blob *item); + +#endif /* BLOB_H */ diff --git a/builtin-add.c b/builtin-add.c new file mode 100644 index 0000000000..87e16aa220 --- /dev/null +++ b/builtin-add.c @@ -0,0 +1,201 @@ +/* + * "git add" builtin command + * + * Copyright (C) 2006 Linus Torvalds + */ +#include "cache.h" +#include "builtin.h" +#include "dir.h" +#include "exec_cmd.h" +#include "cache-tree.h" + +static const char builtin_add_usage[] = +"git-add [-n] [-v] [-f] [--interactive | -i] [--] <filepattern>..."; + +static void prune_directory(struct dir_struct *dir, const char **pathspec, int prefix) +{ + char *seen; + int i, specs; + struct dir_entry **src, **dst; + + for (specs = 0; pathspec[specs]; specs++) + /* nothing */; + seen = xcalloc(specs, 1); + + src = dst = dir->entries; + i = dir->nr; + while (--i >= 0) { + struct dir_entry *entry = *src++; + if (match_pathspec(pathspec, entry->name, entry->len, + prefix, seen)) + *dst++ = entry; + } + dir->nr = dst - dir->entries; + + for (i = 0; i < specs; i++) { + struct stat st; + const char *match; + if (seen[i]) + continue; + + match = pathspec[i]; + if (!match[0]) + continue; + + /* Existing file? We must have ignored it */ + if (!lstat(match, &st)) { + struct dir_entry *ent; + + ent = dir_add_name(dir, match, strlen(match)); + ent->ignored = 1; + if (S_ISDIR(st.st_mode)) + ent->ignored_dir = 1; + continue; + } + die("pathspec '%s' did not match any files", match); + } +} + +static void fill_directory(struct dir_struct *dir, const char **pathspec) +{ + const char *path, *base; + int baselen; + + /* Set up the default git porcelain excludes */ + memset(dir, 0, sizeof(*dir)); + dir->exclude_per_dir = ".gitignore"; + path = git_path("info/exclude"); + if (!access(path, R_OK)) + add_excludes_from_file(dir, path); + + /* + * Calculate common prefix for the pathspec, and + * use that to optimize the directory walk + */ + baselen = common_prefix(pathspec); + path = "."; + base = ""; + if (baselen) { + char *common = xmalloc(baselen + 1); + memcpy(common, *pathspec, baselen); + common[baselen] = 0; + path = base = common; + } + + /* Read the directory and prune it */ + read_directory(dir, path, base, baselen); + if (pathspec) + prune_directory(dir, pathspec, baselen); +} + +static struct lock_file lock_file; + +static const char ignore_warning[] = +"The following paths are ignored by one of your .gitignore files:\n"; + +int cmd_add(int argc, const char **argv, const char *prefix) +{ + int i, newfd; + int verbose = 0, show_only = 0, ignored_too = 0; + const char **pathspec; + struct dir_struct dir; + int add_interactive = 0; + + for (i = 1; i < argc; i++) { + if (!strcmp("--interactive", argv[i]) || + !strcmp("-i", argv[i])) + add_interactive++; + } + if (add_interactive) { + const char *args[] = { "add--interactive", NULL }; + + if (add_interactive != 1 || argc != 2) + die("add --interactive does not take any parameters"); + execv_git_cmd(args); + exit(1); + } + + git_config(git_default_config); + + newfd = hold_lock_file_for_update(&lock_file, get_index_file(), 1); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (arg[0] != '-') + break; + if (!strcmp(arg, "--")) { + i++; + break; + } + if (!strcmp(arg, "-n")) { + show_only = 1; + continue; + } + if (!strcmp(arg, "-f")) { + ignored_too = 1; + continue; + } + if (!strcmp(arg, "-v")) { + verbose = 1; + continue; + } + usage(builtin_add_usage); + } + if (argc <= i) { + fprintf(stderr, "Nothing specified, nothing added.\n"); + fprintf(stderr, "Maybe you wanted to say 'git add .'?\n"); + return 0; + } + pathspec = get_pathspec(prefix, argv + i); + + fill_directory(&dir, pathspec); + + if (show_only) { + const char *sep = "", *eof = ""; + for (i = 0; i < dir.nr; i++) { + if (!ignored_too && dir.entries[i]->ignored) + continue; + printf("%s%s", sep, dir.entries[i]->name); + sep = " "; + eof = "\n"; + } + fputs(eof, stdout); + return 0; + } + + if (read_cache() < 0) + die("index file corrupt"); + + if (!ignored_too) { + int has_ignored = 0; + for (i = 0; i < dir.nr; i++) + if (dir.entries[i]->ignored) + has_ignored = 1; + if (has_ignored) { + fprintf(stderr, ignore_warning); + for (i = 0; i < dir.nr; i++) { + if (!dir.entries[i]->ignored) + continue; + fprintf(stderr, "%s", dir.entries[i]->name); + if (dir.entries[i]->ignored_dir) + fprintf(stderr, " (directory)"); + fputc('\n', stderr); + } + fprintf(stderr, + "Use -f if you really want to add them.\n"); + exit(1); + } + } + + for (i = 0; i < dir.nr; i++) + add_file_to_index(dir.entries[i]->name, verbose); + + if (active_cache_changed) { + if (write_cache(newfd, active_cache, active_nr) || + close(newfd) || commit_lock_file(&lock_file)) + die("Unable to write new index file"); + } + + return 0; +} diff --git a/builtin-annotate.c b/builtin-annotate.c new file mode 100644 index 0000000000..9db7cfe74c --- /dev/null +++ b/builtin-annotate.c @@ -0,0 +1,25 @@ +/* + * "git annotate" builtin alias + * + * Copyright (C) 2006 Ryan Anderson + */ +#include "git-compat-util.h" +#include "builtin.h" + +int cmd_annotate(int argc, const char **argv, const char *prefix) +{ + const char **nargv; + int i; + nargv = xmalloc(sizeof(char *) * (argc + 2)); + + nargv[0] = "annotate"; + nargv[1] = "-c"; + + for (i = 1; i < argc; i++) { + nargv[i+1] = argv[i]; + } + nargv[argc + 1] = NULL; + + return cmd_blame(argc + 1, nargv, prefix); +} + diff --git a/builtin-apply.c b/builtin-apply.c new file mode 100644 index 0000000000..3fefdacd94 --- /dev/null +++ b/builtin-apply.c @@ -0,0 +1,2760 @@ +/* + * apply.c + * + * Copyright (C) Linus Torvalds, 2005 + * + * This applies patches on top of some (arbitrary) version of the SCM. + * + */ +#include "cache.h" +#include "cache-tree.h" +#include "quote.h" +#include "blob.h" +#include "delta.h" +#include "builtin.h" + +/* + * --check turns on checking that the working tree matches the + * files that are being modified, but doesn't apply the patch + * --stat does just a diffstat, and doesn't actually apply + * --numstat does numeric diffstat, and doesn't actually apply + * --index-info shows the old and new index info for paths if available. + * --index updates the cache as well. + * --cached updates only the cache without ever touching the working tree. + */ +static const char *prefix; +static int prefix_length = -1; +static int newfd = -1; + +static int unidiff_zero; +static int p_value = 1; +static int check_index; +static int write_index; +static int cached; +static int diffstat; +static int numstat; +static int summary; +static int check; +static int apply = 1; +static int apply_in_reverse; +static int apply_with_reject; +static int apply_verbosely; +static int no_add; +static int show_index_info; +static int line_termination = '\n'; +static unsigned long p_context = ULONG_MAX; +static const char apply_usage[] = +"git-apply [--stat] [--numstat] [--summary] [--check] [--index] [--cached] [--apply] [--no-add] [--index-info] [--allow-binary-replacement] [--reverse] [--reject] [--verbose] [-z] [-pNUM] [-CNUM] [--whitespace=<nowarn|warn|error|error-all|strip>] <patch>..."; + +static enum whitespace_eol { + nowarn_whitespace, + warn_on_whitespace, + error_on_whitespace, + strip_whitespace, +} new_whitespace = warn_on_whitespace; +static int whitespace_error; +static int squelch_whitespace_errors = 5; +static int applied_after_stripping; +static const char *patch_input_file; + +static void parse_whitespace_option(const char *option) +{ + if (!option) { + new_whitespace = warn_on_whitespace; + return; + } + if (!strcmp(option, "warn")) { + new_whitespace = warn_on_whitespace; + return; + } + if (!strcmp(option, "nowarn")) { + new_whitespace = nowarn_whitespace; + return; + } + if (!strcmp(option, "error")) { + new_whitespace = error_on_whitespace; + return; + } + if (!strcmp(option, "error-all")) { + new_whitespace = error_on_whitespace; + squelch_whitespace_errors = 0; + return; + } + if (!strcmp(option, "strip")) { + new_whitespace = strip_whitespace; + return; + } + die("unrecognized whitespace option '%s'", option); +} + +static void set_default_whitespace_mode(const char *whitespace_option) +{ + if (!whitespace_option && !apply_default_whitespace) { + new_whitespace = (apply + ? warn_on_whitespace + : nowarn_whitespace); + } +} + +/* + * For "diff-stat" like behaviour, we keep track of the biggest change + * we've seen, and the longest filename. That allows us to do simple + * scaling. + */ +static int max_change, max_len; + +/* + * Various "current state", notably line numbers and what + * file (and how) we're patching right now.. The "is_xxxx" + * things are flags, where -1 means "don't know yet". + */ +static int linenr = 1; + +/* + * This represents one "hunk" from a patch, starting with + * "@@ -oldpos,oldlines +newpos,newlines @@" marker. The + * patch text is pointed at by patch, and its byte length + * is stored in size. leading and trailing are the number + * of context lines. + */ +struct fragment { + unsigned long leading, trailing; + unsigned long oldpos, oldlines; + unsigned long newpos, newlines; + const char *patch; + int size; + int rejected; + struct fragment *next; +}; + +/* + * When dealing with a binary patch, we reuse "leading" field + * to store the type of the binary hunk, either deflated "delta" + * or deflated "literal". + */ +#define binary_patch_method leading +#define BINARY_DELTA_DEFLATED 1 +#define BINARY_LITERAL_DEFLATED 2 + +struct patch { + char *new_name, *old_name, *def_name; + unsigned int old_mode, new_mode; + int is_new, is_delete; /* -1 = unknown, 0 = false, 1 = true */ + int rejected; + unsigned long deflate_origlen; + int lines_added, lines_deleted; + int score; + unsigned int inaccurate_eof:1; + unsigned int is_binary:1; + unsigned int is_copy:1; + unsigned int is_rename:1; + struct fragment *fragments; + char *result; + unsigned long resultsize; + char old_sha1_prefix[41]; + char new_sha1_prefix[41]; + struct patch *next; +}; + +static void say_patch_name(FILE *output, const char *pre, struct patch *patch, const char *post) +{ + fputs(pre, output); + if (patch->old_name && patch->new_name && + strcmp(patch->old_name, patch->new_name)) { + write_name_quoted(NULL, 0, patch->old_name, 1, output); + fputs(" => ", output); + write_name_quoted(NULL, 0, patch->new_name, 1, output); + } + else { + const char *n = patch->new_name; + if (!n) + n = patch->old_name; + write_name_quoted(NULL, 0, n, 1, output); + } + fputs(post, output); +} + +#define CHUNKSIZE (8192) +#define SLOP (16) + +static void *read_patch_file(int fd, unsigned long *sizep) +{ + unsigned long size = 0, alloc = CHUNKSIZE; + void *buffer = xmalloc(alloc); + + for (;;) { + int nr = alloc - size; + if (nr < 1024) { + alloc += CHUNKSIZE; + buffer = xrealloc(buffer, alloc); + nr = alloc - size; + } + nr = xread(fd, (char *) buffer + size, nr); + if (!nr) + break; + if (nr < 0) + die("git-apply: read returned %s", strerror(errno)); + size += nr; + } + *sizep = size; + + /* + * Make sure that we have some slop in the buffer + * so that we can do speculative "memcmp" etc, and + * see to it that it is NUL-filled. + */ + if (alloc < size + SLOP) + buffer = xrealloc(buffer, size + SLOP); + memset((char *) buffer + size, 0, SLOP); + return buffer; +} + +static unsigned long linelen(const char *buffer, unsigned long size) +{ + unsigned long len = 0; + while (size--) { + len++; + if (*buffer++ == '\n') + break; + } + return len; +} + +static int is_dev_null(const char *str) +{ + return !memcmp("/dev/null", str, 9) && isspace(str[9]); +} + +#define TERM_SPACE 1 +#define TERM_TAB 2 + +static int name_terminate(const char *name, int namelen, int c, int terminate) +{ + if (c == ' ' && !(terminate & TERM_SPACE)) + return 0; + if (c == '\t' && !(terminate & TERM_TAB)) + return 0; + + return 1; +} + +static char * find_name(const char *line, char *def, int p_value, int terminate) +{ + int len; + const char *start = line; + char *name; + + if (*line == '"') { + /* Proposed "new-style" GNU patch/diff format; see + * http://marc.theaimsgroup.com/?l=git&m=112927316408690&w=2 + */ + name = unquote_c_style(line, NULL); + if (name) { + char *cp = name; + while (p_value) { + cp = strchr(name, '/'); + if (!cp) + break; + cp++; + p_value--; + } + if (cp) { + /* name can later be freed, so we need + * to memmove, not just return cp + */ + memmove(name, cp, strlen(cp) + 1); + free(def); + return name; + } + else { + free(name); + name = NULL; + } + } + } + + for (;;) { + char c = *line; + + if (isspace(c)) { + if (c == '\n') + break; + if (name_terminate(start, line-start, c, terminate)) + break; + } + line++; + if (c == '/' && !--p_value) + start = line; + } + if (!start) + return def; + len = line - start; + if (!len) + return def; + + /* + * Generally we prefer the shorter name, especially + * if the other one is just a variation of that with + * something else tacked on to the end (ie "file.orig" + * or "file~"). + */ + if (def) { + int deflen = strlen(def); + if (deflen < len && !strncmp(start, def, deflen)) + return def; + } + + name = xmalloc(len + 1); + memcpy(name, start, len); + name[len] = 0; + free(def); + return name; +} + +/* + * Get the name etc info from the --/+++ lines of a traditional patch header + * + * NOTE! This hardcodes "-p1" behaviour in filename detection. + * + * FIXME! The end-of-filename heuristics are kind of screwy. For existing + * files, we can happily check the index for a match, but for creating a + * new file we should try to match whatever "patch" does. I have no idea. + */ +static void parse_traditional_patch(const char *first, const char *second, struct patch *patch) +{ + char *name; + + first += 4; /* skip "--- " */ + second += 4; /* skip "+++ " */ + if (is_dev_null(first)) { + patch->is_new = 1; + patch->is_delete = 0; + name = find_name(second, NULL, p_value, TERM_SPACE | TERM_TAB); + patch->new_name = name; + } else if (is_dev_null(second)) { + patch->is_new = 0; + patch->is_delete = 1; + name = find_name(first, NULL, p_value, TERM_SPACE | TERM_TAB); + patch->old_name = name; + } else { + name = find_name(first, NULL, p_value, TERM_SPACE | TERM_TAB); + name = find_name(second, name, p_value, TERM_SPACE | TERM_TAB); + patch->old_name = patch->new_name = name; + } + if (!name) + die("unable to find filename in patch at line %d", linenr); +} + +static int gitdiff_hdrend(const char *line, struct patch *patch) +{ + return -1; +} + +/* + * We're anal about diff header consistency, to make + * sure that we don't end up having strange ambiguous + * patches floating around. + * + * As a result, gitdiff_{old|new}name() will check + * their names against any previous information, just + * to make sure.. + */ +static char *gitdiff_verify_name(const char *line, int isnull, char *orig_name, const char *oldnew) +{ + if (!orig_name && !isnull) + return find_name(line, NULL, 1, TERM_TAB); + + if (orig_name) { + int len; + const char *name; + char *another; + name = orig_name; + len = strlen(name); + if (isnull) + die("git-apply: bad git-diff - expected /dev/null, got %s on line %d", name, linenr); + another = find_name(line, NULL, 1, TERM_TAB); + if (!another || memcmp(another, name, len)) + die("git-apply: bad git-diff - inconsistent %s filename on line %d", oldnew, linenr); + free(another); + return orig_name; + } + else { + /* expect "/dev/null" */ + if (memcmp("/dev/null", line, 9) || line[9] != '\n') + die("git-apply: bad git-diff - expected /dev/null on line %d", linenr); + return NULL; + } +} + +static int gitdiff_oldname(const char *line, struct patch *patch) +{ + patch->old_name = gitdiff_verify_name(line, patch->is_new, patch->old_name, "old"); + return 0; +} + +static int gitdiff_newname(const char *line, struct patch *patch) +{ + patch->new_name = gitdiff_verify_name(line, patch->is_delete, patch->new_name, "new"); + return 0; +} + +static int gitdiff_oldmode(const char *line, struct patch *patch) +{ + patch->old_mode = strtoul(line, NULL, 8); + return 0; +} + +static int gitdiff_newmode(const char *line, struct patch *patch) +{ + patch->new_mode = strtoul(line, NULL, 8); + return 0; +} + +static int gitdiff_delete(const char *line, struct patch *patch) +{ + patch->is_delete = 1; + patch->old_name = patch->def_name; + return gitdiff_oldmode(line, patch); +} + +static int gitdiff_newfile(const char *line, struct patch *patch) +{ + patch->is_new = 1; + patch->new_name = patch->def_name; + return gitdiff_newmode(line, patch); +} + +static int gitdiff_copysrc(const char *line, struct patch *patch) +{ + patch->is_copy = 1; + patch->old_name = find_name(line, NULL, 0, 0); + return 0; +} + +static int gitdiff_copydst(const char *line, struct patch *patch) +{ + patch->is_copy = 1; + patch->new_name = find_name(line, NULL, 0, 0); + return 0; +} + +static int gitdiff_renamesrc(const char *line, struct patch *patch) +{ + patch->is_rename = 1; + patch->old_name = find_name(line, NULL, 0, 0); + return 0; +} + +static int gitdiff_renamedst(const char *line, struct patch *patch) +{ + patch->is_rename = 1; + patch->new_name = find_name(line, NULL, 0, 0); + return 0; +} + +static int gitdiff_similarity(const char *line, struct patch *patch) +{ + if ((patch->score = strtoul(line, NULL, 10)) == ULONG_MAX) + patch->score = 0; + return 0; +} + +static int gitdiff_dissimilarity(const char *line, struct patch *patch) +{ + if ((patch->score = strtoul(line, NULL, 10)) == ULONG_MAX) + patch->score = 0; + return 0; +} + +static int gitdiff_index(const char *line, struct patch *patch) +{ + /* index line is N hexadecimal, "..", N hexadecimal, + * and optional space with octal mode. + */ + const char *ptr, *eol; + int len; + + ptr = strchr(line, '.'); + if (!ptr || ptr[1] != '.' || 40 < ptr - line) + return 0; + len = ptr - line; + memcpy(patch->old_sha1_prefix, line, len); + patch->old_sha1_prefix[len] = 0; + + line = ptr + 2; + ptr = strchr(line, ' '); + eol = strchr(line, '\n'); + + if (!ptr || eol < ptr) + ptr = eol; + len = ptr - line; + + if (40 < len) + return 0; + memcpy(patch->new_sha1_prefix, line, len); + patch->new_sha1_prefix[len] = 0; + if (*ptr == ' ') + patch->new_mode = patch->old_mode = strtoul(ptr+1, NULL, 8); + return 0; +} + +/* + * This is normal for a diff that doesn't change anything: we'll fall through + * into the next diff. Tell the parser to break out. + */ +static int gitdiff_unrecognized(const char *line, struct patch *patch) +{ + return -1; +} + +static const char *stop_at_slash(const char *line, int llen) +{ + int i; + + for (i = 0; i < llen; i++) { + int ch = line[i]; + if (ch == '/') + return line + i; + } + return NULL; +} + +/* This is to extract the same name that appears on "diff --git" + * line. We do not find and return anything if it is a rename + * patch, and it is OK because we will find the name elsewhere. + * We need to reliably find name only when it is mode-change only, + * creation or deletion of an empty file. In any of these cases, + * both sides are the same name under a/ and b/ respectively. + */ +static char *git_header_name(char *line, int llen) +{ + int len; + const char *name; + const char *second = NULL; + + line += strlen("diff --git "); + llen -= strlen("diff --git "); + + if (*line == '"') { + const char *cp; + char *first = unquote_c_style(line, &second); + if (!first) + return NULL; + + /* advance to the first slash */ + cp = stop_at_slash(first, strlen(first)); + if (!cp || cp == first) { + /* we do not accept absolute paths */ + free_first_and_fail: + free(first); + return NULL; + } + len = strlen(cp+1); + memmove(first, cp+1, len+1); /* including NUL */ + + /* second points at one past closing dq of name. + * find the second name. + */ + while ((second < line + llen) && isspace(*second)) + second++; + + if (line + llen <= second) + goto free_first_and_fail; + if (*second == '"') { + char *sp = unquote_c_style(second, NULL); + if (!sp) + goto free_first_and_fail; + cp = stop_at_slash(sp, strlen(sp)); + if (!cp || cp == sp) { + free_both_and_fail: + free(sp); + goto free_first_and_fail; + } + /* They must match, otherwise ignore */ + if (strcmp(cp+1, first)) + goto free_both_and_fail; + free(sp); + return first; + } + + /* unquoted second */ + cp = stop_at_slash(second, line + llen - second); + if (!cp || cp == second) + goto free_first_and_fail; + cp++; + if (line + llen - cp != len + 1 || + memcmp(first, cp, len)) + goto free_first_and_fail; + return first; + } + + /* unquoted first name */ + name = stop_at_slash(line, llen); + if (!name || name == line) + return NULL; + + name++; + + /* since the first name is unquoted, a dq if exists must be + * the beginning of the second name. + */ + for (second = name; second < line + llen; second++) { + if (*second == '"') { + const char *cp = second; + const char *np; + char *sp = unquote_c_style(second, NULL); + + if (!sp) + return NULL; + np = stop_at_slash(sp, strlen(sp)); + if (!np || np == sp) { + free_second_and_fail: + free(sp); + return NULL; + } + np++; + len = strlen(np); + if (len < cp - name && + !strncmp(np, name, len) && + isspace(name[len])) { + /* Good */ + memmove(sp, np, len + 1); + return sp; + } + goto free_second_and_fail; + } + } + + /* + * Accept a name only if it shows up twice, exactly the same + * form. + */ + for (len = 0 ; ; len++) { + switch (name[len]) { + default: + continue; + case '\n': + return NULL; + case '\t': case ' ': + second = name+len; + for (;;) { + char c = *second++; + if (c == '\n') + return NULL; + if (c == '/') + break; + } + if (second[len] == '\n' && !memcmp(name, second, len)) { + char *ret = xmalloc(len + 1); + memcpy(ret, name, len); + ret[len] = 0; + return ret; + } + } + } + return NULL; +} + +/* Verify that we recognize the lines following a git header */ +static int parse_git_header(char *line, int len, unsigned int size, struct patch *patch) +{ + unsigned long offset; + + /* A git diff has explicit new/delete information, so we don't guess */ + patch->is_new = 0; + patch->is_delete = 0; + + /* + * Some things may not have the old name in the + * rest of the headers anywhere (pure mode changes, + * or removing or adding empty files), so we get + * the default name from the header. + */ + patch->def_name = git_header_name(line, len); + + line += len; + size -= len; + linenr++; + for (offset = len ; size > 0 ; offset += len, size -= len, line += len, linenr++) { + static const struct opentry { + const char *str; + int (*fn)(const char *, struct patch *); + } optable[] = { + { "@@ -", gitdiff_hdrend }, + { "--- ", gitdiff_oldname }, + { "+++ ", gitdiff_newname }, + { "old mode ", gitdiff_oldmode }, + { "new mode ", gitdiff_newmode }, + { "deleted file mode ", gitdiff_delete }, + { "new file mode ", gitdiff_newfile }, + { "copy from ", gitdiff_copysrc }, + { "copy to ", gitdiff_copydst }, + { "rename old ", gitdiff_renamesrc }, + { "rename new ", gitdiff_renamedst }, + { "rename from ", gitdiff_renamesrc }, + { "rename to ", gitdiff_renamedst }, + { "similarity index ", gitdiff_similarity }, + { "dissimilarity index ", gitdiff_dissimilarity }, + { "index ", gitdiff_index }, + { "", gitdiff_unrecognized }, + }; + int i; + + len = linelen(line, size); + if (!len || line[len-1] != '\n') + break; + for (i = 0; i < ARRAY_SIZE(optable); i++) { + const struct opentry *p = optable + i; + int oplen = strlen(p->str); + if (len < oplen || memcmp(p->str, line, oplen)) + continue; + if (p->fn(line + oplen, patch) < 0) + return offset; + break; + } + } + + return offset; +} + +static int parse_num(const char *line, unsigned long *p) +{ + char *ptr; + + if (!isdigit(*line)) + return 0; + *p = strtoul(line, &ptr, 10); + return ptr - line; +} + +static int parse_range(const char *line, int len, int offset, const char *expect, + unsigned long *p1, unsigned long *p2) +{ + int digits, ex; + + if (offset < 0 || offset >= len) + return -1; + line += offset; + len -= offset; + + digits = parse_num(line, p1); + if (!digits) + return -1; + + offset += digits; + line += digits; + len -= digits; + + *p2 = 1; + if (*line == ',') { + digits = parse_num(line+1, p2); + if (!digits) + return -1; + + offset += digits+1; + line += digits+1; + len -= digits+1; + } + + ex = strlen(expect); + if (ex > len) + return -1; + if (memcmp(line, expect, ex)) + return -1; + + return offset + ex; +} + +/* + * Parse a unified diff fragment header of the + * form "@@ -a,b +c,d @@" + */ +static int parse_fragment_header(char *line, int len, struct fragment *fragment) +{ + int offset; + + if (!len || line[len-1] != '\n') + return -1; + + /* Figure out the number of lines in a fragment */ + offset = parse_range(line, len, 4, " +", &fragment->oldpos, &fragment->oldlines); + offset = parse_range(line, len, offset, " @@", &fragment->newpos, &fragment->newlines); + + return offset; +} + +static int find_header(char *line, unsigned long size, int *hdrsize, struct patch *patch) +{ + unsigned long offset, len; + + patch->is_rename = patch->is_copy = 0; + patch->is_new = patch->is_delete = -1; + patch->old_mode = patch->new_mode = 0; + patch->old_name = patch->new_name = NULL; + for (offset = 0; size > 0; offset += len, size -= len, line += len, linenr++) { + unsigned long nextlen; + + len = linelen(line, size); + if (!len) + break; + + /* Testing this early allows us to take a few shortcuts.. */ + if (len < 6) + continue; + + /* + * Make sure we don't find any unconnected patch fragments. + * That's a sign that we didn't find a header, and that a + * patch has become corrupted/broken up. + */ + if (!memcmp("@@ -", line, 4)) { + struct fragment dummy; + if (parse_fragment_header(line, len, &dummy) < 0) + continue; + die("patch fragment without header at line %d: %.*s", + linenr, (int)len-1, line); + } + + if (size < len + 6) + break; + + /* + * Git patch? It might not have a real patch, just a rename + * or mode change, so we handle that specially + */ + if (!memcmp("diff --git ", line, 11)) { + int git_hdr_len = parse_git_header(line, len, size, patch); + if (git_hdr_len <= len) + continue; + if (!patch->old_name && !patch->new_name) { + if (!patch->def_name) + die("git diff header lacks filename information (line %d)", linenr); + patch->old_name = patch->new_name = patch->def_name; + } + *hdrsize = git_hdr_len; + return offset; + } + + /** --- followed by +++ ? */ + if (memcmp("--- ", line, 4) || memcmp("+++ ", line + len, 4)) + continue; + + /* + * We only accept unified patches, so we want it to + * at least have "@@ -a,b +c,d @@\n", which is 14 chars + * minimum + */ + nextlen = linelen(line + len, size - len); + if (size < nextlen + 14 || memcmp("@@ -", line + len + nextlen, 4)) + continue; + + /* Ok, we'll consider it a patch */ + parse_traditional_patch(line, line+len, patch); + *hdrsize = len + nextlen; + linenr += 2; + return offset; + } + return -1; +} + +static void check_whitespace(const char *line, int len) +{ + const char *err = "Adds trailing whitespace"; + int seen_space = 0; + int i; + + /* + * We know len is at least two, since we have a '+' and we + * checked that the last character was a '\n' before calling + * this function. That is, an addition of an empty line would + * check the '+' here. Sneaky... + */ + if (isspace(line[len-2])) + goto error; + + /* + * Make sure that there is no space followed by a tab in + * indentation. + */ + err = "Space in indent is followed by a tab"; + for (i = 1; i < len; i++) { + if (line[i] == '\t') { + if (seen_space) + goto error; + } + else if (line[i] == ' ') + seen_space = 1; + else + break; + } + return; + + error: + whitespace_error++; + if (squelch_whitespace_errors && + squelch_whitespace_errors < whitespace_error) + ; + else + fprintf(stderr, "%s.\n%s:%d:%.*s\n", + err, patch_input_file, linenr, len-2, line+1); +} + + +/* + * Parse a unified diff. Note that this really needs to parse each + * fragment separately, since the only way to know the difference + * between a "---" that is part of a patch, and a "---" that starts + * the next patch is to look at the line counts.. + */ +static int parse_fragment(char *line, unsigned long size, struct patch *patch, struct fragment *fragment) +{ + int added, deleted; + int len = linelen(line, size), offset; + unsigned long oldlines, newlines; + unsigned long leading, trailing; + + offset = parse_fragment_header(line, len, fragment); + if (offset < 0) + return -1; + oldlines = fragment->oldlines; + newlines = fragment->newlines; + leading = 0; + trailing = 0; + + /* Parse the thing.. */ + line += len; + size -= len; + linenr++; + added = deleted = 0; + for (offset = len; + 0 < size; + offset += len, size -= len, line += len, linenr++) { + if (!oldlines && !newlines) + break; + len = linelen(line, size); + if (!len || line[len-1] != '\n') + return -1; + switch (*line) { + default: + return -1; + case '\n': /* newer GNU diff, an empty context line */ + case ' ': + oldlines--; + newlines--; + if (!deleted && !added) + leading++; + trailing++; + break; + case '-': + deleted++; + oldlines--; + trailing = 0; + break; + case '+': + if (new_whitespace != nowarn_whitespace) + check_whitespace(line, len); + added++; + newlines--; + trailing = 0; + break; + + /* We allow "\ No newline at end of file". Depending + * on locale settings when the patch was produced we + * don't know what this line looks like. The only + * thing we do know is that it begins with "\ ". + * Checking for 12 is just for sanity check -- any + * l10n of "\ No newline..." is at least that long. + */ + case '\\': + if (len < 12 || memcmp(line, "\\ ", 2)) + return -1; + break; + } + } + if (oldlines || newlines) + return -1; + fragment->leading = leading; + fragment->trailing = trailing; + + /* If a fragment ends with an incomplete line, we failed to include + * it in the above loop because we hit oldlines == newlines == 0 + * before seeing it. + */ + if (12 < size && !memcmp(line, "\\ ", 2)) + offset += linelen(line, size); + + patch->lines_added += added; + patch->lines_deleted += deleted; + + if (0 < patch->is_new && oldlines) + return error("new file depends on old contents"); + if (0 < patch->is_delete && newlines) + return error("deleted file still has contents"); + return offset; +} + +static int parse_single_patch(char *line, unsigned long size, struct patch *patch) +{ + unsigned long offset = 0; + unsigned long oldlines = 0, newlines = 0, context = 0; + struct fragment **fragp = &patch->fragments; + + while (size > 4 && !memcmp(line, "@@ -", 4)) { + struct fragment *fragment; + int len; + + fragment = xcalloc(1, sizeof(*fragment)); + len = parse_fragment(line, size, patch, fragment); + if (len <= 0) + die("corrupt patch at line %d", linenr); + fragment->patch = line; + fragment->size = len; + oldlines += fragment->oldlines; + newlines += fragment->newlines; + context += fragment->leading + fragment->trailing; + + *fragp = fragment; + fragp = &fragment->next; + + offset += len; + line += len; + size -= len; + } + + /* + * If something was removed (i.e. we have old-lines) it cannot + * be creation, and if something was added it cannot be + * deletion. However, the reverse is not true; --unified=0 + * patches that only add are not necessarily creation even + * though they do not have any old lines, and ones that only + * delete are not necessarily deletion. + * + * Unfortunately, a real creation/deletion patch do _not_ have + * any context line by definition, so we cannot safely tell it + * apart with --unified=0 insanity. At least if the patch has + * more than one hunk it is not creation or deletion. + */ + if (patch->is_new < 0 && + (oldlines || (patch->fragments && patch->fragments->next))) + patch->is_new = 0; + if (patch->is_delete < 0 && + (newlines || (patch->fragments && patch->fragments->next))) + patch->is_delete = 0; + if (!unidiff_zero || context) { + /* If the user says the patch is not generated with + * --unified=0, or if we have seen context lines, + * then not having oldlines means the patch is creation, + * and not having newlines means the patch is deletion. + */ + if (patch->is_new < 0 && !oldlines) { + patch->is_new = 1; + patch->old_name = NULL; + } + if (patch->is_delete < 0 && !newlines) { + patch->is_delete = 1; + patch->new_name = NULL; + } + } + + if (0 < patch->is_new && oldlines) + die("new file %s depends on old contents", patch->new_name); + if (0 < patch->is_delete && newlines) + die("deleted file %s still has contents", patch->old_name); + if (!patch->is_delete && !newlines && context) + fprintf(stderr, "** warning: file %s becomes empty but " + "is not deleted\n", patch->new_name); + + return offset; +} + +static inline int metadata_changes(struct patch *patch) +{ + return patch->is_rename > 0 || + patch->is_copy > 0 || + patch->is_new > 0 || + patch->is_delete || + (patch->old_mode && patch->new_mode && + patch->old_mode != patch->new_mode); +} + +static char *inflate_it(const void *data, unsigned long size, + unsigned long inflated_size) +{ + z_stream stream; + void *out; + int st; + + memset(&stream, 0, sizeof(stream)); + + stream.next_in = (unsigned char *)data; + stream.avail_in = size; + stream.next_out = out = xmalloc(inflated_size); + stream.avail_out = inflated_size; + inflateInit(&stream); + st = inflate(&stream, Z_FINISH); + if ((st != Z_STREAM_END) || stream.total_out != inflated_size) { + free(out); + return NULL; + } + return out; +} + +static struct fragment *parse_binary_hunk(char **buf_p, + unsigned long *sz_p, + int *status_p, + int *used_p) +{ + /* Expect a line that begins with binary patch method ("literal" + * or "delta"), followed by the length of data before deflating. + * a sequence of 'length-byte' followed by base-85 encoded data + * should follow, terminated by a newline. + * + * Each 5-byte sequence of base-85 encodes up to 4 bytes, + * and we would limit the patch line to 66 characters, + * so one line can fit up to 13 groups that would decode + * to 52 bytes max. The length byte 'A'-'Z' corresponds + * to 1-26 bytes, and 'a'-'z' corresponds to 27-52 bytes. + */ + int llen, used; + unsigned long size = *sz_p; + char *buffer = *buf_p; + int patch_method; + unsigned long origlen; + char *data = NULL; + int hunk_size = 0; + struct fragment *frag; + + llen = linelen(buffer, size); + used = llen; + + *status_p = 0; + + if (!strncmp(buffer, "delta ", 6)) { + patch_method = BINARY_DELTA_DEFLATED; + origlen = strtoul(buffer + 6, NULL, 10); + } + else if (!strncmp(buffer, "literal ", 8)) { + patch_method = BINARY_LITERAL_DEFLATED; + origlen = strtoul(buffer + 8, NULL, 10); + } + else + return NULL; + + linenr++; + buffer += llen; + while (1) { + int byte_length, max_byte_length, newsize; + llen = linelen(buffer, size); + used += llen; + linenr++; + if (llen == 1) { + /* consume the blank line */ + buffer++; + size--; + break; + } + /* Minimum line is "A00000\n" which is 7-byte long, + * and the line length must be multiple of 5 plus 2. + */ + if ((llen < 7) || (llen-2) % 5) + goto corrupt; + max_byte_length = (llen - 2) / 5 * 4; + byte_length = *buffer; + if ('A' <= byte_length && byte_length <= 'Z') + byte_length = byte_length - 'A' + 1; + else if ('a' <= byte_length && byte_length <= 'z') + byte_length = byte_length - 'a' + 27; + else + goto corrupt; + /* if the input length was not multiple of 4, we would + * have filler at the end but the filler should never + * exceed 3 bytes + */ + if (max_byte_length < byte_length || + byte_length <= max_byte_length - 4) + goto corrupt; + newsize = hunk_size + byte_length; + data = xrealloc(data, newsize); + if (decode_85(data + hunk_size, buffer + 1, byte_length)) + goto corrupt; + hunk_size = newsize; + buffer += llen; + size -= llen; + } + + frag = xcalloc(1, sizeof(*frag)); + frag->patch = inflate_it(data, hunk_size, origlen); + if (!frag->patch) + goto corrupt; + free(data); + frag->size = origlen; + *buf_p = buffer; + *sz_p = size; + *used_p = used; + frag->binary_patch_method = patch_method; + return frag; + + corrupt: + free(data); + *status_p = -1; + error("corrupt binary patch at line %d: %.*s", + linenr-1, llen-1, buffer); + return NULL; +} + +static int parse_binary(char *buffer, unsigned long size, struct patch *patch) +{ + /* We have read "GIT binary patch\n"; what follows is a line + * that says the patch method (currently, either "literal" or + * "delta") and the length of data before deflating; a + * sequence of 'length-byte' followed by base-85 encoded data + * follows. + * + * When a binary patch is reversible, there is another binary + * hunk in the same format, starting with patch method (either + * "literal" or "delta") with the length of data, and a sequence + * of length-byte + base-85 encoded data, terminated with another + * empty line. This data, when applied to the postimage, produces + * the preimage. + */ + struct fragment *forward; + struct fragment *reverse; + int status; + int used, used_1; + + forward = parse_binary_hunk(&buffer, &size, &status, &used); + if (!forward && !status) + /* there has to be one hunk (forward hunk) */ + return error("unrecognized binary patch at line %d", linenr-1); + if (status) + /* otherwise we already gave an error message */ + return status; + + reverse = parse_binary_hunk(&buffer, &size, &status, &used_1); + if (reverse) + used += used_1; + else if (status) { + /* not having reverse hunk is not an error, but having + * a corrupt reverse hunk is. + */ + free((void*) forward->patch); + free(forward); + return status; + } + forward->next = reverse; + patch->fragments = forward; + patch->is_binary = 1; + return used; +} + +static int parse_chunk(char *buffer, unsigned long size, struct patch *patch) +{ + int hdrsize, patchsize; + int offset = find_header(buffer, size, &hdrsize, patch); + + if (offset < 0) + return offset; + + patchsize = parse_single_patch(buffer + offset + hdrsize, size - offset - hdrsize, patch); + + if (!patchsize) { + static const char *binhdr[] = { + "Binary files ", + "Files ", + NULL, + }; + static const char git_binary[] = "GIT binary patch\n"; + int i; + int hd = hdrsize + offset; + unsigned long llen = linelen(buffer + hd, size - hd); + + if (llen == sizeof(git_binary) - 1 && + !memcmp(git_binary, buffer + hd, llen)) { + int used; + linenr++; + used = parse_binary(buffer + hd + llen, + size - hd - llen, patch); + if (used) + patchsize = used + llen; + else + patchsize = 0; + } + else if (!memcmp(" differ\n", buffer + hd + llen - 8, 8)) { + for (i = 0; binhdr[i]; i++) { + int len = strlen(binhdr[i]); + if (len < size - hd && + !memcmp(binhdr[i], buffer + hd, len)) { + linenr++; + patch->is_binary = 1; + patchsize = llen; + break; + } + } + } + + /* Empty patch cannot be applied if it is a text patch + * without metadata change. A binary patch appears + * empty to us here. + */ + if ((apply || check) && + (!patch->is_binary && !metadata_changes(patch))) + die("patch with only garbage at line %d", linenr); + } + + return offset + hdrsize + patchsize; +} + +#define swap(a,b) myswap((a),(b),sizeof(a)) + +#define myswap(a, b, size) do { \ + unsigned char mytmp[size]; \ + memcpy(mytmp, &a, size); \ + memcpy(&a, &b, size); \ + memcpy(&b, mytmp, size); \ +} while (0) + +static void reverse_patches(struct patch *p) +{ + for (; p; p = p->next) { + struct fragment *frag = p->fragments; + + swap(p->new_name, p->old_name); + swap(p->new_mode, p->old_mode); + swap(p->is_new, p->is_delete); + swap(p->lines_added, p->lines_deleted); + swap(p->old_sha1_prefix, p->new_sha1_prefix); + + for (; frag; frag = frag->next) { + swap(frag->newpos, frag->oldpos); + swap(frag->newlines, frag->oldlines); + } + } +} + +static const char pluses[] = "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"; +static const char minuses[]= "----------------------------------------------------------------------"; + +static void show_stats(struct patch *patch) +{ + const char *prefix = ""; + char *name = patch->new_name; + char *qname = NULL; + int len, max, add, del, total; + + if (!name) + name = patch->old_name; + + if (0 < (len = quote_c_style(name, NULL, NULL, 0))) { + qname = xmalloc(len + 1); + quote_c_style(name, qname, NULL, 0); + name = qname; + } + + /* + * "scale" the filename + */ + len = strlen(name); + max = max_len; + if (max > 50) + max = 50; + if (len > max) { + char *slash; + prefix = "..."; + max -= 3; + name += len - max; + slash = strchr(name, '/'); + if (slash) + name = slash; + } + len = max; + + /* + * scale the add/delete + */ + max = max_change; + if (max + len > 70) + max = 70 - len; + + add = patch->lines_added; + del = patch->lines_deleted; + total = add + del; + + if (max_change > 0) { + total = (total * max + max_change / 2) / max_change; + add = (add * max + max_change / 2) / max_change; + del = total - add; + } + if (patch->is_binary) + printf(" %s%-*s | Bin\n", prefix, len, name); + else + printf(" %s%-*s |%5d %.*s%.*s\n", prefix, + len, name, patch->lines_added + patch->lines_deleted, + add, pluses, del, minuses); + free(qname); +} + +static int read_old_data(struct stat *st, const char *path, void *buf, unsigned long size) +{ + int fd; + unsigned long got; + + switch (st->st_mode & S_IFMT) { + case S_IFLNK: + return readlink(path, buf, size); + case S_IFREG: + fd = open(path, O_RDONLY); + if (fd < 0) + return error("unable to open %s", path); + got = 0; + for (;;) { + int ret = xread(fd, (char *) buf + got, size - got); + if (ret <= 0) + break; + got += ret; + } + close(fd); + return got; + + default: + return -1; + } +} + +static int find_offset(const char *buf, unsigned long size, const char *fragment, unsigned long fragsize, int line, int *lines) +{ + int i; + unsigned long start, backwards, forwards; + + if (fragsize > size) + return -1; + + start = 0; + if (line > 1) { + unsigned long offset = 0; + i = line-1; + while (offset + fragsize <= size) { + if (buf[offset++] == '\n') { + start = offset; + if (!--i) + break; + } + } + } + + /* Exact line number? */ + if (!memcmp(buf + start, fragment, fragsize)) + return start; + + /* + * There's probably some smart way to do this, but I'll leave + * that to the smart and beautiful people. I'm simple and stupid. + */ + backwards = start; + forwards = start; + for (i = 0; ; i++) { + unsigned long try; + int n; + + /* "backward" */ + if (i & 1) { + if (!backwards) { + if (forwards + fragsize > size) + break; + continue; + } + do { + --backwards; + } while (backwards && buf[backwards-1] != '\n'); + try = backwards; + } else { + while (forwards + fragsize <= size) { + if (buf[forwards++] == '\n') + break; + } + try = forwards; + } + + if (try + fragsize > size) + continue; + if (memcmp(buf + try, fragment, fragsize)) + continue; + n = (i >> 1)+1; + if (i & 1) + n = -n; + *lines = n; + return try; + } + + /* + * We should start searching forward and backward. + */ + return -1; +} + +static void remove_first_line(const char **rbuf, int *rsize) +{ + const char *buf = *rbuf; + int size = *rsize; + unsigned long offset; + offset = 0; + while (offset <= size) { + if (buf[offset++] == '\n') + break; + } + *rsize = size - offset; + *rbuf = buf + offset; +} + +static void remove_last_line(const char **rbuf, int *rsize) +{ + const char *buf = *rbuf; + int size = *rsize; + unsigned long offset; + offset = size - 1; + while (offset > 0) { + if (buf[--offset] == '\n') + break; + } + *rsize = offset + 1; +} + +struct buffer_desc { + char *buffer; + unsigned long size; + unsigned long alloc; +}; + +static int apply_line(char *output, const char *patch, int plen) +{ + /* plen is number of bytes to be copied from patch, + * starting at patch+1 (patch[0] is '+'). Typically + * patch[plen] is '\n', unless this is the incomplete + * last line. + */ + int i; + int add_nl_to_tail = 0; + int fixed = 0; + int last_tab_in_indent = -1; + int last_space_in_indent = -1; + int need_fix_leading_space = 0; + char *buf; + + if ((new_whitespace != strip_whitespace) || !whitespace_error) { + memcpy(output, patch + 1, plen); + return plen; + } + + if (1 < plen && isspace(patch[plen-1])) { + if (patch[plen] == '\n') + add_nl_to_tail = 1; + plen--; + while (0 < plen && isspace(patch[plen])) + plen--; + fixed = 1; + } + + for (i = 1; i < plen; i++) { + char ch = patch[i]; + if (ch == '\t') { + last_tab_in_indent = i; + if (0 <= last_space_in_indent) + need_fix_leading_space = 1; + } + else if (ch == ' ') + last_space_in_indent = i; + else + break; + } + + buf = output; + if (need_fix_leading_space) { + /* between patch[1..last_tab_in_indent] strip the + * funny spaces, updating them to tab as needed. + */ + for (i = 1; i < last_tab_in_indent; i++, plen--) { + char ch = patch[i]; + if (ch != ' ') + *output++ = ch; + else if ((i % 8) == 0) + *output++ = '\t'; + } + fixed = 1; + i = last_tab_in_indent; + } + else + i = 1; + + memcpy(output, patch + i, plen); + if (add_nl_to_tail) + output[plen++] = '\n'; + if (fixed) + applied_after_stripping++; + return output + plen - buf; +} + +static int apply_one_fragment(struct buffer_desc *desc, struct fragment *frag, int inaccurate_eof) +{ + int match_beginning, match_end; + char *buf = desc->buffer; + const char *patch = frag->patch; + int offset, size = frag->size; + char *old = xmalloc(size); + char *new = xmalloc(size); + const char *oldlines, *newlines; + int oldsize = 0, newsize = 0; + unsigned long leading, trailing; + int pos, lines; + + while (size > 0) { + char first; + int len = linelen(patch, size); + int plen; + + if (!len) + break; + + /* + * "plen" is how much of the line we should use for + * the actual patch data. Normally we just remove the + * first character on the line, but if the line is + * followed by "\ No newline", then we also remove the + * last one (which is the newline, of course). + */ + plen = len-1; + if (len < size && patch[len] == '\\') + plen--; + first = *patch; + if (apply_in_reverse) { + if (first == '-') + first = '+'; + else if (first == '+') + first = '-'; + } + switch (first) { + case '\n': + /* Newer GNU diff, empty context line */ + if (plen < 0) + /* ... followed by '\No newline'; nothing */ + break; + old[oldsize++] = '\n'; + new[newsize++] = '\n'; + break; + case ' ': + case '-': + memcpy(old + oldsize, patch + 1, plen); + oldsize += plen; + if (first == '-') + break; + /* Fall-through for ' ' */ + case '+': + if (first != '+' || !no_add) + newsize += apply_line(new + newsize, patch, + plen); + break; + case '@': case '\\': + /* Ignore it, we already handled it */ + break; + default: + return -1; + } + patch += len; + size -= len; + } + + if (inaccurate_eof && oldsize > 0 && old[oldsize - 1] == '\n' && + newsize > 0 && new[newsize - 1] == '\n') { + oldsize--; + newsize--; + } + + oldlines = old; + 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. + * + * But that would break if the patch is generated with + * --unified=0; sane people wouldn't do that to cause us + * trouble, but we try to please not so sane ones as well. + */ + if (unidiff_zero) { + match_beginning = (!leading && !frag->oldpos); + match_end = 0; + } + else { + 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; + unsigned long alloc = desc->alloc; + + /* Warn if it was necessary to reduce the number + * of context lines. + */ + if ((leading != frag->leading) || + (trailing != frag->trailing)) + fprintf(stderr, "Context reduced to (%ld/%ld)" + " to apply fragment at %d\n", + leading, trailing, pos + lines); + + if (size > alloc) { + alloc = size + 8192; + desc->alloc = alloc; + buf = xrealloc(buf, alloc); + desc->buffer = buf; + } + desc->size = size; + memmove(buf + offset + newsize, + buf + offset + oldsize, + size - offset - newsize); + memcpy(buf + offset, newlines, newsize); + offset = 0; + + break; + } + + /* 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 (leading >= trailing) { + remove_first_line(&oldlines, &oldsize); + remove_first_line(&newlines, &newsize); + pos--; + leading--; + } + if (trailing > leading) { + remove_last_line(&oldlines, &oldsize); + remove_last_line(&newlines, &newsize); + trailing--; + } + } + + free(old); + free(new); + return offset; +} + +static int apply_binary_fragment(struct buffer_desc *desc, struct patch *patch) +{ + unsigned long dst_size; + struct fragment *fragment = patch->fragments; + void *data; + void *result; + + /* Binary patch is irreversible without the optional second hunk */ + if (apply_in_reverse) { + if (!fragment->next) + return error("cannot reverse-apply a binary patch " + "without the reverse hunk to '%s'", + patch->new_name + ? patch->new_name : patch->old_name); + fragment = fragment->next; + } + data = (void*) fragment->patch; + switch (fragment->binary_patch_method) { + case BINARY_DELTA_DEFLATED: + result = patch_delta(desc->buffer, desc->size, + data, + fragment->size, + &dst_size); + free(desc->buffer); + desc->buffer = result; + break; + case BINARY_LITERAL_DEFLATED: + free(desc->buffer); + desc->buffer = data; + dst_size = fragment->size; + break; + } + if (!desc->buffer) + return -1; + desc->size = desc->alloc = dst_size; + return 0; +} + +static int apply_binary(struct buffer_desc *desc, struct patch *patch) +{ + const char *name = patch->old_name ? patch->old_name : patch->new_name; + unsigned char sha1[20]; + + /* For safety, we require patch index line to contain + * full 40-byte textual SHA1 for old and new, at least for now. + */ + if (strlen(patch->old_sha1_prefix) != 40 || + strlen(patch->new_sha1_prefix) != 40 || + get_sha1_hex(patch->old_sha1_prefix, sha1) || + get_sha1_hex(patch->new_sha1_prefix, sha1)) + return error("cannot apply binary patch to '%s' " + "without full index line", name); + + if (patch->old_name) { + /* See if the old one matches what the patch + * applies to. + */ + hash_sha1_file(desc->buffer, desc->size, blob_type, sha1); + if (strcmp(sha1_to_hex(sha1), patch->old_sha1_prefix)) + return error("the patch applies to '%s' (%s), " + "which does not match the " + "current contents.", + name, sha1_to_hex(sha1)); + } + else { + /* Otherwise, the old one must be empty. */ + if (desc->size) + return error("the patch applies to an empty " + "'%s' but it is not empty", name); + } + + get_sha1_hex(patch->new_sha1_prefix, sha1); + if (is_null_sha1(sha1)) { + free(desc->buffer); + desc->alloc = desc->size = 0; + desc->buffer = NULL; + return 0; /* deletion patch */ + } + + if (has_sha1_file(sha1)) { + /* We already have the postimage */ + char type[10]; + unsigned long size; + + free(desc->buffer); + desc->buffer = read_sha1_file(sha1, type, &size); + if (!desc->buffer) + return error("the necessary postimage %s for " + "'%s' cannot be read", + patch->new_sha1_prefix, name); + desc->alloc = desc->size = size; + } + else { + /* We have verified desc matches the preimage; + * apply the patch data to it, which is stored + * in the patch->fragments->{patch,size}. + */ + if (apply_binary_fragment(desc, patch)) + return error("binary patch does not apply to '%s'", + name); + + /* verify that the result matches */ + hash_sha1_file(desc->buffer, desc->size, blob_type, sha1); + if (strcmp(sha1_to_hex(sha1), patch->new_sha1_prefix)) + return error("binary patch to '%s' creates incorrect result (expecting %s, got %s)", name, patch->new_sha1_prefix, sha1_to_hex(sha1)); + } + + return 0; +} + +static int apply_fragments(struct buffer_desc *desc, struct patch *patch) +{ + struct fragment *frag = patch->fragments; + const char *name = patch->old_name ? patch->old_name : patch->new_name; + + if (patch->is_binary) + return apply_binary(desc, patch); + + while (frag) { + if (apply_one_fragment(desc, frag, patch->inaccurate_eof)) { + error("patch failed: %s:%ld", name, frag->oldpos); + if (!apply_with_reject) + return -1; + frag->rejected = 1; + } + frag = frag->next; + } + return 0; +} + +static int apply_data(struct patch *patch, struct stat *st, struct cache_entry *ce) +{ + char *buf; + unsigned long size, alloc; + struct buffer_desc desc; + + size = 0; + alloc = 0; + buf = NULL; + if (cached) { + if (ce) { + char type[20]; + buf = read_sha1_file(ce->sha1, type, &size); + if (!buf) + return error("read of %s failed", + patch->old_name); + alloc = size; + } + } + else if (patch->old_name) { + size = st->st_size; + alloc = size + 8192; + buf = xmalloc(alloc); + if (read_old_data(st, patch->old_name, buf, alloc) != size) + return error("read of %s failed", patch->old_name); + } + + desc.size = size; + desc.alloc = alloc; + desc.buffer = buf; + + if (apply_fragments(&desc, patch) < 0) + return -1; /* note with --reject this succeeds. */ + + /* NUL terminate the result */ + if (desc.alloc <= desc.size) + desc.buffer = xrealloc(desc.buffer, desc.size + 1); + desc.buffer[desc.size] = 0; + + patch->result = desc.buffer; + patch->resultsize = desc.size; + + if (0 < patch->is_delete && patch->resultsize) + return error("removal patch leaves file contents"); + + return 0; +} + +static int check_patch(struct patch *patch, struct patch *prev_patch) +{ + struct stat st; + const char *old_name = patch->old_name; + const char *new_name = patch->new_name; + const char *name = old_name ? old_name : new_name; + struct cache_entry *ce = NULL; + int ok_if_exists; + + patch->rejected = 1; /* we will drop this after we succeed */ + if (old_name) { + int changed = 0; + int stat_ret = 0; + unsigned st_mode = 0; + + if (!cached) + stat_ret = lstat(old_name, &st); + if (check_index) { + int pos = cache_name_pos(old_name, strlen(old_name)); + if (pos < 0) + return error("%s: does not exist in index", + old_name); + ce = active_cache[pos]; + if (stat_ret < 0) { + struct checkout costate; + if (errno != ENOENT) + return error("%s: %s", old_name, + strerror(errno)); + /* checkout */ + costate.base_dir = ""; + costate.base_dir_len = 0; + costate.force = 0; + costate.quiet = 0; + costate.not_new = 0; + costate.refresh_cache = 1; + if (checkout_entry(ce, + &costate, + NULL) || + lstat(old_name, &st)) + return -1; + } + if (!cached) + changed = ce_match_stat(ce, &st, 1); + if (changed) + return error("%s: does not match index", + old_name); + if (cached) + st_mode = ntohl(ce->ce_mode); + } + else if (stat_ret < 0) + return error("%s: %s", old_name, strerror(errno)); + + if (!cached) + st_mode = ntohl(create_ce_mode(st.st_mode)); + + if (patch->is_new < 0) + patch->is_new = 0; + if (!patch->old_mode) + patch->old_mode = st_mode; + if ((st_mode ^ patch->old_mode) & S_IFMT) + return error("%s: wrong type", old_name); + if (st_mode != patch->old_mode) + fprintf(stderr, "warning: %s has type %o, expected %o\n", + old_name, st_mode, patch->old_mode); + } + + if (new_name && prev_patch && 0 < prev_patch->is_delete && + !strcmp(prev_patch->old_name, new_name)) + /* A type-change diff is always split into a patch to + * delete old, immediately followed by a patch to + * create new (see diff.c::run_diff()); in such a case + * it is Ok that the entry to be deleted by the + * previous patch is still in the working tree and in + * the index. + */ + ok_if_exists = 1; + else + ok_if_exists = 0; + + if (new_name && + ((0 < patch->is_new) | (0 < patch->is_rename) | patch->is_copy)) { + if (check_index && + cache_name_pos(new_name, strlen(new_name)) >= 0 && + !ok_if_exists) + return error("%s: already exists in index", new_name); + if (!cached) { + struct stat nst; + if (!lstat(new_name, &nst)) { + if (S_ISDIR(nst.st_mode) || ok_if_exists) + ; /* ok */ + else + return error("%s: already exists in working directory", new_name); + } + else if ((errno != ENOENT) && (errno != ENOTDIR)) + return error("%s: %s", new_name, strerror(errno)); + } + if (!patch->new_mode) { + if (0 < patch->is_new) + patch->new_mode = S_IFREG | 0644; + else + patch->new_mode = patch->old_mode; + } + } + + if (new_name && old_name) { + int same = !strcmp(old_name, new_name); + if (!patch->new_mode) + patch->new_mode = patch->old_mode; + if ((patch->old_mode ^ patch->new_mode) & S_IFMT) + return error("new mode (%o) of %s does not match old mode (%o)%s%s", + patch->new_mode, new_name, patch->old_mode, + same ? "" : " of ", same ? "" : old_name); + } + + if (apply_data(patch, &st, ce) < 0) + return error("%s: patch does not apply", name); + patch->rejected = 0; + return 0; +} + +static int check_patch_list(struct patch *patch) +{ + struct patch *prev_patch = NULL; + int err = 0; + + for (prev_patch = NULL; patch ; patch = patch->next) { + if (apply_verbosely) + say_patch_name(stderr, + "Checking patch ", patch, "...\n"); + err |= check_patch(patch, prev_patch); + prev_patch = patch; + } + return err; +} + +static void show_index_list(struct patch *list) +{ + struct patch *patch; + + /* Once we start supporting the reverse patch, it may be + * worth showing the new sha1 prefix, but until then... + */ + for (patch = list; patch; patch = patch->next) { + const unsigned char *sha1_ptr; + unsigned char sha1[20]; + const char *name; + + name = patch->old_name ? patch->old_name : patch->new_name; + if (0 < patch->is_new) + sha1_ptr = null_sha1; + else if (get_sha1(patch->old_sha1_prefix, sha1)) + die("sha1 information is lacking or useless (%s).", + name); + else + sha1_ptr = sha1; + + printf("%06o %s ",patch->old_mode, sha1_to_hex(sha1_ptr)); + if (line_termination && quote_c_style(name, NULL, NULL, 0)) + quote_c_style(name, NULL, stdout, 0); + else + fputs(name, stdout); + putchar(line_termination); + } +} + +static void stat_patch_list(struct patch *patch) +{ + int files, adds, dels; + + for (files = adds = dels = 0 ; patch ; patch = patch->next) { + files++; + adds += patch->lines_added; + dels += patch->lines_deleted; + show_stats(patch); + } + + printf(" %d files changed, %d insertions(+), %d deletions(-)\n", files, adds, dels); +} + +static void numstat_patch_list(struct patch *patch) +{ + for ( ; patch; patch = patch->next) { + const char *name; + name = patch->new_name ? patch->new_name : patch->old_name; + if (patch->is_binary) + printf("-\t-\t"); + else + printf("%d\t%d\t", + patch->lines_added, patch->lines_deleted); + if (line_termination && quote_c_style(name, NULL, NULL, 0)) + quote_c_style(name, NULL, stdout, 0); + else + fputs(name, stdout); + putchar(line_termination); + } +} + +static void show_file_mode_name(const char *newdelete, unsigned int mode, const char *name) +{ + if (mode) + printf(" %s mode %06o %s\n", newdelete, mode, name); + else + printf(" %s %s\n", newdelete, name); +} + +static void show_mode_change(struct patch *p, int show_name) +{ + if (p->old_mode && p->new_mode && p->old_mode != p->new_mode) { + if (show_name) + printf(" mode change %06o => %06o %s\n", + p->old_mode, p->new_mode, p->new_name); + else + printf(" mode change %06o => %06o\n", + p->old_mode, p->new_mode); + } +} + +static void show_rename_copy(struct patch *p) +{ + const char *renamecopy = p->is_rename ? "rename" : "copy"; + const char *old, *new; + + /* Find common prefix */ + old = p->old_name; + new = p->new_name; + while (1) { + const char *slash_old, *slash_new; + slash_old = strchr(old, '/'); + slash_new = strchr(new, '/'); + if (!slash_old || + !slash_new || + slash_old - old != slash_new - new || + memcmp(old, new, slash_new - new)) + break; + old = slash_old + 1; + new = slash_new + 1; + } + /* p->old_name thru old is the common prefix, and old and new + * through the end of names are renames + */ + if (old != p->old_name) + printf(" %s %.*s{%s => %s} (%d%%)\n", renamecopy, + (int)(old - p->old_name), p->old_name, + old, new, p->score); + else + printf(" %s %s => %s (%d%%)\n", renamecopy, + p->old_name, p->new_name, p->score); + show_mode_change(p, 0); +} + +static void summary_patch_list(struct patch *patch) +{ + struct patch *p; + + for (p = patch; p; p = p->next) { + if (p->is_new) + show_file_mode_name("create", p->new_mode, p->new_name); + else if (p->is_delete) + show_file_mode_name("delete", p->old_mode, p->old_name); + else { + if (p->is_rename || p->is_copy) + show_rename_copy(p); + else { + if (p->score) { + printf(" rewrite %s (%d%%)\n", + p->new_name, p->score); + show_mode_change(p, 0); + } + else + show_mode_change(p, 1); + } + } + } +} + +static void patch_stats(struct patch *patch) +{ + int lines = patch->lines_added + patch->lines_deleted; + + if (lines > max_change) + max_change = lines; + if (patch->old_name) { + int len = quote_c_style(patch->old_name, NULL, NULL, 0); + if (!len) + len = strlen(patch->old_name); + if (len > max_len) + max_len = len; + } + if (patch->new_name) { + int len = quote_c_style(patch->new_name, NULL, NULL, 0); + if (!len) + len = strlen(patch->new_name); + if (len > max_len) + max_len = len; + } +} + +static void remove_file(struct patch *patch) +{ + 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) { + if (!unlink(patch->old_name)) { + char *name = xstrdup(patch->old_name); + char *end = strrchr(name, '/'); + while (end) { + *end = 0; + if (rmdir(name)) + break; + end = strrchr(name, '/'); + } + free(name); + } + } +} + +static void add_index_file(const char *path, unsigned mode, void *buf, unsigned long size) +{ + struct stat st; + struct cache_entry *ce; + int namelen = strlen(path); + unsigned ce_size = cache_entry_size(namelen); + + if (!write_index) + return; + + ce = xcalloc(1, ce_size); + memcpy(ce->name, path, namelen); + ce->ce_mode = create_ce_mode(mode); + ce->ce_flags = htons(namelen); + if (!cached) { + if (lstat(path, &st) < 0) + die("unable to stat newly created file %s", path); + fill_stat_cache_info(ce, &st); + } + if (write_sha1_file(buf, size, blob_type, ce->sha1) < 0) + die("unable to create backing store for newly created file %s", path); + if (add_cache_entry(ce, ADD_CACHE_OK_TO_ADD) < 0) + die("unable to add cache entry for %s", path); +} + +static int try_create_file(const char *path, unsigned int mode, const char *buf, unsigned long size) +{ + int fd; + + if (S_ISLNK(mode)) + /* Although buf:size is counted string, it also is NUL + * terminated. + */ + return symlink(buf, path); + fd = open(path, O_CREAT | O_EXCL | O_WRONLY, (mode & 0100) ? 0777 : 0666); + if (fd < 0) + return -1; + while (size) { + int written = xwrite(fd, buf, size); + if (written < 0) + die("writing file %s: %s", path, strerror(errno)); + if (!written) + die("out of space writing file %s", path); + buf += written; + size -= written; + } + if (close(fd) < 0) + die("closing file %s: %s", path, strerror(errno)); + return 0; +} + +/* + * We optimistically assume that the directories exist, + * which is true 99% of the time anyway. If they don't, + * we create them and try again. + */ +static void create_one_file(char *path, unsigned mode, const char *buf, unsigned long size) +{ + if (cached) + return; + if (!try_create_file(path, mode, buf, size)) + return; + + if (errno == ENOENT) { + if (safe_create_leading_directories(path)) + return; + if (!try_create_file(path, mode, buf, size)) + return; + } + + if (errno == EEXIST || errno == EACCES) { + /* We may be trying to create a file where a directory + * used to be. + */ + struct stat st; + errno = 0; + if (!lstat(path, &st) && S_ISDIR(st.st_mode) && !rmdir(path)) + errno = EEXIST; + } + + if (errno == EEXIST) { + unsigned int nr = getpid(); + + for (;;) { + const char *newpath; + newpath = mkpath("%s~%u", path, nr); + if (!try_create_file(newpath, mode, buf, size)) { + if (!rename(newpath, path)) + return; + unlink(newpath); + break; + } + if (errno != EEXIST) + break; + ++nr; + } + } + die("unable to write file %s mode %o", path, mode); +} + +static void create_file(struct patch *patch) +{ + char *path = patch->new_name; + unsigned mode = patch->new_mode; + unsigned long size = patch->resultsize; + char *buf = patch->result; + + if (!mode) + mode = S_IFREG | 0644; + create_one_file(path, mode, buf, size); + add_index_file(path, mode, buf, size); + cache_tree_invalidate_path(active_cache_tree, path); +} + +/* phase zero is to remove, phase one is to create */ +static void write_out_one_result(struct patch *patch, int phase) +{ + if (patch->is_delete > 0) { + if (phase == 0) + remove_file(patch); + return; + } + if (patch->is_new > 0 || patch->is_copy) { + if (phase == 1) + create_file(patch); + return; + } + /* + * Rename or modification boils down to the same + * thing: remove the old, write the new + */ + if (phase == 0) + remove_file(patch); + if (phase == 1) + create_file(patch); +} + +static int write_out_one_reject(struct patch *patch) +{ + FILE *rej; + char namebuf[PATH_MAX]; + struct fragment *frag; + int cnt = 0; + + for (cnt = 0, frag = patch->fragments; frag; frag = frag->next) { + if (!frag->rejected) + continue; + cnt++; + } + + if (!cnt) { + if (apply_verbosely) + say_patch_name(stderr, + "Applied patch ", patch, " cleanly.\n"); + return 0; + } + + /* This should not happen, because a removal patch that leaves + * contents are marked "rejected" at the patch level. + */ + if (!patch->new_name) + die("internal error"); + + /* Say this even without --verbose */ + say_patch_name(stderr, "Applying patch ", patch, " with"); + fprintf(stderr, " %d rejects...\n", cnt); + + cnt = strlen(patch->new_name); + if (ARRAY_SIZE(namebuf) <= cnt + 5) { + cnt = ARRAY_SIZE(namebuf) - 5; + fprintf(stderr, + "warning: truncating .rej filename to %.*s.rej", + cnt - 1, patch->new_name); + } + memcpy(namebuf, patch->new_name, cnt); + memcpy(namebuf + cnt, ".rej", 5); + + rej = fopen(namebuf, "w"); + if (!rej) + return error("cannot open %s: %s", namebuf, strerror(errno)); + + /* Normal git tools never deal with .rej, so do not pretend + * this is a git patch by saying --git nor give extended + * headers. While at it, maybe please "kompare" that wants + * the trailing TAB and some garbage at the end of line ;-). + */ + fprintf(rej, "diff a/%s b/%s\t(rejected hunks)\n", + patch->new_name, patch->new_name); + for (cnt = 1, frag = patch->fragments; + frag; + cnt++, frag = frag->next) { + if (!frag->rejected) { + fprintf(stderr, "Hunk #%d applied cleanly.\n", cnt); + continue; + } + fprintf(stderr, "Rejected hunk #%d.\n", cnt); + fprintf(rej, "%.*s", frag->size, frag->patch); + if (frag->patch[frag->size-1] != '\n') + fputc('\n', rej); + } + fclose(rej); + return -1; +} + +static int write_out_results(struct patch *list, int skipped_patch) +{ + int phase; + int errs = 0; + struct patch *l; + + if (!list && !skipped_patch) + return error("No changes"); + + for (phase = 0; phase < 2; phase++) { + l = list; + while (l) { + if (l->rejected) + errs = 1; + else { + write_out_one_result(l, phase); + if (phase == 1 && write_out_one_reject(l)) + errs = 1; + } + l = l->next; + } + } + return errs; +} + +static struct lock_file lock_file; + +static struct excludes { + struct excludes *next; + const char *path; +} *excludes; + +static int use_patch(struct patch *p) +{ + const char *pathname = p->new_name ? p->new_name : p->old_name; + struct excludes *x = excludes; + while (x) { + if (fnmatch(x->path, pathname, 0) == 0) + return 0; + x = x->next; + } + if (0 < prefix_length) { + int pathlen = strlen(pathname); + if (pathlen <= prefix_length || + memcmp(prefix, pathname, prefix_length)) + return 0; + } + return 1; +} + +static int apply_patch(int fd, const char *filename, int inaccurate_eof) +{ + unsigned long offset, size; + char *buffer = read_patch_file(fd, &size); + struct patch *list = NULL, **listp = &list; + int skipped_patch = 0; + + patch_input_file = filename; + if (!buffer) + return -1; + offset = 0; + while (size > 0) { + struct patch *patch; + int nr; + + patch = xcalloc(1, sizeof(*patch)); + patch->inaccurate_eof = inaccurate_eof; + nr = parse_chunk(buffer + offset, size, patch); + if (nr < 0) + break; + if (apply_in_reverse) + reverse_patches(patch); + if (use_patch(patch)) { + patch_stats(patch); + *listp = patch; + listp = &patch->next; + } else { + /* perhaps free it a bit better? */ + free(patch); + skipped_patch++; + } + offset += nr; + size -= nr; + } + + if (whitespace_error && (new_whitespace == error_on_whitespace)) + apply = 0; + + write_index = check_index && apply; + if (write_index && newfd < 0) + newfd = hold_lock_file_for_update(&lock_file, + get_index_file(), 1); + if (check_index) { + if (read_cache() < 0) + die("unable to read index file"); + } + + if ((check || apply) && + check_patch_list(list) < 0 && + !apply_with_reject) + exit(1); + + if (apply && write_out_results(list, skipped_patch)) + exit(1); + + if (show_index_info) + show_index_list(list); + + if (diffstat) + stat_patch_list(list); + + if (numstat) + numstat_patch_list(list); + + if (summary) + summary_patch_list(list); + + free(buffer); + return 0; +} + +static int git_apply_config(const char *var, const char *value) +{ + if (!strcmp(var, "apply.whitespace")) { + apply_default_whitespace = xstrdup(value); + return 0; + } + return git_default_config(var, value); +} + + +int cmd_apply(int argc, const char **argv, const char *unused_prefix) +{ + int i; + int read_stdin = 1; + int inaccurate_eof = 0; + int errs = 0; + + const char *whitespace_option = NULL; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + char *end; + int fd; + + if (!strcmp(arg, "-")) { + errs |= apply_patch(0, "<stdin>", inaccurate_eof); + read_stdin = 0; + continue; + } + if (!strncmp(arg, "--exclude=", 10)) { + struct excludes *x = xmalloc(sizeof(*x)); + x->path = arg + 10; + x->next = excludes; + excludes = x; + continue; + } + if (!strncmp(arg, "-p", 2)) { + p_value = atoi(arg + 2); + continue; + } + if (!strcmp(arg, "--no-add")) { + no_add = 1; + continue; + } + if (!strcmp(arg, "--stat")) { + apply = 0; + diffstat = 1; + continue; + } + if (!strcmp(arg, "--allow-binary-replacement") || + !strcmp(arg, "--binary")) { + continue; /* now no-op */ + } + if (!strcmp(arg, "--numstat")) { + apply = 0; + numstat = 1; + continue; + } + if (!strcmp(arg, "--summary")) { + apply = 0; + summary = 1; + continue; + } + if (!strcmp(arg, "--check")) { + apply = 0; + check = 1; + continue; + } + if (!strcmp(arg, "--index")) { + check_index = 1; + continue; + } + if (!strcmp(arg, "--cached")) { + check_index = 1; + cached = 1; + continue; + } + if (!strcmp(arg, "--apply")) { + apply = 1; + continue; + } + if (!strcmp(arg, "--index-info")) { + apply = 0; + show_index_info = 1; + continue; + } + if (!strcmp(arg, "-z")) { + line_termination = 0; + continue; + } + if (!strncmp(arg, "-C", 2)) { + p_context = strtoul(arg + 2, &end, 0); + if (*end != '\0') + die("unrecognized context count '%s'", arg + 2); + continue; + } + if (!strncmp(arg, "--whitespace=", 13)) { + whitespace_option = arg + 13; + parse_whitespace_option(arg + 13); + continue; + } + if (!strcmp(arg, "-R") || !strcmp(arg, "--reverse")) { + apply_in_reverse = 1; + continue; + } + if (!strcmp(arg, "--unidiff-zero")) { + unidiff_zero = 1; + continue; + } + if (!strcmp(arg, "--reject")) { + apply = apply_with_reject = apply_verbosely = 1; + continue; + } + if (!strcmp(arg, "--verbose")) { + apply_verbosely = 1; + continue; + } + if (!strcmp(arg, "--inaccurate-eof")) { + inaccurate_eof = 1; + continue; + } + + if (check_index && prefix_length < 0) { + prefix = setup_git_directory(); + prefix_length = prefix ? strlen(prefix) : 0; + git_config(git_apply_config); + if (!whitespace_option && apply_default_whitespace) + parse_whitespace_option(apply_default_whitespace); + } + if (0 < prefix_length) + arg = prefix_filename(prefix, prefix_length, arg); + + fd = open(arg, O_RDONLY); + if (fd < 0) + usage(apply_usage); + read_stdin = 0; + set_default_whitespace_mode(whitespace_option); + errs |= apply_patch(fd, arg, inaccurate_eof); + close(fd); + } + set_default_whitespace_mode(whitespace_option); + if (read_stdin) + errs |= apply_patch(0, "<stdin>", inaccurate_eof); + if (whitespace_error) { + if (squelch_whitespace_errors && + squelch_whitespace_errors < whitespace_error) { + int squelched = + whitespace_error - squelch_whitespace_errors; + fprintf(stderr, "warning: squelched %d " + "whitespace error%s\n", + squelched, + squelched == 1 ? "" : "s"); + } + if (new_whitespace == error_on_whitespace) + die("%d line%s add%s trailing whitespaces.", + whitespace_error, + whitespace_error == 1 ? "" : "s", + whitespace_error == 1 ? "s" : ""); + if (applied_after_stripping) + fprintf(stderr, "warning: %d line%s applied after" + " stripping trailing whitespaces.\n", + applied_after_stripping, + applied_after_stripping == 1 ? "" : "s"); + else if (whitespace_error) + fprintf(stderr, "warning: %d line%s add%s trailing" + " whitespaces.\n", + whitespace_error, + whitespace_error == 1 ? "" : "s", + whitespace_error == 1 ? "s" : ""); + } + + if (write_index) { + if (write_cache(newfd, active_cache, active_nr) || + close(newfd) || commit_lock_file(&lock_file)) + die("Unable to write new index file"); + } + + return !!errs; +} diff --git a/builtin-archive.c b/builtin-archive.c new file mode 100644 index 0000000000..f613ac2516 --- /dev/null +++ b/builtin-archive.c @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2006 Franck Bui-Huu + * Copyright (c) 2006 Rene Scharfe + */ +#include "cache.h" +#include "builtin.h" +#include "archive.h" +#include "commit.h" +#include "tree-walk.h" +#include "exec_cmd.h" +#include "pkt-line.h" +#include "sideband.h" + +static const char archive_usage[] = \ +"git-archive --format=<fmt> [--prefix=<prefix>/] [--verbose] [<extra>] <tree-ish> [path...]"; + +static struct archiver_desc +{ + const char *name; + write_archive_fn_t write_archive; + parse_extra_args_fn_t parse_extra; +} archivers[] = { + { "tar", write_tar_archive, NULL }, + { "zip", write_zip_archive, parse_extra_zip_args }, +}; + +static int run_remote_archiver(const char *remote, int argc, + const char **argv) +{ + char *url, buf[LARGE_PACKET_MAX]; + int fd[2], i, len, rv; + pid_t pid; + const char *exec = "git-upload-archive"; + int exec_at = 0; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strncmp("--exec=", arg, 7)) { + if (exec_at) + die("multiple --exec specified"); + exec = arg + 7; + exec_at = i; + break; + } + } + + url = xstrdup(remote); + pid = git_connect(fd, url, exec); + if (pid < 0) + return pid; + + for (i = 1; i < argc; i++) { + if (i == exec_at) + continue; + packet_write(fd[1], "argument %s\n", argv[i]); + } + packet_flush(fd[1]); + + len = packet_read_line(fd[0], buf, sizeof(buf)); + if (!len) + die("git-archive: expected ACK/NAK, got EOF"); + if (buf[len-1] == '\n') + buf[--len] = 0; + if (strcmp(buf, "ACK")) { + if (len > 5 && !strncmp(buf, "NACK ", 5)) + die("git-archive: NACK %s", buf + 5); + die("git-archive: protocol error"); + } + + len = packet_read_line(fd[0], buf, sizeof(buf)); + if (len) + die("git-archive: expected a flush"); + + /* Now, start reading from fd[0] and spit it out to stdout */ + rv = recv_sideband("archive", fd[0], 1, 2); + close(fd[0]); + close(fd[1]); + rv |= finish_connect(pid); + + return !!rv; +} + +static int init_archiver(const char *name, struct archiver *ar) +{ + int rv = -1, i; + + for (i = 0; i < ARRAY_SIZE(archivers); i++) { + if (!strcmp(name, archivers[i].name)) { + memset(ar, 0, sizeof(*ar)); + ar->name = archivers[i].name; + ar->write_archive = archivers[i].write_archive; + ar->parse_extra = archivers[i].parse_extra; + rv = 0; + break; + } + } + return rv; +} + +void parse_pathspec_arg(const char **pathspec, struct archiver_args *ar_args) +{ + ar_args->pathspec = get_pathspec(ar_args->base, pathspec); +} + +void parse_treeish_arg(const char **argv, struct archiver_args *ar_args, + const char *prefix) +{ + const char *name = argv[0]; + const unsigned char *commit_sha1; + time_t archive_time; + struct tree *tree; + struct commit *commit; + unsigned char sha1[20]; + + if (get_sha1(name, sha1)) + die("Not a valid object name"); + + commit = lookup_commit_reference_gently(sha1, 1); + if (commit) { + commit_sha1 = commit->object.sha1; + archive_time = commit->date; + } else { + commit_sha1 = NULL; + archive_time = time(NULL); + } + + tree = parse_tree_indirect(sha1); + if (tree == NULL) + die("not a tree object"); + + if (prefix) { + unsigned char tree_sha1[20]; + unsigned int mode; + int err; + + err = get_tree_entry(tree->object.sha1, prefix, + tree_sha1, &mode); + if (err || !S_ISDIR(mode)) + die("current working directory is untracked"); + + tree = parse_tree_indirect(tree_sha1); + } + ar_args->tree = tree; + ar_args->commit_sha1 = commit_sha1; + ar_args->time = archive_time; +} + +int parse_archive_args(int argc, const char **argv, struct archiver *ar) +{ + const char *extra_argv[MAX_EXTRA_ARGS]; + int extra_argc = 0; + const char *format = NULL; /* might want to default to "tar" */ + const char *base = ""; + int verbose = 0; + int i; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (!strcmp(arg, "--list") || !strcmp(arg, "-l")) { + for (i = 0; i < ARRAY_SIZE(archivers); i++) + printf("%s\n", archivers[i].name); + exit(0); + } + if (!strcmp(arg, "--verbose") || !strcmp(arg, "-v")) { + verbose = 1; + continue; + } + if (!strncmp(arg, "--format=", 9)) { + format = arg + 9; + continue; + } + if (!strncmp(arg, "--prefix=", 9)) { + base = arg + 9; + continue; + } + if (!strcmp(arg, "--")) { + i++; + break; + } + if (arg[0] == '-') { + if (extra_argc > MAX_EXTRA_ARGS - 1) + die("Too many extra options"); + extra_argv[extra_argc++] = arg; + continue; + } + break; + } + + /* We need at least one parameter -- tree-ish */ + if (argc - 1 < i) + usage(archive_usage); + if (!format) + die("You must specify an archive format"); + if (init_archiver(format, ar) < 0) + die("Unknown archive format '%s'", format); + + if (extra_argc) { + if (!ar->parse_extra) + die("'%s' format does not handle %s", + ar->name, extra_argv[0]); + ar->args.extra = ar->parse_extra(extra_argc, extra_argv); + } + ar->args.verbose = verbose; + ar->args.base = base; + + return i; +} + +static const char *extract_remote_arg(int *ac, const char **av) +{ + int ix, iy, cnt = *ac; + int no_more_options = 0; + const char *remote = NULL; + + for (ix = iy = 1; ix < cnt; ix++) { + const char *arg = av[ix]; + if (!strcmp(arg, "--")) + no_more_options = 1; + if (!no_more_options) { + if (!strncmp(arg, "--remote=", 9)) { + if (remote) + die("Multiple --remote specified"); + remote = arg + 9; + continue; + } + if (arg[0] != '-') + no_more_options = 1; + } + if (ix != iy) + av[iy] = arg; + iy++; + } + if (remote) { + av[--cnt] = NULL; + *ac = cnt; + } + return remote; +} + +int cmd_archive(int argc, const char **argv, const char *prefix) +{ + struct archiver ar; + int tree_idx; + const char *remote = NULL; + + remote = extract_remote_arg(&argc, argv); + if (remote) + return run_remote_archiver(remote, argc, argv); + + setvbuf(stderr, NULL, _IOLBF, BUFSIZ); + + memset(&ar, 0, sizeof(ar)); + tree_idx = parse_archive_args(argc, argv, &ar); + if (prefix == NULL) + prefix = setup_git_directory(); + + argv += tree_idx; + parse_treeish_arg(argv, &ar.args, prefix); + parse_pathspec_arg(argv + 1, &ar.args); + + return ar.write_archive(&ar.args); +} diff --git a/builtin-blame.c b/builtin-blame.c new file mode 100644 index 0000000000..69fc145a38 --- /dev/null +++ b/builtin-blame.c @@ -0,0 +1,2357 @@ +/* + * Pickaxe + * + * Copyright (c) 2006, Junio C Hamano + */ + +#include "cache.h" +#include "builtin.h" +#include "blob.h" +#include "commit.h" +#include "tag.h" +#include "tree-walk.h" +#include "diff.h" +#include "diffcore.h" +#include "revision.h" +#include "quote.h" +#include "xdiff-interface.h" +#include "cache-tree.h" + +static char blame_usage[] = +"git-blame [-c] [-l] [-t] [-f] [-n] [-p] [-L n,m] [-S <revs-file>] [-M] [-C] [-C] [--contents <filename>] [--incremental] [commit] [--] file\n" +" -c, --compatibility Use the same output mode as git-annotate (Default: off)\n" +" -b Show blank SHA-1 for boundary commits (Default: off)\n" +" -l, --long Show long commit SHA1 (Default: off)\n" +" --root Do not treat root commits as boundaries (Default: off)\n" +" -t, --time Show raw timestamp (Default: off)\n" +" -f, --show-name Show original filename (Default: auto)\n" +" -n, --show-number Show original linenumber (Default: off)\n" +" -p, --porcelain Show in a format designed for machine consumption\n" +" -L n,m Process only line range n,m, counting from 1\n" +" -M, -C Find line movements within and across files\n" +" --incremental Show blame entries as we find them, incrementally\n" +" --contents file Use <file>'s contents as the final image\n" +" -S revs-file Use revisions from revs-file instead of calling git-rev-list\n"; + +static int longest_file; +static int longest_author; +static int max_orig_digits; +static int max_digits; +static int max_score_digits; +static int show_root; +static int blank_boundary; +static int incremental; +static int cmd_is_annotate; + +#ifndef DEBUG +#define DEBUG 0 +#endif + +/* stats */ +static int num_read_blob; +static int num_get_patch; +static int num_commits; + +#define PICKAXE_BLAME_MOVE 01 +#define PICKAXE_BLAME_COPY 02 +#define PICKAXE_BLAME_COPY_HARDER 04 + +/* + * blame for a blame_entry with score lower than these thresholds + * is not passed to the parent using move/copy logic. + */ +static unsigned blame_move_score; +static unsigned blame_copy_score; +#define BLAME_DEFAULT_MOVE_SCORE 20 +#define BLAME_DEFAULT_COPY_SCORE 40 + +/* bits #0..7 in revision.h, #8..11 used for merge_bases() in commit.c */ +#define METAINFO_SHOWN (1u<<12) +#define MORE_THAN_ONE_PATH (1u<<13) + +/* + * One blob in a commit that is being suspected + */ +struct origin { + int refcnt; + struct commit *commit; + mmfile_t file; + unsigned char blob_sha1[20]; + char path[FLEX_ARRAY]; +}; + +/* + * Given an origin, prepare mmfile_t structure to be used by the + * diff machinery + */ +static char *fill_origin_blob(struct origin *o, mmfile_t *file) +{ + if (!o->file.ptr) { + char type[10]; + num_read_blob++; + file->ptr = read_sha1_file(o->blob_sha1, type, + (unsigned long *)(&(file->size))); + o->file = *file; + } + else + *file = o->file; + return file->ptr; +} + +/* + * Origin is refcounted and usually we keep the blob contents to be + * reused. + */ +static inline struct origin *origin_incref(struct origin *o) +{ + if (o) + o->refcnt++; + return o; +} + +static void origin_decref(struct origin *o) +{ + if (o && --o->refcnt <= 0) { + if (o->file.ptr) + free(o->file.ptr); + memset(o, 0, sizeof(*o)); + free(o); + } +} + +/* + * Each group of lines is described by a blame_entry; it can be split + * as we pass blame to the parents. They form a linked list in the + * scoreboard structure, sorted by the target line number. + */ +struct blame_entry { + struct blame_entry *prev; + struct blame_entry *next; + + /* the first line of this group in the final image; + * internally all line numbers are 0 based. + */ + int lno; + + /* how many lines this group has */ + int num_lines; + + /* the commit that introduced this group into the final image */ + struct origin *suspect; + + /* true if the suspect is truly guilty; false while we have not + * checked if the group came from one of its parents. + */ + char guilty; + + /* the line number of the first line of this group in the + * suspect's file; internally all line numbers are 0 based. + */ + int s_lno; + + /* how significant this entry is -- cached to avoid + * scanning the lines over and over. + */ + unsigned score; +}; + +/* + * The current state of the blame assignment. + */ +struct scoreboard { + /* the final commit (i.e. where we started digging from) */ + struct commit *final; + + const char *path; + + /* + * The contents in the final image. + * Used by many functions to obtain contents of the nth line, + * indexed with scoreboard.lineno[blame_entry.lno]. + */ + const char *final_buf; + unsigned long final_buf_size; + + /* linked list of blames */ + struct blame_entry *ent; + + /* look-up a line in the final buffer */ + int num_lines; + int *lineno; +}; + +static int cmp_suspect(struct origin *a, struct origin *b) +{ + int cmp = hashcmp(a->commit->object.sha1, b->commit->object.sha1); + if (cmp) + return cmp; + return strcmp(a->path, b->path); +} + +#define cmp_suspect(a, b) ( ((a)==(b)) ? 0 : cmp_suspect(a,b) ) + +static void sanity_check_refcnt(struct scoreboard *); + +/* + * If two blame entries that are next to each other came from + * contiguous lines in the same origin (i.e. <commit, path> pair), + * merge them together. + */ +static void coalesce(struct scoreboard *sb) +{ + struct blame_entry *ent, *next; + + for (ent = sb->ent; ent && (next = ent->next); ent = next) { + if (!cmp_suspect(ent->suspect, next->suspect) && + ent->guilty == next->guilty && + ent->s_lno + ent->num_lines == next->s_lno) { + ent->num_lines += next->num_lines; + ent->next = next->next; + if (ent->next) + ent->next->prev = ent; + origin_decref(next->suspect); + free(next); + ent->score = 0; + next = ent; /* again */ + } + } + + if (DEBUG) /* sanity */ + sanity_check_refcnt(sb); +} + +/* + * Given a commit and a path in it, create a new origin structure. + * The callers that add blame to the scoreboard should use + * get_origin() to obtain shared, refcounted copy instead of calling + * this function directly. + */ +static struct origin *make_origin(struct commit *commit, const char *path) +{ + struct origin *o; + o = xcalloc(1, sizeof(*o) + strlen(path) + 1); + o->commit = commit; + o->refcnt = 1; + strcpy(o->path, path); + return o; +} + +/* + * Locate an existing origin or create a new one. + */ +static struct origin *get_origin(struct scoreboard *sb, + struct commit *commit, + const char *path) +{ + struct blame_entry *e; + + for (e = sb->ent; e; e = e->next) { + if (e->suspect->commit == commit && + !strcmp(e->suspect->path, path)) + return origin_incref(e->suspect); + } + return make_origin(commit, path); +} + +/* + * Fill the blob_sha1 field of an origin if it hasn't, so that later + * call to fill_origin_blob() can use it to locate the data. blob_sha1 + * for an origin is also used to pass the blame for the entire file to + * the parent to detect the case where a child's blob is identical to + * that of its parent's. + */ +static int fill_blob_sha1(struct origin *origin) +{ + unsigned mode; + char type[10]; + + if (!is_null_sha1(origin->blob_sha1)) + return 0; + if (get_tree_entry(origin->commit->object.sha1, + origin->path, + origin->blob_sha1, &mode)) + goto error_out; + if (sha1_object_info(origin->blob_sha1, type, NULL) || + strcmp(type, blob_type)) + goto error_out; + return 0; + error_out: + hashclr(origin->blob_sha1); + return -1; +} + +/* + * We have an origin -- check if the same path exists in the + * parent and return an origin structure to represent it. + */ +static struct origin *find_origin(struct scoreboard *sb, + struct commit *parent, + struct origin *origin) +{ + struct origin *porigin = NULL; + struct diff_options diff_opts; + const char *paths[2]; + + if (parent->util) { + /* + * Each commit object can cache one origin in that + * commit. This is a freestanding copy of origin and + * not refcounted. + */ + struct origin *cached = parent->util; + if (!strcmp(cached->path, origin->path)) { + /* + * The same path between origin and its parent + * without renaming -- the most common case. + */ + porigin = get_origin(sb, parent, cached->path); + + /* + * If the origin was newly created (i.e. get_origin + * would call make_origin if none is found in the + * scoreboard), it does not know the blob_sha1, + * so copy it. Otherwise porigin was in the + * scoreboard and already knows blob_sha1. + */ + if (porigin->refcnt == 1) + hashcpy(porigin->blob_sha1, cached->blob_sha1); + return porigin; + } + /* otherwise it was not very useful; free it */ + free(parent->util); + parent->util = NULL; + } + + /* See if the origin->path is different between parent + * and origin first. Most of the time they are the + * same and diff-tree is fairly efficient about this. + */ + diff_setup(&diff_opts); + diff_opts.recursive = 1; + diff_opts.detect_rename = 0; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + paths[0] = origin->path; + paths[1] = NULL; + + diff_tree_setup_paths(paths, &diff_opts); + if (diff_setup_done(&diff_opts) < 0) + die("diff-setup"); + + if (is_null_sha1(origin->commit->object.sha1)) + do_diff_cache(parent->tree->object.sha1, &diff_opts); + else + diff_tree_sha1(parent->tree->object.sha1, + origin->commit->tree->object.sha1, + "", &diff_opts); + diffcore_std(&diff_opts); + + /* It is either one entry that says "modified", or "created", + * or nothing. + */ + if (!diff_queued_diff.nr) { + /* The path is the same as parent */ + porigin = get_origin(sb, parent, origin->path); + hashcpy(porigin->blob_sha1, origin->blob_sha1); + } + else if (diff_queued_diff.nr != 1) + die("internal error in blame::find_origin"); + else { + struct diff_filepair *p = diff_queued_diff.queue[0]; + switch (p->status) { + default: + die("internal error in blame::find_origin (%c)", + p->status); + case 'M': + porigin = get_origin(sb, parent, origin->path); + hashcpy(porigin->blob_sha1, p->one->sha1); + break; + case 'A': + case 'T': + /* Did not exist in parent, or type changed */ + break; + } + } + diff_flush(&diff_opts); + if (porigin) { + /* + * Create a freestanding copy that is not part of + * the refcounted origin found in the scoreboard, and + * cache it in the commit. + */ + struct origin *cached; + + cached = make_origin(porigin->commit, porigin->path); + hashcpy(cached->blob_sha1, porigin->blob_sha1); + parent->util = cached; + } + return porigin; +} + +/* + * We have an origin -- find the path that corresponds to it in its + * parent and return an origin structure to represent it. + */ +static struct origin *find_rename(struct scoreboard *sb, + struct commit *parent, + struct origin *origin) +{ + struct origin *porigin = NULL; + struct diff_options diff_opts; + int i; + const char *paths[2]; + + diff_setup(&diff_opts); + diff_opts.recursive = 1; + diff_opts.detect_rename = DIFF_DETECT_RENAME; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_opts.single_follow = origin->path; + paths[0] = NULL; + diff_tree_setup_paths(paths, &diff_opts); + if (diff_setup_done(&diff_opts) < 0) + die("diff-setup"); + + if (is_null_sha1(origin->commit->object.sha1)) + do_diff_cache(parent->tree->object.sha1, &diff_opts); + else + diff_tree_sha1(parent->tree->object.sha1, + origin->commit->tree->object.sha1, + "", &diff_opts); + diffcore_std(&diff_opts); + + for (i = 0; i < diff_queued_diff.nr; i++) { + struct diff_filepair *p = diff_queued_diff.queue[i]; + if ((p->status == 'R' || p->status == 'C') && + !strcmp(p->two->path, origin->path)) { + porigin = get_origin(sb, parent, p->one->path); + hashcpy(porigin->blob_sha1, p->one->sha1); + break; + } + } + diff_flush(&diff_opts); + return porigin; +} + +/* + * Parsing of patch chunks... + */ +struct chunk { + /* line number in postimage; up to but not including this + * line is the same as preimage + */ + int same; + + /* preimage line number after this chunk */ + int p_next; + + /* postimage line number after this chunk */ + int t_next; +}; + +struct patch { + struct chunk *chunks; + int num; +}; + +struct blame_diff_state { + struct xdiff_emit_state xm; + struct patch *ret; + unsigned hunk_post_context; + unsigned hunk_in_pre_context : 1; +}; + +static void process_u_diff(void *state_, char *line, unsigned long len) +{ + struct blame_diff_state *state = state_; + struct chunk *chunk; + int off1, off2, len1, len2, num; + + num = state->ret->num; + if (len < 4 || line[0] != '@' || line[1] != '@') { + if (state->hunk_in_pre_context && line[0] == ' ') + state->ret->chunks[num - 1].same++; + else { + state->hunk_in_pre_context = 0; + if (line[0] == ' ') + state->hunk_post_context++; + else + state->hunk_post_context = 0; + } + return; + } + + if (num && state->hunk_post_context) { + chunk = &state->ret->chunks[num - 1]; + chunk->p_next -= state->hunk_post_context; + chunk->t_next -= state->hunk_post_context; + } + state->ret->num = ++num; + state->ret->chunks = xrealloc(state->ret->chunks, + sizeof(struct chunk) * num); + chunk = &state->ret->chunks[num - 1]; + if (parse_hunk_header(line, len, &off1, &len1, &off2, &len2)) { + state->ret->num--; + return; + } + + /* Line numbers in patch output are one based. */ + off1--; + off2--; + + chunk->same = len2 ? off2 : (off2 + 1); + + chunk->p_next = off1 + (len1 ? len1 : 1); + chunk->t_next = chunk->same + len2; + state->hunk_in_pre_context = 1; + state->hunk_post_context = 0; +} + +static struct patch *compare_buffer(mmfile_t *file_p, mmfile_t *file_o, + int context) +{ + struct blame_diff_state state; + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + + xpp.flags = XDF_NEED_MINIMAL; + xecfg.ctxlen = context; + xecfg.flags = 0; + ecb.outf = xdiff_outf; + ecb.priv = &state; + memset(&state, 0, sizeof(state)); + state.xm.consume = process_u_diff; + state.ret = xmalloc(sizeof(struct patch)); + state.ret->chunks = NULL; + state.ret->num = 0; + + xdl_diff(file_p, file_o, &xpp, &xecfg, &ecb); + + if (state.ret->num) { + struct chunk *chunk; + chunk = &state.ret->chunks[state.ret->num - 1]; + chunk->p_next -= state.hunk_post_context; + chunk->t_next -= state.hunk_post_context; + } + return state.ret; +} + +/* + * Run diff between two origins and grab the patch output, so that + * we can pass blame for lines origin is currently suspected for + * to its parent. + */ +static struct patch *get_patch(struct origin *parent, struct origin *origin) +{ + mmfile_t file_p, file_o; + struct patch *patch; + + fill_origin_blob(parent, &file_p); + fill_origin_blob(origin, &file_o); + if (!file_p.ptr || !file_o.ptr) + return NULL; + patch = compare_buffer(&file_p, &file_o, 0); + num_get_patch++; + return patch; +} + +static void free_patch(struct patch *p) +{ + free(p->chunks); + free(p); +} + +/* + * Link in a new blame entry to the scoreboard. Entries that cover the + * same line range have been removed from the scoreboard previously. + */ +static void add_blame_entry(struct scoreboard *sb, struct blame_entry *e) +{ + struct blame_entry *ent, *prev = NULL; + + origin_incref(e->suspect); + + for (ent = sb->ent; ent && ent->lno < e->lno; ent = ent->next) + prev = ent; + + /* prev, if not NULL, is the last one that is below e */ + e->prev = prev; + if (prev) { + e->next = prev->next; + prev->next = e; + } + else { + e->next = sb->ent; + sb->ent = e; + } + if (e->next) + e->next->prev = e; +} + +/* + * src typically is on-stack; we want to copy the information in it to + * an malloced blame_entry that is already on the linked list of the + * scoreboard. The origin of dst loses a refcnt while the origin of src + * gains one. + */ +static void dup_entry(struct blame_entry *dst, struct blame_entry *src) +{ + struct blame_entry *p, *n; + + p = dst->prev; + n = dst->next; + origin_incref(src->suspect); + origin_decref(dst->suspect); + memcpy(dst, src, sizeof(*src)); + dst->prev = p; + dst->next = n; + dst->score = 0; +} + +static const char *nth_line(struct scoreboard *sb, int lno) +{ + return sb->final_buf + sb->lineno[lno]; +} + +/* + * It is known that lines between tlno to same came from parent, and e + * has an overlap with that range. it also is known that parent's + * line plno corresponds to e's line tlno. + * + * <---- e -----> + * <------> + * <------------> + * <------------> + * <------------------> + * + * Split e into potentially three parts; before this chunk, the chunk + * to be blamed for the parent, and after that portion. + */ +static void split_overlap(struct blame_entry *split, + struct blame_entry *e, + int tlno, int plno, int same, + struct origin *parent) +{ + int chunk_end_lno; + memset(split, 0, sizeof(struct blame_entry [3])); + + if (e->s_lno < tlno) { + /* there is a pre-chunk part not blamed on parent */ + split[0].suspect = origin_incref(e->suspect); + split[0].lno = e->lno; + split[0].s_lno = e->s_lno; + split[0].num_lines = tlno - e->s_lno; + split[1].lno = e->lno + tlno - e->s_lno; + split[1].s_lno = plno; + } + else { + split[1].lno = e->lno; + split[1].s_lno = plno + (e->s_lno - tlno); + } + + if (same < e->s_lno + e->num_lines) { + /* there is a post-chunk part not blamed on parent */ + split[2].suspect = origin_incref(e->suspect); + split[2].lno = e->lno + (same - e->s_lno); + split[2].s_lno = e->s_lno + (same - e->s_lno); + split[2].num_lines = e->s_lno + e->num_lines - same; + chunk_end_lno = split[2].lno; + } + else + chunk_end_lno = e->lno + e->num_lines; + split[1].num_lines = chunk_end_lno - split[1].lno; + + /* + * if it turns out there is nothing to blame the parent for, + * forget about the splitting. !split[1].suspect signals this. + */ + if (split[1].num_lines < 1) + return; + split[1].suspect = origin_incref(parent); +} + +/* + * split_overlap() divided an existing blame e into up to three parts + * in split. Adjust the linked list of blames in the scoreboard to + * reflect the split. + */ +static void split_blame(struct scoreboard *sb, + struct blame_entry *split, + struct blame_entry *e) +{ + struct blame_entry *new_entry; + + if (split[0].suspect && split[2].suspect) { + /* The first part (reuse storage for the existing entry e) */ + dup_entry(e, &split[0]); + + /* The last part -- me */ + new_entry = xmalloc(sizeof(*new_entry)); + memcpy(new_entry, &(split[2]), sizeof(struct blame_entry)); + add_blame_entry(sb, new_entry); + + /* ... and the middle part -- parent */ + new_entry = xmalloc(sizeof(*new_entry)); + memcpy(new_entry, &(split[1]), sizeof(struct blame_entry)); + add_blame_entry(sb, new_entry); + } + else if (!split[0].suspect && !split[2].suspect) + /* + * The parent covers the entire area; reuse storage for + * e and replace it with the parent. + */ + dup_entry(e, &split[1]); + else if (split[0].suspect) { + /* me and then parent */ + dup_entry(e, &split[0]); + + new_entry = xmalloc(sizeof(*new_entry)); + memcpy(new_entry, &(split[1]), sizeof(struct blame_entry)); + add_blame_entry(sb, new_entry); + } + else { + /* parent and then me */ + dup_entry(e, &split[1]); + + new_entry = xmalloc(sizeof(*new_entry)); + memcpy(new_entry, &(split[2]), sizeof(struct blame_entry)); + add_blame_entry(sb, new_entry); + } + + if (DEBUG) { /* sanity */ + struct blame_entry *ent; + int lno = sb->ent->lno, corrupt = 0; + + for (ent = sb->ent; ent; ent = ent->next) { + if (lno != ent->lno) + corrupt = 1; + if (ent->s_lno < 0) + corrupt = 1; + lno += ent->num_lines; + } + if (corrupt) { + lno = sb->ent->lno; + for (ent = sb->ent; ent; ent = ent->next) { + printf("L %8d l %8d n %8d\n", + lno, ent->lno, ent->num_lines); + lno = ent->lno + ent->num_lines; + } + die("oops"); + } + } +} + +/* + * After splitting the blame, the origins used by the + * on-stack blame_entry should lose one refcnt each. + */ +static void decref_split(struct blame_entry *split) +{ + int i; + + for (i = 0; i < 3; i++) + origin_decref(split[i].suspect); +} + +/* + * Helper for blame_chunk(). blame_entry e is known to overlap with + * the patch hunk; split it and pass blame to the parent. + */ +static void blame_overlap(struct scoreboard *sb, struct blame_entry *e, + int tlno, int plno, int same, + struct origin *parent) +{ + struct blame_entry split[3]; + + split_overlap(split, e, tlno, plno, same, parent); + if (split[1].suspect) + split_blame(sb, split, e); + decref_split(split); +} + +/* + * Find the line number of the last line the target is suspected for. + */ +static int find_last_in_target(struct scoreboard *sb, struct origin *target) +{ + struct blame_entry *e; + int last_in_target = -1; + + for (e = sb->ent; e; e = e->next) { + if (e->guilty || cmp_suspect(e->suspect, target)) + continue; + if (last_in_target < e->s_lno + e->num_lines) + last_in_target = e->s_lno + e->num_lines; + } + return last_in_target; +} + +/* + * Process one hunk from the patch between the current suspect for + * blame_entry e and its parent. Find and split the overlap, and + * pass blame to the overlapping part to the parent. + */ +static void blame_chunk(struct scoreboard *sb, + int tlno, int plno, int same, + struct origin *target, struct origin *parent) +{ + struct blame_entry *e; + + for (e = sb->ent; e; e = e->next) { + if (e->guilty || cmp_suspect(e->suspect, target)) + continue; + if (same <= e->s_lno) + continue; + if (tlno < e->s_lno + e->num_lines) + blame_overlap(sb, e, tlno, plno, same, parent); + } +} + +/* + * We are looking at the origin 'target' and aiming to pass blame + * for the lines it is suspected to its parent. Run diff to find + * which lines came from parent and pass blame for them. + */ +static int pass_blame_to_parent(struct scoreboard *sb, + struct origin *target, + struct origin *parent) +{ + int i, last_in_target, plno, tlno; + struct patch *patch; + + last_in_target = find_last_in_target(sb, target); + if (last_in_target < 0) + return 1; /* nothing remains for this target */ + + patch = get_patch(parent, target); + plno = tlno = 0; + for (i = 0; i < patch->num; i++) { + struct chunk *chunk = &patch->chunks[i]; + + blame_chunk(sb, tlno, plno, chunk->same, target, parent); + plno = chunk->p_next; + tlno = chunk->t_next; + } + /* The rest (i.e. anything after tlno) are the same as the parent */ + blame_chunk(sb, tlno, plno, last_in_target, target, parent); + + free_patch(patch); + return 0; +} + +/* + * The lines in blame_entry after splitting blames many times can become + * very small and trivial, and at some point it becomes pointless to + * blame the parents. E.g. "\t\t}\n\t}\n\n" appears everywhere in any + * ordinary C program, and it is not worth to say it was copied from + * totally unrelated file in the parent. + * + * Compute how trivial the lines in the blame_entry are. + */ +static unsigned ent_score(struct scoreboard *sb, struct blame_entry *e) +{ + unsigned score; + const char *cp, *ep; + + if (e->score) + return e->score; + + score = 1; + cp = nth_line(sb, e->lno); + ep = nth_line(sb, e->lno + e->num_lines); + while (cp < ep) { + unsigned ch = *((unsigned char *)cp); + if (isalnum(ch)) + score++; + cp++; + } + e->score = score; + return score; +} + +/* + * best_so_far[] and this[] are both a split of an existing blame_entry + * that passes blame to the parent. Maintain best_so_far the best split + * so far, by comparing this and best_so_far and copying this into + * bst_so_far as needed. + */ +static void copy_split_if_better(struct scoreboard *sb, + struct blame_entry *best_so_far, + struct blame_entry *this) +{ + int i; + + if (!this[1].suspect) + return; + if (best_so_far[1].suspect) { + if (ent_score(sb, &this[1]) < ent_score(sb, &best_so_far[1])) + return; + } + + for (i = 0; i < 3; i++) + origin_incref(this[i].suspect); + decref_split(best_so_far); + memcpy(best_so_far, this, sizeof(struct blame_entry [3])); +} + +/* + * Find the lines from parent that are the same as ent so that + * we can pass blames to it. file_p has the blob contents for + * the parent. + */ +static void find_copy_in_blob(struct scoreboard *sb, + struct blame_entry *ent, + struct origin *parent, + struct blame_entry *split, + mmfile_t *file_p) +{ + const char *cp; + int cnt; + mmfile_t file_o; + struct patch *patch; + int i, plno, tlno; + + /* + * Prepare mmfile that contains only the lines in ent. + */ + cp = nth_line(sb, ent->lno); + file_o.ptr = (char*) cp; + cnt = ent->num_lines; + + while (cnt && cp < sb->final_buf + sb->final_buf_size) { + if (*cp++ == '\n') + cnt--; + } + file_o.size = cp - file_o.ptr; + + patch = compare_buffer(file_p, &file_o, 1); + + memset(split, 0, sizeof(struct blame_entry [3])); + plno = tlno = 0; + for (i = 0; i < patch->num; i++) { + struct chunk *chunk = &patch->chunks[i]; + + /* tlno to chunk->same are the same as ent */ + if (ent->num_lines <= tlno) + break; + if (tlno < chunk->same) { + struct blame_entry this[3]; + split_overlap(this, ent, + tlno + ent->s_lno, plno, + chunk->same + ent->s_lno, + parent); + copy_split_if_better(sb, split, this); + decref_split(this); + } + plno = chunk->p_next; + tlno = chunk->t_next; + } + free_patch(patch); +} + +/* + * See if lines currently target is suspected for can be attributed to + * parent. + */ +static int find_move_in_parent(struct scoreboard *sb, + struct origin *target, + struct origin *parent) +{ + int last_in_target, made_progress; + struct blame_entry *e, split[3]; + mmfile_t file_p; + + last_in_target = find_last_in_target(sb, target); + if (last_in_target < 0) + return 1; /* nothing remains for this target */ + + fill_origin_blob(parent, &file_p); + if (!file_p.ptr) + return 0; + + made_progress = 1; + while (made_progress) { + made_progress = 0; + for (e = sb->ent; e; e = e->next) { + if (e->guilty || cmp_suspect(e->suspect, target)) + continue; + find_copy_in_blob(sb, e, parent, split, &file_p); + if (split[1].suspect && + blame_move_score < ent_score(sb, &split[1])) { + split_blame(sb, split, e); + made_progress = 1; + } + decref_split(split); + } + } + return 0; +} + +struct blame_list { + struct blame_entry *ent; + struct blame_entry split[3]; +}; + +/* + * Count the number of entries the target is suspected for, + * and prepare a list of entry and the best split. + */ +static struct blame_list *setup_blame_list(struct scoreboard *sb, + struct origin *target, + int *num_ents_p) +{ + struct blame_entry *e; + int num_ents, i; + struct blame_list *blame_list = NULL; + + for (e = sb->ent, num_ents = 0; e; e = e->next) + if (!e->guilty && !cmp_suspect(e->suspect, target)) + num_ents++; + if (num_ents) { + blame_list = xcalloc(num_ents, sizeof(struct blame_list)); + for (e = sb->ent, i = 0; e; e = e->next) + if (!e->guilty && !cmp_suspect(e->suspect, target)) + blame_list[i++].ent = e; + } + *num_ents_p = num_ents; + return blame_list; +} + +/* + * For lines target is suspected for, see if we can find code movement + * across file boundary from the parent commit. porigin is the path + * in the parent we already tried. + */ +static int find_copy_in_parent(struct scoreboard *sb, + struct origin *target, + struct commit *parent, + struct origin *porigin, + int opt) +{ + struct diff_options diff_opts; + const char *paths[1]; + int i, j; + int retval; + struct blame_list *blame_list; + int num_ents; + + blame_list = setup_blame_list(sb, target, &num_ents); + if (!blame_list) + return 1; /* nothing remains for this target */ + + diff_setup(&diff_opts); + diff_opts.recursive = 1; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + + paths[0] = NULL; + diff_tree_setup_paths(paths, &diff_opts); + if (diff_setup_done(&diff_opts) < 0) + die("diff-setup"); + + /* Try "find copies harder" on new path if requested; + * we do not want to use diffcore_rename() actually to + * match things up; find_copies_harder is set only to + * force diff_tree_sha1() to feed all filepairs to diff_queue, + * and this code needs to be after diff_setup_done(), which + * usually makes find-copies-harder imply copy detection. + */ + if ((opt & PICKAXE_BLAME_COPY_HARDER) && + (!porigin || strcmp(target->path, porigin->path))) + diff_opts.find_copies_harder = 1; + + if (is_null_sha1(target->commit->object.sha1)) + do_diff_cache(parent->tree->object.sha1, &diff_opts); + else + diff_tree_sha1(parent->tree->object.sha1, + target->commit->tree->object.sha1, + "", &diff_opts); + + if (!diff_opts.find_copies_harder) + diffcore_std(&diff_opts); + + retval = 0; + while (1) { + int made_progress = 0; + + for (i = 0; i < diff_queued_diff.nr; i++) { + struct diff_filepair *p = diff_queued_diff.queue[i]; + struct origin *norigin; + mmfile_t file_p; + struct blame_entry this[3]; + + if (!DIFF_FILE_VALID(p->one)) + continue; /* does not exist in parent */ + if (porigin && !strcmp(p->one->path, porigin->path)) + /* find_move already dealt with this path */ + continue; + + norigin = get_origin(sb, parent, p->one->path); + hashcpy(norigin->blob_sha1, p->one->sha1); + fill_origin_blob(norigin, &file_p); + if (!file_p.ptr) + continue; + + for (j = 0; j < num_ents; j++) { + find_copy_in_blob(sb, blame_list[j].ent, + norigin, this, &file_p); + copy_split_if_better(sb, blame_list[j].split, + this); + decref_split(this); + } + origin_decref(norigin); + } + + for (j = 0; j < num_ents; j++) { + struct blame_entry *split = blame_list[j].split; + if (split[1].suspect && + blame_copy_score < ent_score(sb, &split[1])) { + split_blame(sb, split, blame_list[j].ent); + made_progress = 1; + } + decref_split(split); + } + free(blame_list); + + if (!made_progress) + break; + blame_list = setup_blame_list(sb, target, &num_ents); + if (!blame_list) { + retval = 1; + break; + } + } + diff_flush(&diff_opts); + + return retval; +} + +/* + * The blobs of origin and porigin exactly match, so everything + * origin is suspected for can be blamed on the parent. + */ +static void pass_whole_blame(struct scoreboard *sb, + struct origin *origin, struct origin *porigin) +{ + struct blame_entry *e; + + if (!porigin->file.ptr && origin->file.ptr) { + /* Steal its file */ + porigin->file = origin->file; + origin->file.ptr = NULL; + } + for (e = sb->ent; e; e = e->next) { + if (cmp_suspect(e->suspect, origin)) + continue; + origin_incref(porigin); + origin_decref(e->suspect); + e->suspect = porigin; + } +} + +#define MAXPARENT 16 + +static void pass_blame(struct scoreboard *sb, struct origin *origin, int opt) +{ + int i, pass; + struct commit *commit = origin->commit; + struct commit_list *parent; + struct origin *parent_origin[MAXPARENT], *porigin; + + memset(parent_origin, 0, sizeof(parent_origin)); + + /* The first pass looks for unrenamed path to optimize for + * common cases, then we look for renames in the second pass. + */ + for (pass = 0; pass < 2; pass++) { + struct origin *(*find)(struct scoreboard *, + struct commit *, struct origin *); + find = pass ? find_rename : find_origin; + + for (i = 0, parent = commit->parents; + i < MAXPARENT && parent; + parent = parent->next, i++) { + struct commit *p = parent->item; + int j, same; + + if (parent_origin[i]) + continue; + if (parse_commit(p)) + continue; + porigin = find(sb, p, origin); + if (!porigin) + continue; + if (!hashcmp(porigin->blob_sha1, origin->blob_sha1)) { + pass_whole_blame(sb, origin, porigin); + origin_decref(porigin); + goto finish; + } + for (j = same = 0; j < i; j++) + if (parent_origin[j] && + !hashcmp(parent_origin[j]->blob_sha1, + porigin->blob_sha1)) { + same = 1; + break; + } + if (!same) + parent_origin[i] = porigin; + else + origin_decref(porigin); + } + } + + num_commits++; + for (i = 0, parent = commit->parents; + i < MAXPARENT && parent; + parent = parent->next, i++) { + struct origin *porigin = parent_origin[i]; + if (!porigin) + continue; + if (pass_blame_to_parent(sb, origin, porigin)) + goto finish; + } + + /* + * Optionally find moves in parents' files. + */ + if (opt & PICKAXE_BLAME_MOVE) + for (i = 0, parent = commit->parents; + i < MAXPARENT && parent; + parent = parent->next, i++) { + struct origin *porigin = parent_origin[i]; + if (!porigin) + continue; + if (find_move_in_parent(sb, origin, porigin)) + goto finish; + } + + /* + * Optionally find copies from parents' files. + */ + if (opt & PICKAXE_BLAME_COPY) + for (i = 0, parent = commit->parents; + i < MAXPARENT && parent; + parent = parent->next, i++) { + struct origin *porigin = parent_origin[i]; + if (find_copy_in_parent(sb, origin, parent->item, + porigin, opt)) + goto finish; + } + + finish: + for (i = 0; i < MAXPARENT; i++) + origin_decref(parent_origin[i]); +} + +/* + * Information on commits, used for output. + */ +struct commit_info +{ + char *author; + char *author_mail; + unsigned long author_time; + char *author_tz; + + /* filled only when asked for details */ + char *committer; + char *committer_mail; + unsigned long committer_time; + char *committer_tz; + + char *summary; +}; + +/* + * Parse author/committer line in the commit object buffer + */ +static void get_ac_line(const char *inbuf, const char *what, + int bufsz, char *person, char **mail, + unsigned long *time, char **tz) +{ + int len; + char *tmp, *endp; + + tmp = strstr(inbuf, what); + if (!tmp) + goto error_out; + tmp += strlen(what); + endp = strchr(tmp, '\n'); + if (!endp) + len = strlen(tmp); + else + len = endp - tmp; + if (bufsz <= len) { + error_out: + /* Ugh */ + person = *mail = *tz = "(unknown)"; + *time = 0; + return; + } + memcpy(person, tmp, len); + + tmp = person; + tmp += len; + *tmp = 0; + while (*tmp != ' ') + tmp--; + *tz = tmp+1; + + *tmp = 0; + while (*tmp != ' ') + tmp--; + *time = strtoul(tmp, NULL, 10); + + *tmp = 0; + while (*tmp != ' ') + tmp--; + *mail = tmp + 1; + *tmp = 0; +} + +static void get_commit_info(struct commit *commit, + struct commit_info *ret, + int detailed) +{ + int len; + char *tmp, *endp; + static char author_buf[1024]; + static char committer_buf[1024]; + static char summary_buf[1024]; + + /* + * We've operated without save_commit_buffer, so + * we now need to populate them for output. + */ + if (!commit->buffer) { + char type[20]; + unsigned long size; + commit->buffer = + read_sha1_file(commit->object.sha1, type, &size); + } + ret->author = author_buf; + get_ac_line(commit->buffer, "\nauthor ", + sizeof(author_buf), author_buf, &ret->author_mail, + &ret->author_time, &ret->author_tz); + + if (!detailed) + return; + + ret->committer = committer_buf; + get_ac_line(commit->buffer, "\ncommitter ", + sizeof(committer_buf), committer_buf, &ret->committer_mail, + &ret->committer_time, &ret->committer_tz); + + ret->summary = summary_buf; + tmp = strstr(commit->buffer, "\n\n"); + if (!tmp) { + error_out: + sprintf(summary_buf, "(%s)", sha1_to_hex(commit->object.sha1)); + return; + } + tmp += 2; + endp = strchr(tmp, '\n'); + if (!endp) + endp = tmp + strlen(tmp); + len = endp - tmp; + if (len >= sizeof(summary_buf) || len == 0) + goto error_out; + memcpy(summary_buf, tmp, len); + summary_buf[len] = 0; +} + +/* + * To allow LF and other nonportable characters in pathnames, + * they are c-style quoted as needed. + */ +static void write_filename_info(const char *path) +{ + printf("filename "); + write_name_quoted(NULL, 0, path, 1, stdout); + putchar('\n'); +} + +/* + * The blame_entry is found to be guilty for the range. Mark it + * as such, and show it in incremental output. + */ +static void found_guilty_entry(struct blame_entry *ent) +{ + if (ent->guilty) + return; + ent->guilty = 1; + if (incremental) { + struct origin *suspect = ent->suspect; + + printf("%s %d %d %d\n", + sha1_to_hex(suspect->commit->object.sha1), + ent->s_lno + 1, ent->lno + 1, ent->num_lines); + if (!(suspect->commit->object.flags & METAINFO_SHOWN)) { + struct commit_info ci; + suspect->commit->object.flags |= METAINFO_SHOWN; + get_commit_info(suspect->commit, &ci, 1); + printf("author %s\n", ci.author); + printf("author-mail %s\n", ci.author_mail); + printf("author-time %lu\n", ci.author_time); + printf("author-tz %s\n", ci.author_tz); + printf("committer %s\n", ci.committer); + printf("committer-mail %s\n", ci.committer_mail); + printf("committer-time %lu\n", ci.committer_time); + printf("committer-tz %s\n", ci.committer_tz); + printf("summary %s\n", ci.summary); + if (suspect->commit->object.flags & UNINTERESTING) + printf("boundary\n"); + } + write_filename_info(suspect->path); + } +} + +/* + * The main loop -- while the scoreboard has lines whose true origin + * is still unknown, pick one blame_entry, and allow its current + * suspect to pass blames to its parents. + */ +static void assign_blame(struct scoreboard *sb, struct rev_info *revs, int opt) +{ + while (1) { + struct blame_entry *ent; + struct commit *commit; + struct origin *suspect = NULL; + + /* find one suspect to break down */ + for (ent = sb->ent; !suspect && ent; ent = ent->next) + if (!ent->guilty) + suspect = ent->suspect; + if (!suspect) + return; /* all done */ + + /* + * We will use this suspect later in the loop, + * so hold onto it in the meantime. + */ + origin_incref(suspect); + commit = suspect->commit; + if (!commit->object.parsed) + parse_commit(commit); + if (!(commit->object.flags & UNINTERESTING) && + !(revs->max_age != -1 && commit->date < revs->max_age)) + pass_blame(sb, suspect, opt); + else { + commit->object.flags |= UNINTERESTING; + if (commit->object.parsed) + mark_parents_uninteresting(commit); + } + /* treat root commit as boundary */ + if (!commit->parents && !show_root) + commit->object.flags |= UNINTERESTING; + + /* Take responsibility for the remaining entries */ + for (ent = sb->ent; ent; ent = ent->next) + if (!cmp_suspect(ent->suspect, suspect)) + found_guilty_entry(ent); + origin_decref(suspect); + + if (DEBUG) /* sanity */ + sanity_check_refcnt(sb); + } +} + +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); + minutes = tz < 0 ? -minutes : minutes; + t = time + minutes * 60; + tm = gmtime(&t); + + strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S ", tm); + strcat(time_buf, tz_str); + return time_buf; +} + +#define OUTPUT_ANNOTATE_COMPAT 001 +#define OUTPUT_LONG_OBJECT_NAME 002 +#define OUTPUT_RAW_TIMESTAMP 004 +#define OUTPUT_PORCELAIN 010 +#define OUTPUT_SHOW_NAME 020 +#define OUTPUT_SHOW_NUMBER 040 +#define OUTPUT_SHOW_SCORE 0100 + +static void emit_porcelain(struct scoreboard *sb, struct blame_entry *ent) +{ + int cnt; + const char *cp; + struct origin *suspect = ent->suspect; + char hex[41]; + + strcpy(hex, sha1_to_hex(suspect->commit->object.sha1)); + printf("%s%c%d %d %d\n", + hex, + ent->guilty ? ' ' : '*', // purely for debugging + ent->s_lno + 1, + ent->lno + 1, + ent->num_lines); + if (!(suspect->commit->object.flags & METAINFO_SHOWN)) { + struct commit_info ci; + suspect->commit->object.flags |= METAINFO_SHOWN; + get_commit_info(suspect->commit, &ci, 1); + printf("author %s\n", ci.author); + printf("author-mail %s\n", ci.author_mail); + printf("author-time %lu\n", ci.author_time); + printf("author-tz %s\n", ci.author_tz); + printf("committer %s\n", ci.committer); + printf("committer-mail %s\n", ci.committer_mail); + printf("committer-time %lu\n", ci.committer_time); + printf("committer-tz %s\n", ci.committer_tz); + write_filename_info(suspect->path); + printf("summary %s\n", ci.summary); + if (suspect->commit->object.flags & UNINTERESTING) + printf("boundary\n"); + } + else if (suspect->commit->object.flags & MORE_THAN_ONE_PATH) + write_filename_info(suspect->path); + + cp = nth_line(sb, ent->lno); + for (cnt = 0; cnt < ent->num_lines; cnt++) { + char ch; + if (cnt) + printf("%s %d %d\n", hex, + ent->s_lno + 1 + cnt, + ent->lno + 1 + cnt); + putchar('\t'); + do { + ch = *cp++; + putchar(ch); + } while (ch != '\n' && + cp < sb->final_buf + sb->final_buf_size); + } +} + +static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt) +{ + int cnt; + const char *cp; + struct origin *suspect = ent->suspect; + struct commit_info ci; + char hex[41]; + int show_raw_time = !!(opt & OUTPUT_RAW_TIMESTAMP); + + get_commit_info(suspect->commit, &ci, 1); + strcpy(hex, sha1_to_hex(suspect->commit->object.sha1)); + + cp = nth_line(sb, ent->lno); + for (cnt = 0; cnt < ent->num_lines; cnt++) { + char ch; + int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? 40 : 8; + + if (suspect->commit->object.flags & UNINTERESTING) { + if (blank_boundary) + memset(hex, ' ', length); + else if (!cmd_is_annotate) { + length--; + putchar('^'); + } + } + + printf("%.*s", length, hex); + if (opt & OUTPUT_ANNOTATE_COMPAT) + printf("\t(%10s\t%10s\t%d)", ci.author, + format_time(ci.author_time, ci.author_tz, + show_raw_time), + ent->lno + 1 + cnt); + else { + if (opt & OUTPUT_SHOW_SCORE) + printf(" %*d %02d", + max_score_digits, ent->score, + ent->suspect->refcnt); + if (opt & OUTPUT_SHOW_NAME) + printf(" %-*.*s", longest_file, longest_file, + suspect->path); + if (opt & OUTPUT_SHOW_NUMBER) + printf(" %*d", max_orig_digits, + ent->s_lno + 1 + cnt); + printf(" (%-*.*s %10s %*d) ", + longest_author, longest_author, ci.author, + format_time(ci.author_time, ci.author_tz, + show_raw_time), + max_digits, ent->lno + 1 + cnt); + } + do { + ch = *cp++; + putchar(ch); + } while (ch != '\n' && + cp < sb->final_buf + sb->final_buf_size); + } +} + +static void output(struct scoreboard *sb, int option) +{ + struct blame_entry *ent; + + if (option & OUTPUT_PORCELAIN) { + for (ent = sb->ent; ent; ent = ent->next) { + struct blame_entry *oth; + struct origin *suspect = ent->suspect; + struct commit *commit = suspect->commit; + if (commit->object.flags & MORE_THAN_ONE_PATH) + continue; + for (oth = ent->next; oth; oth = oth->next) { + if ((oth->suspect->commit != commit) || + !strcmp(oth->suspect->path, suspect->path)) + continue; + commit->object.flags |= MORE_THAN_ONE_PATH; + break; + } + } + } + + for (ent = sb->ent; ent; ent = ent->next) { + if (option & OUTPUT_PORCELAIN) + emit_porcelain(sb, ent); + else { + emit_other(sb, ent, option); + } + } +} + +/* + * To allow quick access to the contents of nth line in the + * final image, prepare an index in the scoreboard. + */ +static int prepare_lines(struct scoreboard *sb) +{ + const char *buf = sb->final_buf; + unsigned long len = sb->final_buf_size; + int num = 0, incomplete = 0, bol = 1; + + if (len && buf[len-1] != '\n') + incomplete++; /* incomplete line at the end */ + while (len--) { + if (bol) { + sb->lineno = xrealloc(sb->lineno, + sizeof(int* ) * (num + 1)); + sb->lineno[num] = buf - sb->final_buf; + bol = 0; + } + if (*buf++ == '\n') { + num++; + bol = 1; + } + } + sb->lineno = xrealloc(sb->lineno, + sizeof(int* ) * (num + incomplete + 1)); + sb->lineno[num + incomplete] = buf - sb->final_buf; + sb->num_lines = num + incomplete; + return sb->num_lines; +} + +/* + * Add phony grafts for use with -S; this is primarily to + * support git-cvsserver that wants to give a linear history + * to its clients. + */ +static int read_ancestry(const char *graft_file) +{ + FILE *fp = fopen(graft_file, "r"); + char buf[1024]; + if (!fp) + return -1; + while (fgets(buf, sizeof(buf), fp)) { + /* The format is just "Commit Parent1 Parent2 ...\n" */ + int len = strlen(buf); + struct commit_graft *graft = read_graft_line(buf, len); + if (graft) + register_commit_graft(graft, 0); + } + fclose(fp); + return 0; +} + +/* + * How many columns do we need to show line numbers in decimal? + */ +static int lineno_width(int lines) +{ + int i, width; + + for (width = 1, i = 10; i <= lines + 1; width++) + i *= 10; + return width; +} + +/* + * How many columns do we need to show line numbers, authors, + * and filenames? + */ +static void find_alignment(struct scoreboard *sb, int *option) +{ + int longest_src_lines = 0; + int longest_dst_lines = 0; + unsigned largest_score = 0; + struct blame_entry *e; + + for (e = sb->ent; e; e = e->next) { + struct origin *suspect = e->suspect; + struct commit_info ci; + int num; + + if (strcmp(suspect->path, sb->path)) + *option |= OUTPUT_SHOW_NAME; + num = strlen(suspect->path); + if (longest_file < num) + longest_file = num; + if (!(suspect->commit->object.flags & METAINFO_SHOWN)) { + suspect->commit->object.flags |= METAINFO_SHOWN; + get_commit_info(suspect->commit, &ci, 1); + num = strlen(ci.author); + if (longest_author < num) + longest_author = num; + } + num = e->s_lno + e->num_lines; + if (longest_src_lines < num) + longest_src_lines = num; + num = e->lno + e->num_lines; + if (longest_dst_lines < num) + longest_dst_lines = num; + if (largest_score < ent_score(sb, e)) + largest_score = ent_score(sb, e); + } + max_orig_digits = lineno_width(longest_src_lines); + max_digits = lineno_width(longest_dst_lines); + max_score_digits = lineno_width(largest_score); +} + +/* + * For debugging -- origin is refcounted, and this asserts that + * we do not underflow. + */ +static void sanity_check_refcnt(struct scoreboard *sb) +{ + int baa = 0; + struct blame_entry *ent; + + for (ent = sb->ent; ent; ent = ent->next) { + /* Nobody should have zero or negative refcnt */ + if (ent->suspect->refcnt <= 0) { + fprintf(stderr, "%s in %s has negative refcnt %d\n", + ent->suspect->path, + sha1_to_hex(ent->suspect->commit->object.sha1), + ent->suspect->refcnt); + baa = 1; + } + } + for (ent = sb->ent; ent; ent = ent->next) { + /* Mark the ones that haven't been checked */ + if (0 < ent->suspect->refcnt) + ent->suspect->refcnt = -ent->suspect->refcnt; + } + for (ent = sb->ent; ent; ent = ent->next) { + /* + * ... then pick each and see if they have the the + * correct refcnt. + */ + int found; + struct blame_entry *e; + struct origin *suspect = ent->suspect; + + if (0 < suspect->refcnt) + continue; + suspect->refcnt = -suspect->refcnt; /* Unmark */ + for (found = 0, e = sb->ent; e; e = e->next) { + if (e->suspect != suspect) + continue; + found++; + } + if (suspect->refcnt != found) { + fprintf(stderr, "%s in %s has refcnt %d, not %d\n", + ent->suspect->path, + sha1_to_hex(ent->suspect->commit->object.sha1), + ent->suspect->refcnt, found); + baa = 2; + } + } + if (baa) { + int opt = 0160; + find_alignment(sb, &opt); + output(sb, opt); + die("Baa %d!", baa); + } +} + +/* + * Used for the command line parsing; check if the path exists + * in the working tree. + */ +static int has_path_in_work_tree(const char *path) +{ + struct stat st; + return !lstat(path, &st); +} + +static unsigned parse_score(const char *arg) +{ + char *end; + unsigned long score = strtoul(arg, &end, 10); + if (*end) + return 0; + return score; +} + +static const char *add_prefix(const char *prefix, const char *path) +{ + if (!prefix || !prefix[0]) + return path; + return prefix_path(prefix, strlen(prefix), path); +} + +/* + * Parsing of (comma separated) one item in the -L option + */ +static const char *parse_loc(const char *spec, + struct scoreboard *sb, long lno, + long begin, long *ret) +{ + char *term; + const char *line; + long num; + int reg_error; + regex_t regexp; + regmatch_t match[1]; + + /* Allow "-L <something>,+20" to mean starting at <something> + * for 20 lines, or "-L <something>,-5" for 5 lines ending at + * <something>. + */ + if (1 < begin && (spec[0] == '+' || spec[0] == '-')) { + num = strtol(spec + 1, &term, 10); + if (term != spec + 1) { + if (spec[0] == '-') + num = 0 - num; + if (0 < num) + *ret = begin + num - 2; + else if (!num) + *ret = begin; + else + *ret = begin + num; + return term; + } + return spec; + } + num = strtol(spec, &term, 10); + if (term != spec) { + *ret = num; + return term; + } + if (spec[0] != '/') + return spec; + + /* it could be a regexp of form /.../ */ + for (term = (char*) spec + 1; *term && *term != '/'; term++) { + if (*term == '\\') + term++; + } + if (*term != '/') + return spec; + + /* try [spec+1 .. term-1] as regexp */ + *term = 0; + begin--; /* input is in human terms */ + line = nth_line(sb, begin); + + if (!(reg_error = regcomp(®exp, spec + 1, REG_NEWLINE)) && + !(reg_error = regexec(®exp, line, 1, match, 0))) { + const char *cp = line + match[0].rm_so; + const char *nline; + + while (begin++ < lno) { + nline = nth_line(sb, begin); + if (line <= cp && cp < nline) + break; + line = nline; + } + *ret = begin; + regfree(®exp); + *term++ = '/'; + return term; + } + else { + char errbuf[1024]; + regerror(reg_error, ®exp, errbuf, 1024); + die("-L parameter '%s': %s", spec + 1, errbuf); + } +} + +/* + * Parsing of -L option + */ +static void prepare_blame_range(struct scoreboard *sb, + const char *bottomtop, + long lno, + long *bottom, long *top) +{ + const char *term; + + term = parse_loc(bottomtop, sb, lno, 1, bottom); + if (*term == ',') { + term = parse_loc(term + 1, sb, lno, *bottom + 1, top); + if (*term) + usage(blame_usage); + } + if (*term) + usage(blame_usage); +} + +static int git_blame_config(const char *var, const char *value) +{ + if (!strcmp(var, "blame.showroot")) { + show_root = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "blame.blankboundary")) { + blank_boundary = git_config_bool(var, value); + return 0; + } + return git_default_config(var, value); +} + +static struct commit *fake_working_tree_commit(const char *path, const char *contents_from) +{ + struct commit *commit; + struct origin *origin; + unsigned char head_sha1[20]; + char *buf; + const char *ident; + int fd; + time_t now; + unsigned long fin_size; + int size, len; + struct cache_entry *ce; + unsigned mode; + + if (get_sha1("HEAD", head_sha1)) + die("No such ref: HEAD"); + + time(&now); + commit = xcalloc(1, sizeof(*commit)); + commit->parents = xcalloc(1, sizeof(*commit->parents)); + commit->parents->item = lookup_commit_reference(head_sha1); + commit->object.parsed = 1; + commit->date = now; + commit->object.type = OBJ_COMMIT; + + origin = make_origin(commit, path); + + if (!contents_from || strcmp("-", contents_from)) { + struct stat st; + const char *read_from; + + if (contents_from) { + if (stat(contents_from, &st) < 0) + die("Cannot stat %s", contents_from); + read_from = contents_from; + } + else { + if (lstat(path, &st) < 0) + die("Cannot lstat %s", path); + read_from = path; + } + fin_size = st.st_size; + buf = xmalloc(fin_size+1); + mode = canon_mode(st.st_mode); + switch (st.st_mode & S_IFMT) { + case S_IFREG: + fd = open(read_from, O_RDONLY); + if (fd < 0) + die("cannot open %s", read_from); + if (read_in_full(fd, buf, fin_size) != fin_size) + die("cannot read %s", read_from); + break; + case S_IFLNK: + if (readlink(read_from, buf, fin_size+1) != fin_size) + die("cannot readlink %s", read_from); + break; + default: + die("unsupported file type %s", read_from); + } + } + else { + /* Reading from stdin */ + contents_from = "standard input"; + buf = NULL; + fin_size = 0; + mode = 0; + while (1) { + ssize_t cnt = 8192; + buf = xrealloc(buf, fin_size + cnt); + cnt = xread(0, buf + fin_size, cnt); + if (cnt < 0) + die("read error %s from stdin", + strerror(errno)); + if (!cnt) + break; + fin_size += cnt; + } + buf = xrealloc(buf, fin_size + 1); + } + buf[fin_size] = 0; + origin->file.ptr = buf; + origin->file.size = fin_size; + pretend_sha1_file(buf, fin_size, blob_type, origin->blob_sha1); + commit->util = origin; + + /* + * Read the current index, replace the path entry with + * origin->blob_sha1 without mucking with its mode or type + * bits; we are not going to write this index out -- we just + * want to run "diff-index --cached". + */ + discard_cache(); + read_cache(); + + len = strlen(path); + if (!mode) { + int pos = cache_name_pos(path, len); + if (0 <= pos) + mode = ntohl(active_cache[pos]->ce_mode); + else + /* Let's not bother reading from HEAD tree */ + mode = S_IFREG | 0644; + } + size = cache_entry_size(len); + ce = xcalloc(1, size); + hashcpy(ce->sha1, origin->blob_sha1); + memcpy(ce->name, path, len); + ce->ce_flags = create_ce_flags(len, 0); + ce->ce_mode = create_ce_mode(mode); + add_cache_entry(ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); + + /* + * We are not going to write this out, so this does not matter + * right now, but someday we might optimize diff-index --cached + * with cache-tree information. + */ + cache_tree_invalidate_path(active_cache_tree, path); + + commit->buffer = xmalloc(400); + ident = fmt_ident("Not Committed Yet", "not.committed.yet", NULL, 0); + sprintf(commit->buffer, + "tree 0000000000000000000000000000000000000000\n" + "parent %s\n" + "author %s\n" + "committer %s\n\n" + "Version of %s from %s\n", + sha1_to_hex(head_sha1), + ident, ident, path, contents_from ? contents_from : path); + return commit; +} + +int cmd_blame(int argc, const char **argv, const char *prefix) +{ + struct rev_info revs; + const char *path; + struct scoreboard sb; + struct origin *o; + struct blame_entry *ent; + int i, seen_dashdash, unk, opt; + long bottom, top, lno; + int output_option = 0; + const char *revs_file = NULL; + const char *final_commit_name = NULL; + char type[10]; + const char *bottomtop = NULL; + const char *contents_from = NULL; + + cmd_is_annotate = !strcmp(argv[0], "annotate"); + + git_config(git_blame_config); + save_commit_buffer = 0; + + opt = 0; + seen_dashdash = 0; + for (unk = i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (*arg != '-') + break; + else if (!strcmp("-b", arg)) + blank_boundary = 1; + else if (!strcmp("--root", arg)) + show_root = 1; + else if (!strcmp("-c", arg)) + output_option |= OUTPUT_ANNOTATE_COMPAT; + else if (!strcmp("-t", arg)) + output_option |= OUTPUT_RAW_TIMESTAMP; + else if (!strcmp("-l", arg)) + output_option |= OUTPUT_LONG_OBJECT_NAME; + else if (!strcmp("-S", arg) && ++i < argc) + revs_file = argv[i]; + else if (!strncmp("-M", arg, 2)) { + opt |= PICKAXE_BLAME_MOVE; + blame_move_score = parse_score(arg+2); + } + else if (!strncmp("-C", arg, 2)) { + if (opt & PICKAXE_BLAME_COPY) + opt |= PICKAXE_BLAME_COPY_HARDER; + opt |= PICKAXE_BLAME_COPY | PICKAXE_BLAME_MOVE; + blame_copy_score = parse_score(arg+2); + } + else if (!strncmp("-L", arg, 2)) { + if (!arg[2]) { + if (++i >= argc) + usage(blame_usage); + arg = argv[i]; + } + else + arg += 2; + if (bottomtop) + die("More than one '-L n,m' option given"); + bottomtop = arg; + } + else if (!strcmp("--contents", arg)) { + if (++i >= argc) + usage(blame_usage); + contents_from = argv[i]; + } + else if (!strcmp("--incremental", arg)) + incremental = 1; + else if (!strcmp("--score-debug", arg)) + output_option |= OUTPUT_SHOW_SCORE; + else if (!strcmp("-f", arg) || + !strcmp("--show-name", arg)) + output_option |= OUTPUT_SHOW_NAME; + else if (!strcmp("-n", arg) || + !strcmp("--show-number", arg)) + output_option |= OUTPUT_SHOW_NUMBER; + else if (!strcmp("-p", arg) || + !strcmp("--porcelain", arg)) + output_option |= OUTPUT_PORCELAIN; + else if (!strcmp("--", arg)) { + seen_dashdash = 1; + i++; + break; + } + else + argv[unk++] = arg; + } + + if (!incremental) + setup_pager(); + + if (!blame_move_score) + blame_move_score = BLAME_DEFAULT_MOVE_SCORE; + if (!blame_copy_score) + blame_copy_score = BLAME_DEFAULT_COPY_SCORE; + + /* + * We have collected options unknown to us in argv[1..unk] + * which are to be passed to revision machinery if we are + * going to do the "bottom" processing. + * + * The remaining are: + * + * (1) if seen_dashdash, its either + * "-options -- <path>" or + * "-options -- <path> <rev>". + * but the latter is allowed only if there is no + * options that we passed to revision machinery. + * + * (2) otherwise, we may have "--" somewhere later and + * might be looking at the first one of multiple 'rev' + * parameters (e.g. " master ^next ^maint -- path"). + * See if there is a dashdash first, and give the + * arguments before that to revision machinery. + * After that there must be one 'path'. + * + * (3) otherwise, its one of the three: + * "-options <path> <rev>" + * "-options <rev> <path>" + * "-options <path>" + * but again the first one is allowed only if + * there is no options that we passed to revision + * machinery. + */ + + if (seen_dashdash) { + /* (1) */ + if (argc <= i) + usage(blame_usage); + path = add_prefix(prefix, argv[i]); + if (i + 1 == argc - 1) { + if (unk != 1) + usage(blame_usage); + argv[unk++] = argv[i + 1]; + } + else if (i + 1 != argc) + /* garbage at end */ + usage(blame_usage); + } + else { + int j; + for (j = i; !seen_dashdash && j < argc; j++) + if (!strcmp(argv[j], "--")) + seen_dashdash = j; + if (seen_dashdash) { + if (seen_dashdash + 1 != argc - 1) + usage(blame_usage); + path = add_prefix(prefix, argv[seen_dashdash + 1]); + for (j = i; j < seen_dashdash; j++) + argv[unk++] = argv[j]; + } + else { + /* (3) */ + path = add_prefix(prefix, argv[i]); + if (i + 1 == argc - 1) { + final_commit_name = argv[i + 1]; + + /* if (unk == 1) we could be getting + * old-style + */ + if (unk == 1 && !has_path_in_work_tree(path)) { + path = add_prefix(prefix, argv[i + 1]); + final_commit_name = argv[i]; + } + } + else if (i != argc - 1) + usage(blame_usage); /* garbage at end */ + + if (!has_path_in_work_tree(path)) + die("cannot stat path %s: %s", + path, strerror(errno)); + } + } + + if (final_commit_name) + argv[unk++] = final_commit_name; + + /* + * Now we got rev and path. We do not want the path pruning + * but we may want "bottom" processing. + */ + argv[unk++] = "--"; /* terminate the rev name */ + argv[unk] = NULL; + + init_revisions(&revs, NULL); + setup_revisions(unk, argv, &revs, NULL); + memset(&sb, 0, sizeof(sb)); + + /* + * There must be one and only one positive commit in the + * revs->pending array. + */ + for (i = 0; i < revs.pending.nr; i++) { + struct object *obj = revs.pending.objects[i].item; + if (obj->flags & UNINTERESTING) + continue; + while (obj->type == OBJ_TAG) + obj = deref_tag(obj, NULL, 0); + if (obj->type != OBJ_COMMIT) + die("Non commit %s?", + revs.pending.objects[i].name); + if (sb.final) + die("More than one commit to dig from %s and %s?", + revs.pending.objects[i].name, + final_commit_name); + sb.final = (struct commit *) obj; + final_commit_name = revs.pending.objects[i].name; + } + + if (!sb.final) { + /* + * "--not A B -- path" without anything positive; + * do not default to HEAD, but use the working tree + * or "--contents". + */ + sb.final = fake_working_tree_commit(path, contents_from); + add_pending_object(&revs, &(sb.final->object), ":"); + } + else if (contents_from) + die("Cannot use --contents with final commit object name"); + + /* + * If we have bottom, this will mark the ancestors of the + * bottom commits we would reach while traversing as + * uninteresting. + */ + prepare_revision_walk(&revs); + + if (is_null_sha1(sb.final->object.sha1)) { + char *buf; + o = sb.final->util; + buf = xmalloc(o->file.size + 1); + memcpy(buf, o->file.ptr, o->file.size + 1); + sb.final_buf = buf; + sb.final_buf_size = o->file.size; + } + else { + o = get_origin(&sb, sb.final, path); + if (fill_blob_sha1(o)) + die("no such path %s in %s", path, final_commit_name); + + sb.final_buf = read_sha1_file(o->blob_sha1, type, + &sb.final_buf_size); + } + num_read_blob++; + lno = prepare_lines(&sb); + + bottom = top = 0; + if (bottomtop) + prepare_blame_range(&sb, bottomtop, lno, &bottom, &top); + if (bottom && top && top < bottom) { + long tmp; + tmp = top; top = bottom; bottom = tmp; + } + if (bottom < 1) + bottom = 1; + if (top < 1) + top = lno; + bottom--; + if (lno < top) + die("file %s has only %lu lines", path, lno); + + ent = xcalloc(1, sizeof(*ent)); + ent->lno = bottom; + ent->num_lines = top - bottom; + ent->suspect = o; + ent->s_lno = bottom; + + sb.ent = ent; + sb.path = path; + + if (revs_file && read_ancestry(revs_file)) + die("reading graft file %s failed: %s", + revs_file, strerror(errno)); + + assign_blame(&sb, &revs, opt); + + if (incremental) + return 0; + + coalesce(&sb); + + if (!(output_option & OUTPUT_PORCELAIN)) + find_alignment(&sb, &output_option); + + output(&sb, output_option); + free((void *)sb.final_buf); + for (ent = sb.ent; ent; ) { + struct blame_entry *e = ent->next; + free(ent); + ent = e; + } + + if (DEBUG) { + printf("num read blob: %d\n", num_read_blob); + printf("num get patch: %d\n", num_get_patch); + printf("num commits: %d\n", num_commits); + } + return 0; +} diff --git a/builtin-branch.c b/builtin-branch.c new file mode 100644 index 0000000000..2d8d61b453 --- /dev/null +++ b/builtin-branch.c @@ -0,0 +1,500 @@ +/* + * Builtin "git branch" + * + * Copyright (c) 2006 Kristian Høgsberg <krh@redhat.com> + * Based on git-branch.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "color.h" +#include "refs.h" +#include "commit.h" +#include "builtin.h" + +static const char builtin_branch_usage[] = + "git-branch [-r] (-d | -D) <branchname> | [-l] [-f] <branchname> [<start-point>] | (-m | -M) [<oldbranch>] <newbranch> | [--color | --no-color] [-r | -a] [-v [--abbrev=<length>]]"; + +#define REF_UNKNOWN_TYPE 0x00 +#define REF_LOCAL_BRANCH 0x01 +#define REF_REMOTE_BRANCH 0x02 +#define REF_TAG 0x04 + +static const char *head; +static unsigned char head_sha1[20]; + +static int branch_use_color; +static char branch_colors[][COLOR_MAXLEN] = { + "\033[m", /* reset */ + "", /* PLAIN (normal) */ + "\033[31m", /* REMOTE (red) */ + "", /* LOCAL (normal) */ + "\033[32m", /* CURRENT (green) */ +}; +enum color_branch { + COLOR_BRANCH_RESET = 0, + COLOR_BRANCH_PLAIN = 1, + COLOR_BRANCH_REMOTE = 2, + COLOR_BRANCH_LOCAL = 3, + COLOR_BRANCH_CURRENT = 4, +}; + +static int parse_branch_color_slot(const char *var, int ofs) +{ + if (!strcasecmp(var+ofs, "plain")) + return COLOR_BRANCH_PLAIN; + if (!strcasecmp(var+ofs, "reset")) + return COLOR_BRANCH_RESET; + if (!strcasecmp(var+ofs, "remote")) + return COLOR_BRANCH_REMOTE; + if (!strcasecmp(var+ofs, "local")) + return COLOR_BRANCH_LOCAL; + if (!strcasecmp(var+ofs, "current")) + return COLOR_BRANCH_CURRENT; + die("bad config variable '%s'", var); +} + +int git_branch_config(const char *var, const char *value) +{ + if (!strcmp(var, "color.branch")) { + branch_use_color = git_config_colorbool(var, value); + return 0; + } + if (!strncmp(var, "color.branch.", 13)) { + int slot = parse_branch_color_slot(var, 13); + color_parse(value, var, branch_colors[slot]); + return 0; + } + return git_default_config(var, value); +} + +const char *branch_get_color(enum color_branch ix) +{ + if (branch_use_color) + return branch_colors[ix]; + return ""; +} + +static int delete_branches(int argc, const char **argv, int force, int kinds) +{ + struct commit *rev, *head_rev = head_rev; + unsigned char sha1[20]; + char *name = NULL; + const char *fmt, *remote; + int i; + int ret = 0; + + switch (kinds) { + case REF_REMOTE_BRANCH: + fmt = "refs/remotes/%s"; + remote = "remote "; + force = 1; + break; + case REF_LOCAL_BRANCH: + fmt = "refs/heads/%s"; + remote = ""; + break; + default: + die("cannot use -a with -d"); + } + + if (!force) { + head_rev = lookup_commit_reference(head_sha1); + if (!head_rev) + die("Couldn't look up commit object for HEAD"); + } + for (i = 0; i < argc; i++) { + if (kinds == REF_LOCAL_BRANCH && !strcmp(head, argv[i])) { + error("Cannot delete the branch '%s' " + "which you are currently on.", argv[i]); + ret = 1; + continue; + } + + if (name) + free(name); + + name = xstrdup(mkpath(fmt, argv[i])); + if (!resolve_ref(name, sha1, 1, NULL)) { + error("%sbranch '%s' not found.", + remote, argv[i]); + ret = 1; + continue; + } + + rev = lookup_commit_reference(sha1); + if (!rev) { + error("Couldn't look up commit object for '%s'", name); + ret = 1; + continue; + } + + /* This checks whether the merge bases of branch and + * HEAD contains branch -- which means that the HEAD + * contains everything in both. + */ + + if (!force && + !in_merge_bases(rev, head_rev)) { + error("The branch '%s' is not a strict subset of " + "your current HEAD.\n" + "If you are sure you want to delete it, " + "run 'git branch -D %s'.", argv[i], argv[i]); + ret = 1; + continue; + } + + if (delete_ref(name, sha1)) { + error("Error deleting %sbranch '%s'", remote, + argv[i]); + ret = 1; + } else + printf("Deleted %sbranch %s.\n", remote, argv[i]); + + } + + if (name) + free(name); + + return(ret); +} + +struct ref_item { + char *name; + unsigned int kind; + unsigned char sha1[20]; +}; + +struct ref_list { + int index, alloc, maxwidth; + struct ref_item *list; + int kinds; +}; + +static int append_ref(const char *refname, const unsigned char *sha1, int flags, void *cb_data) +{ + struct ref_list *ref_list = (struct ref_list*)(cb_data); + struct ref_item *newitem; + int kind = REF_UNKNOWN_TYPE; + int len; + + /* Detect kind */ + if (!strncmp(refname, "refs/heads/", 11)) { + kind = REF_LOCAL_BRANCH; + refname += 11; + } else if (!strncmp(refname, "refs/remotes/", 13)) { + kind = REF_REMOTE_BRANCH; + refname += 13; + } else if (!strncmp(refname, "refs/tags/", 10)) { + kind = REF_TAG; + refname += 10; + } + + /* Don't add types the caller doesn't want */ + if ((kind & ref_list->kinds) == 0) + return 0; + + /* Resize buffer */ + if (ref_list->index >= ref_list->alloc) { + ref_list->alloc = alloc_nr(ref_list->alloc); + ref_list->list = xrealloc(ref_list->list, + ref_list->alloc * sizeof(struct ref_item)); + } + + /* Record the new item */ + newitem = &(ref_list->list[ref_list->index++]); + newitem->name = xstrdup(refname); + newitem->kind = kind; + hashcpy(newitem->sha1, sha1); + len = strlen(newitem->name); + if (len > ref_list->maxwidth) + ref_list->maxwidth = len; + + return 0; +} + +static void free_ref_list(struct ref_list *ref_list) +{ + int i; + + for (i = 0; i < ref_list->index; i++) + free(ref_list->list[i].name); + free(ref_list->list); +} + +static int ref_cmp(const void *r1, const void *r2) +{ + struct ref_item *c1 = (struct ref_item *)(r1); + struct ref_item *c2 = (struct ref_item *)(r2); + + if (c1->kind != c2->kind) + return c1->kind - c2->kind; + return strcmp(c1->name, c2->name); +} + +static void print_ref_item(struct ref_item *item, int maxwidth, int verbose, + int abbrev, int current) +{ + char c; + int color; + struct commit *commit; + char subject[256]; + + switch (item->kind) { + case REF_LOCAL_BRANCH: + color = COLOR_BRANCH_LOCAL; + break; + case REF_REMOTE_BRANCH: + color = COLOR_BRANCH_REMOTE; + break; + default: + color = COLOR_BRANCH_PLAIN; + break; + } + + c = ' '; + if (current) { + c = '*'; + color = COLOR_BRANCH_CURRENT; + } + + if (verbose) { + commit = lookup_commit(item->sha1); + if (commit && !parse_commit(commit)) + pretty_print_commit(CMIT_FMT_ONELINE, commit, ~0, + subject, sizeof(subject), 0, + NULL, NULL, 0); + else + strcpy(subject, " **** invalid ref ****"); + printf("%c %s%-*s%s %s %s\n", c, branch_get_color(color), + maxwidth, item->name, + branch_get_color(COLOR_BRANCH_RESET), + find_unique_abbrev(item->sha1, abbrev), subject); + } else { + printf("%c %s%s%s\n", c, branch_get_color(color), item->name, + branch_get_color(COLOR_BRANCH_RESET)); + } +} + +static void print_ref_list(int kinds, int detached, int verbose, int abbrev) +{ + int i; + struct ref_list ref_list; + + memset(&ref_list, 0, sizeof(ref_list)); + ref_list.kinds = kinds; + for_each_ref(append_ref, &ref_list); + + qsort(ref_list.list, ref_list.index, sizeof(struct ref_item), ref_cmp); + + detached = (detached && (kinds & REF_LOCAL_BRANCH)); + if (detached) { + struct ref_item item; + item.name = "(no branch)"; + item.kind = REF_LOCAL_BRANCH; + hashcpy(item.sha1, head_sha1); + if (strlen(item.name) > ref_list.maxwidth) + ref_list.maxwidth = strlen(item.name); + print_ref_item(&item, ref_list.maxwidth, verbose, abbrev, 1); + } + + for (i = 0; i < ref_list.index; i++) { + int current = !detached && + (ref_list.list[i].kind == REF_LOCAL_BRANCH) && + !strcmp(ref_list.list[i].name, head); + print_ref_item(&ref_list.list[i], ref_list.maxwidth, verbose, + abbrev, current); + } + + free_ref_list(&ref_list); +} + +static void create_branch(const char *name, const char *start_name, + unsigned char *start_sha1, + int force, int reflog) +{ + struct ref_lock *lock; + struct commit *commit; + unsigned char sha1[20]; + char ref[PATH_MAX], msg[PATH_MAX + 20]; + int forcing = 0; + + snprintf(ref, sizeof ref, "refs/heads/%s", name); + if (check_ref_format(ref)) + die("'%s' is not a valid branch name.", name); + + if (resolve_ref(ref, sha1, 1, NULL)) { + if (!force) + die("A branch named '%s' already exists.", name); + else if (!is_bare_repository() && !strcmp(head, name)) + die("Cannot force update the current branch."); + forcing = 1; + } + + if (start_sha1) + /* detached HEAD */ + hashcpy(sha1, start_sha1); + else if (get_sha1(start_name, sha1)) + die("Not a valid object name: '%s'.", start_name); + + if ((commit = lookup_commit_reference(sha1)) == NULL) + die("Not a valid branch point: '%s'.", start_name); + hashcpy(sha1, commit->object.sha1); + + lock = lock_any_ref_for_update(ref, NULL); + if (!lock) + die("Failed to lock ref for update: %s.", strerror(errno)); + + if (reflog) + log_all_ref_updates = 1; + + if (forcing) + snprintf(msg, sizeof msg, "branch: Reset from %s", + start_name); + else + snprintf(msg, sizeof msg, "branch: Created from %s", + start_name); + + if (write_ref_sha1(lock, sha1, msg) < 0) + die("Failed to write ref: %s.", strerror(errno)); +} + +static void rename_branch(const char *oldname, const char *newname, int force) +{ + char oldref[PATH_MAX], newref[PATH_MAX], logmsg[PATH_MAX*2 + 100]; + unsigned char sha1[20]; + + if (!oldname) + die("cannot rename the current branch while not on any."); + + if (snprintf(oldref, sizeof(oldref), "refs/heads/%s", oldname) > sizeof(oldref)) + die("Old branchname too long"); + + if (check_ref_format(oldref)) + die("Invalid branch name: %s", oldref); + + if (snprintf(newref, sizeof(newref), "refs/heads/%s", newname) > sizeof(newref)) + die("New branchname too long"); + + if (check_ref_format(newref)) + die("Invalid branch name: %s", newref); + + if (resolve_ref(newref, sha1, 1, NULL) && !force) + die("A branch named '%s' already exists.", newname); + + snprintf(logmsg, sizeof(logmsg), "Branch: renamed %s to %s", + oldref, newref); + + if (rename_ref(oldref, newref, logmsg)) + die("Branch rename failed"); + + /* no need to pass logmsg here as HEAD didn't really move */ + if (!strcmp(oldname, head) && create_symref("HEAD", newref, NULL)) + die("Branch renamed to %s, but HEAD is not updated!", newname); +} + +int cmd_branch(int argc, const char **argv, const char *prefix) +{ + int delete = 0, force_delete = 0, force_create = 0; + int rename = 0, force_rename = 0; + int verbose = 0, abbrev = DEFAULT_ABBREV, detached = 0; + int reflog = 0; + int kinds = REF_LOCAL_BRANCH; + int i; + + git_config(git_branch_config); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (arg[0] != '-') + break; + if (!strcmp(arg, "--")) { + i++; + break; + } + if (!strcmp(arg, "-d")) { + delete = 1; + continue; + } + if (!strcmp(arg, "-D")) { + delete = 1; + force_delete = 1; + continue; + } + if (!strcmp(arg, "-f")) { + force_create = 1; + continue; + } + if (!strcmp(arg, "-m")) { + rename = 1; + continue; + } + if (!strcmp(arg, "-M")) { + rename = 1; + force_rename = 1; + continue; + } + if (!strcmp(arg, "-r")) { + kinds = REF_REMOTE_BRANCH; + continue; + } + if (!strcmp(arg, "-a")) { + kinds = REF_REMOTE_BRANCH | REF_LOCAL_BRANCH; + continue; + } + if (!strcmp(arg, "-l")) { + reflog = 1; + continue; + } + if (!strncmp(arg, "--abbrev=", 9)) { + abbrev = atoi(arg+9); + continue; + } + if (!strcmp(arg, "-v")) { + verbose = 1; + continue; + } + if (!strcmp(arg, "--color")) { + branch_use_color = 1; + continue; + } + if (!strcmp(arg, "--no-color")) { + branch_use_color = 0; + continue; + } + usage(builtin_branch_usage); + } + + if ((delete && rename) || (delete && force_create) || + (rename && force_create)) + usage(builtin_branch_usage); + + head = xstrdup(resolve_ref("HEAD", head_sha1, 0, NULL)); + if (!head) + die("Failed to resolve HEAD as a valid ref."); + if (!strcmp(head, "HEAD")) { + detached = 1; + } + else { + if (strncmp(head, "refs/heads/", 11)) + die("HEAD not found below refs/heads!"); + head += 11; + } + + if (delete) + return delete_branches(argc - i, argv + i, force_delete, kinds); + else if (i == argc) + print_ref_list(kinds, detached, verbose, abbrev); + else if (rename && (i == argc - 1)) + rename_branch(head, argv[i], force_rename); + else if (rename && (i == argc - 2)) + rename_branch(argv[i], argv[i + 1], force_rename); + else if (i == argc - 1) + create_branch(argv[i], head, head_sha1, force_create, reflog); + else if (i == argc - 2) + create_branch(argv[i], argv[i+1], NULL, force_create, reflog); + else + usage(builtin_branch_usage); + + return 0; +} diff --git a/builtin-cat-file.c b/builtin-cat-file.c new file mode 100644 index 0000000000..6c16bfa1ae --- /dev/null +++ b/builtin-cat-file.c @@ -0,0 +1,150 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "exec_cmd.h" +#include "tag.h" +#include "tree.h" +#include "builtin.h" + +static void pprint_tag(const unsigned char *sha1, const char *buf, unsigned long size) +{ + /* the parser in tag.c is useless here. */ + const char *endp = buf + size; + const char *cp = buf; + + while (cp < endp) { + char c = *cp++; + if (c != '\n') + continue; + if (7 <= endp - cp && !memcmp("tagger ", cp, 7)) { + const char *tagger = cp; + + /* Found the tagger line. Copy out the contents + * of the buffer so far. + */ + write_or_die(1, buf, cp - buf); + + /* + * Do something intelligent, like pretty-printing + * the date. + */ + while (cp < endp) { + if (*cp++ == '\n') { + /* tagger to cp is a line + * that has ident and time. + */ + const char *sp = tagger; + char *ep; + unsigned long date; + long tz; + while (sp < cp && *sp != '>') + sp++; + if (sp == cp) { + /* give up */ + write_or_die(1, tagger, + cp - tagger); + break; + } + while (sp < cp && + !('0' <= *sp && *sp <= '9')) + sp++; + write_or_die(1, tagger, sp - tagger); + date = strtoul(sp, &ep, 10); + tz = strtol(ep, NULL, 10); + sp = show_date(date, tz, 0); + write_or_die(1, sp, strlen(sp)); + xwrite(1, "\n", 1); + break; + } + } + break; + } + if (cp < endp && *cp == '\n') + /* end of header */ + break; + } + /* At this point, we have copied out the header up to the end of + * the tagger line and cp points at one past \n. It could be the + * next header line after the tagger line, or it could be another + * \n that marks the end of the headers. We need to copy out the + * remainder as is. + */ + if (cp < endp) + write_or_die(1, cp, endp - cp); +} + +int cmd_cat_file(int argc, const char **argv, const char *prefix) +{ + unsigned char sha1[20]; + char type[20]; + void *buf; + unsigned long size; + int opt; + + git_config(git_default_config); + if (argc != 3) + usage("git-cat-file [-t|-s|-e|-p|<type>] <sha1>"); + if (get_sha1(argv[2], sha1)) + die("Not a valid object name %s", argv[2]); + + opt = 0; + if ( argv[1][0] == '-' ) { + opt = argv[1][1]; + if ( !opt || argv[1][2] ) + opt = -1; /* Not a single character option */ + } + + buf = NULL; + switch (opt) { + case 't': + if (!sha1_object_info(sha1, type, NULL)) { + printf("%s\n", type); + return 0; + } + break; + + case 's': + if (!sha1_object_info(sha1, type, &size)) { + printf("%lu\n", size); + return 0; + } + break; + + case 'e': + return !has_sha1_file(sha1); + + case 'p': + if (sha1_object_info(sha1, type, NULL)) + die("Not a valid object name %s", argv[2]); + + /* custom pretty-print here */ + if (!strcmp(type, tree_type)) + return cmd_ls_tree(2, argv + 1, NULL); + + buf = read_sha1_file(sha1, type, &size); + if (!buf) + die("Cannot read object %s", argv[2]); + if (!strcmp(type, tag_type)) { + pprint_tag(sha1, buf, size); + return 0; + } + + /* otherwise just spit out the data */ + break; + case 0: + buf = read_object_with_reference(sha1, argv[1], &size, NULL); + break; + + default: + die("git-cat-file: unknown option: %s\n", argv[1]); + } + + if (!buf) + die("git-cat-file %s: bad file", argv[2]); + + write_or_die(1, buf, size); + return 0; +} diff --git a/builtin-check-ref-format.c b/builtin-check-ref-format.c new file mode 100644 index 0000000000..fe04be77a9 --- /dev/null +++ b/builtin-check-ref-format.c @@ -0,0 +1,14 @@ +/* + * GIT - The information manager from hell + */ + +#include "cache.h" +#include "refs.h" +#include "builtin.h" + +int cmd_check_ref_format(int argc, const char **argv, const char *prefix) +{ + if (argc != 2) + usage("git-check-ref-format refname"); + return !!check_ref_format(argv[1]); +} diff --git a/builtin-checkout-index.c b/builtin-checkout-index.c new file mode 100644 index 0000000000..b097c888a0 --- /dev/null +++ b/builtin-checkout-index.c @@ -0,0 +1,308 @@ +/* + * Check-out files from the "current cache directory" + * + * Copyright (C) 2005 Linus Torvalds + * + * Careful: order of argument flags does matter. For example, + * + * git-checkout-index -a -f file.c + * + * Will first check out all files listed in the cache (but not + * overwrite any old ones), and then force-checkout "file.c" a + * second time (ie that one _will_ overwrite any old contents + * with the same filename). + * + * Also, just doing "git-checkout-index" does nothing. You probably + * meant "git-checkout-index -a". And if you want to force it, you + * want "git-checkout-index -f -a". + * + * Intuitiveness is not the goal here. Repeatability is. The + * reason for the "no arguments means no work" thing is that + * from scripts you are supposed to be able to do things like + * + * find . -name '*.h' -print0 | xargs -0 git-checkout-index -f -- + * + * or: + * + * find . -name '*.h' -print0 | git-checkout-index -f -z --stdin + * + * which will force all existing *.h files to be replaced with + * their cached copies. If an empty command line implied "all", + * then this would force-refresh everything in the cache, which + * was not the point. + * + * Oh, and the "--" is just a good idea when you know the rest + * will be filenames. Just so that you wouldn't have a filename + * of "-a" causing problems (not possible in the above example, + * but get used to it in scripting!). + */ +#include "cache.h" +#include "strbuf.h" +#include "quote.h" +#include "cache-tree.h" + +#define CHECKOUT_ALL 4 +static int line_termination = '\n'; +static int checkout_stage; /* default to checkout stage0 */ +static int to_tempfile; +static char topath[4][PATH_MAX + 1]; + +static struct checkout state; + +static void write_tempfile_record(const char *name, int prefix_length) +{ + int i; + + if (CHECKOUT_ALL == checkout_stage) { + for (i = 1; i < 4; i++) { + if (i > 1) + putchar(' '); + if (topath[i][0]) + fputs(topath[i], stdout); + else + putchar('.'); + } + } else + fputs(topath[checkout_stage], stdout); + + putchar('\t'); + write_name_quoted("", 0, name + prefix_length, + line_termination, stdout); + putchar(line_termination); + + for (i = 0; i < 4; i++) { + topath[i][0] = 0; + } +} + +static int checkout_file(const char *name, int prefix_length) +{ + int namelen = strlen(name); + int pos = cache_name_pos(name, namelen); + int has_same_name = 0; + int did_checkout = 0; + int errs = 0; + + if (pos < 0) + pos = -pos - 1; + + while (pos < active_nr) { + struct cache_entry *ce = active_cache[pos]; + if (ce_namelen(ce) != namelen || + memcmp(ce->name, name, namelen)) + break; + has_same_name = 1; + pos++; + if (ce_stage(ce) != checkout_stage + && (CHECKOUT_ALL != checkout_stage || !ce_stage(ce))) + continue; + did_checkout = 1; + if (checkout_entry(ce, &state, + to_tempfile ? topath[ce_stage(ce)] : NULL) < 0) + errs++; + } + + if (did_checkout) { + if (to_tempfile) + write_tempfile_record(name, prefix_length); + return errs > 0 ? -1 : 0; + } + + if (!state.quiet) { + fprintf(stderr, "git-checkout-index: %s ", name); + if (!has_same_name) + fprintf(stderr, "is not in the cache"); + else if (checkout_stage) + fprintf(stderr, "does not exist at stage %d", + checkout_stage); + else + fprintf(stderr, "is unmerged"); + fputc('\n', stderr); + } + return -1; +} + +static void checkout_all(const char *prefix, int prefix_length) +{ + int i, errs = 0; + struct cache_entry* last_ce = NULL; + + for (i = 0; i < active_nr ; i++) { + struct cache_entry *ce = active_cache[i]; + if (ce_stage(ce) != checkout_stage + && (CHECKOUT_ALL != checkout_stage || !ce_stage(ce))) + continue; + if (prefix && *prefix && + (ce_namelen(ce) <= prefix_length || + memcmp(prefix, ce->name, prefix_length))) + continue; + if (last_ce && to_tempfile) { + if (ce_namelen(last_ce) != ce_namelen(ce) + || memcmp(last_ce->name, ce->name, ce_namelen(ce))) + write_tempfile_record(last_ce->name, prefix_length); + } + if (checkout_entry(ce, &state, + to_tempfile ? topath[ce_stage(ce)] : NULL) < 0) + errs++; + last_ce = ce; + } + if (last_ce && to_tempfile) + write_tempfile_record(last_ce->name, prefix_length); + if (errs) + /* we have already done our error reporting. + * exit with the same code as die(). + */ + exit(128); +} + +static const char checkout_cache_usage[] = +"git-checkout-index [-u] [-q] [-a] [-f] [-n] [--stage=[123]|all] [--prefix=<string>] [--temp] [--] <file>..."; + +static struct lock_file lock_file; + +int cmd_checkout_index(int argc, const char **argv, const char *prefix) +{ + int i; + int newfd = -1; + int all = 0; + int read_from_stdin = 0; + int prefix_length; + + git_config(git_default_config); + state.base_dir = ""; + prefix_length = prefix ? strlen(prefix) : 0; + + if (read_cache() < 0) { + die("invalid cache"); + } + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (!strcmp(arg, "--")) { + i++; + break; + } + if (!strcmp(arg, "-a") || !strcmp(arg, "--all")) { + all = 1; + continue; + } + if (!strcmp(arg, "-f") || !strcmp(arg, "--force")) { + state.force = 1; + continue; + } + if (!strcmp(arg, "-q") || !strcmp(arg, "--quiet")) { + state.quiet = 1; + continue; + } + if (!strcmp(arg, "-n") || !strcmp(arg, "--no-create")) { + state.not_new = 1; + continue; + } + if (!strcmp(arg, "-u") || !strcmp(arg, "--index")) { + state.refresh_cache = 1; + if (newfd < 0) + newfd = hold_lock_file_for_update + (&lock_file, get_index_file(), 1); + if (newfd < 0) + die("cannot open index.lock file."); + continue; + } + if (!strcmp(arg, "-z")) { + line_termination = 0; + continue; + } + if (!strcmp(arg, "--stdin")) { + if (i != argc - 1) + die("--stdin must be at the end"); + read_from_stdin = 1; + i++; /* do not consider arg as a file name */ + break; + } + if (!strcmp(arg, "--temp")) { + to_tempfile = 1; + continue; + } + if (!strncmp(arg, "--prefix=", 9)) { + state.base_dir = arg+9; + state.base_dir_len = strlen(state.base_dir); + continue; + } + if (!strncmp(arg, "--stage=", 8)) { + if (!strcmp(arg + 8, "all")) { + to_tempfile = 1; + checkout_stage = CHECKOUT_ALL; + } else { + int ch = arg[8]; + if ('1' <= ch && ch <= '3') + checkout_stage = arg[8] - '0'; + else + die("stage should be between 1 and 3 or all"); + } + continue; + } + if (arg[0] == '-') + usage(checkout_cache_usage); + break; + } + + if (state.base_dir_len || to_tempfile) { + /* when --prefix is specified we do not + * want to update cache. + */ + if (state.refresh_cache) { + close(newfd); newfd = -1; + rollback_lock_file(&lock_file); + } + state.refresh_cache = 0; + } + + /* Check out named files first */ + for ( ; i < argc; i++) { + const char *arg = argv[i]; + const char *p; + + if (all) + die("git-checkout-index: don't mix '--all' and explicit filenames"); + if (read_from_stdin) + die("git-checkout-index: don't mix '--stdin' and explicit filenames"); + p = prefix_path(prefix, prefix_length, arg); + checkout_file(p, prefix_length); + if (p < arg || p > arg + strlen(arg)) + free((char*)p); + } + + if (read_from_stdin) { + struct strbuf buf; + if (all) + die("git-checkout-index: don't mix '--all' and '--stdin'"); + strbuf_init(&buf); + while (1) { + char *path_name; + const char *p; + + read_line(&buf, stdin, line_termination); + if (buf.eof) + break; + if (line_termination && buf.buf[0] == '"') + path_name = unquote_c_style(buf.buf, NULL); + else + path_name = buf.buf; + p = prefix_path(prefix, prefix_length, path_name); + checkout_file(p, prefix_length); + if (p < path_name || p > path_name + strlen(path_name)) + free((char *)p); + if (path_name != buf.buf) + free(path_name); + } + } + + if (all) + checkout_all(prefix, prefix_length); + + if (0 <= newfd && + (write_cache(newfd, active_cache, active_nr) || + close(newfd) || commit_lock_file(&lock_file))) + die("Unable to write new index file"); + return 0; +} diff --git a/builtin-commit-tree.c b/builtin-commit-tree.c new file mode 100644 index 0000000000..2a818a0a2c --- /dev/null +++ b/builtin-commit-tree.c @@ -0,0 +1,157 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "commit.h" +#include "tree.h" +#include "builtin.h" +#include "utf8.h" + +#define BLOCKING (1ul << 14) + +/* + * FIXME! Share the code with "write-tree.c" + */ +static void init_buffer(char **bufp, unsigned int *sizep) +{ + char *buf = xmalloc(BLOCKING); + *sizep = 0; + *bufp = buf; +} + +static void add_buffer(char **bufp, unsigned int *sizep, const char *fmt, ...) +{ + char one_line[2048]; + va_list args; + int len; + unsigned long alloc, size, newsize; + char *buf; + + va_start(args, fmt); + len = vsnprintf(one_line, sizeof(one_line), fmt, args); + va_end(args); + size = *sizep; + newsize = size + len + 1; + alloc = (size + 32767) & ~32767; + buf = *bufp; + if (newsize > alloc) { + alloc = (newsize + 32767) & ~32767; + buf = xrealloc(buf, alloc); + *bufp = buf; + } + *sizep = newsize - 1; + memcpy(buf + size, one_line, len); +} + +static void check_valid(unsigned char *sha1, const char *expect) +{ + char type[20]; + + if (sha1_object_info(sha1, type, NULL)) + die("%s is not a valid object", sha1_to_hex(sha1)); + if (expect && strcmp(type, expect)) + die("%s is not a valid '%s' object", sha1_to_hex(sha1), + expect); +} + +/* + * Having more than two parents is not strange at all, and this is + * how multi-way merges are represented. + */ +#define MAXPARENT (16) +static unsigned char parent_sha1[MAXPARENT][20]; + +static const char commit_tree_usage[] = "git-commit-tree <sha1> [-p <sha1>]* < changelog"; + +static int new_parent(int idx) +{ + int i; + unsigned char *sha1 = parent_sha1[idx]; + for (i = 0; i < idx; i++) { + if (!hashcmp(parent_sha1[i], sha1)) { + error("duplicate parent %s ignored", sha1_to_hex(sha1)); + return 0; + } + } + return 1; +} + +static const char commit_utf8_warn[] = +"Warning: commit message does not conform to UTF-8.\n" +"You may want to amend it after fixing the message, or set the config\n" +"variable i18n.commitencoding to the encoding your project uses.\n"; + +int cmd_commit_tree(int argc, const char **argv, const char *prefix) +{ + int i; + int parents = 0; + unsigned char tree_sha1[20]; + unsigned char commit_sha1[20]; + char comment[1000]; + char *buffer; + unsigned int size; + int encoding_is_utf8; + + git_config(git_default_config); + + if (argc < 2) + usage(commit_tree_usage); + if (get_sha1(argv[1], tree_sha1)) + die("Not a valid object name %s", argv[1]); + + check_valid(tree_sha1, tree_type); + for (i = 2; i < argc; i += 2) { + const char *a, *b; + a = argv[i]; b = argv[i+1]; + if (!b || strcmp(a, "-p")) + usage(commit_tree_usage); + + if (parents >= MAXPARENT) + die("Too many parents (%d max)", MAXPARENT); + if (get_sha1(b, parent_sha1[parents])) + die("Not a valid object name %s", b); + check_valid(parent_sha1[parents], commit_type); + if (new_parent(parents)) + parents++; + } + + /* Not having i18n.commitencoding is the same as having utf-8 */ + encoding_is_utf8 = is_encoding_utf8(git_commit_encoding); + + init_buffer(&buffer, &size); + add_buffer(&buffer, &size, "tree %s\n", sha1_to_hex(tree_sha1)); + + /* + * NOTE! This ordering means that the same exact tree merged with a + * different order of parents will be a _different_ changeset even + * if everything else stays the same. + */ + for (i = 0; i < parents; i++) + add_buffer(&buffer, &size, "parent %s\n", sha1_to_hex(parent_sha1[i])); + + /* Person/date information */ + add_buffer(&buffer, &size, "author %s\n", git_author_info(1)); + add_buffer(&buffer, &size, "committer %s\n", git_committer_info(1)); + if (!encoding_is_utf8) + add_buffer(&buffer, &size, + "encoding %s\n", git_commit_encoding); + add_buffer(&buffer, &size, "\n"); + + /* And add the comment */ + while (fgets(comment, sizeof(comment), stdin) != NULL) + add_buffer(&buffer, &size, "%s", comment); + + /* And check the encoding */ + buffer[size] = '\0'; + if (encoding_is_utf8 && !is_utf8(buffer)) + fprintf(stderr, commit_utf8_warn); + + if (!write_sha1_file(buffer, size, commit_type, commit_sha1)) { + printf("%s\n", sha1_to_hex(commit_sha1)); + return 0; + } + else + return 1; +} diff --git a/builtin-config.c b/builtin-config.c new file mode 100644 index 0000000000..0f9051da17 --- /dev/null +++ b/builtin-config.c @@ -0,0 +1,220 @@ +#include "builtin.h" +#include "cache.h" + +static const char git_config_set_usage[] = +"git-config [ --global ] [ --bool | --int ] [--get | --get-all | --get-regexp | --replace-all | --add | --unset | --unset-all] name [value [value_regex]] | --rename-section old_name new_name | --list"; + +static char *key; +static regex_t *key_regexp; +static regex_t *regexp; +static int show_keys; +static int use_key_regexp; +static int do_all; +static int do_not_match; +static int seen; +static enum { T_RAW, T_INT, T_BOOL } type = T_RAW; + +static int show_all_config(const char *key_, const char *value_) +{ + if (value_) + printf("%s=%s\n", key_, value_); + else + printf("%s\n", key_); + return 0; +} + +static int show_config(const char* key_, const char* value_) +{ + char value[256]; + const char *vptr = value; + int dup_error = 0; + + if (!use_key_regexp && strcmp(key_, key)) + return 0; + if (use_key_regexp && regexec(key_regexp, key_, 0, NULL, 0)) + return 0; + if (regexp != NULL && + (do_not_match ^ + regexec(regexp, (value_?value_:""), 0, NULL, 0))) + return 0; + + if (show_keys) + printf("%s ", key_); + if (seen && !do_all) + dup_error = 1; + if (type == T_INT) + sprintf(value, "%d", git_config_int(key_, value_?value_:"")); + else if (type == T_BOOL) + vptr = git_config_bool(key_, value_) ? "true" : "false"; + else + vptr = value_?value_:""; + seen++; + if (dup_error) { + error("More than one value for the key %s: %s", + key_, vptr); + } + else + printf("%s\n", vptr); + + return 0; +} + +static int get_value(const char* key_, const char* regex_) +{ + int ret = -1; + char *tl; + char *global = NULL, *repo_config = NULL; + const char *local; + + local = getenv(CONFIG_ENVIRONMENT); + if (!local) { + const char *home = getenv("HOME"); + local = getenv(CONFIG_LOCAL_ENVIRONMENT); + if (!local) + local = repo_config = xstrdup(git_path("config")); + if (home) + global = xstrdup(mkpath("%s/.gitconfig", home)); + } + + key = xstrdup(key_); + for (tl=key+strlen(key)-1; tl >= key && *tl != '.'; --tl) + *tl = tolower(*tl); + for (tl=key; *tl && *tl != '.'; ++tl) + *tl = tolower(*tl); + + if (use_key_regexp) { + key_regexp = (regex_t*)xmalloc(sizeof(regex_t)); + if (regcomp(key_regexp, key, REG_EXTENDED)) { + fprintf(stderr, "Invalid key pattern: %s\n", key_); + goto free_strings; + } + } + + if (regex_) { + if (regex_[0] == '!') { + do_not_match = 1; + regex_++; + } + + regexp = (regex_t*)xmalloc(sizeof(regex_t)); + if (regcomp(regexp, regex_, REG_EXTENDED)) { + fprintf(stderr, "Invalid pattern: %s\n", regex_); + goto free_strings; + } + } + + if (do_all && global) + git_config_from_file(show_config, global); + git_config_from_file(show_config, local); + if (!do_all && !seen && global) + git_config_from_file(show_config, global); + + free(key); + if (regexp) { + regfree(regexp); + free(regexp); + } + + if (do_all) + ret = !seen; + else + ret = (seen == 1) ? 0 : seen > 1 ? 2 : 1; + +free_strings: + free(repo_config); + free(global); + return ret; +} + +int cmd_config(int argc, const char **argv, const char *prefix) +{ + int nongit = 0; + setup_git_directory_gently(&nongit); + + while (1 < argc) { + if (!strcmp(argv[1], "--int")) + type = T_INT; + else if (!strcmp(argv[1], "--bool")) + type = T_BOOL; + else if (!strcmp(argv[1], "--list") || !strcmp(argv[1], "-l")) + return git_config(show_all_config); + else if (!strcmp(argv[1], "--global")) { + char *home = getenv("HOME"); + if (home) { + char *user_config = xstrdup(mkpath("%s/.gitconfig", home)); + setenv("GIT_CONFIG", user_config, 1); + free(user_config); + } else { + die("$HOME not set"); + } + } else if (!strcmp(argv[1], "--rename-section")) { + int ret; + if (argc != 4) + usage(git_config_set_usage); + ret = git_config_rename_section(argv[2], argv[3]); + if (ret < 0) + return ret; + if (ret == 0) { + fprintf(stderr, "No such section!\n"); + return 1; + } + return 0; + } else + break; + argc--; + argv++; + } + + switch (argc) { + case 2: + return get_value(argv[1], NULL); + case 3: + if (!strcmp(argv[1], "--unset")) + return git_config_set(argv[2], NULL); + else if (!strcmp(argv[1], "--unset-all")) + return git_config_set_multivar(argv[2], NULL, NULL, 1); + else if (!strcmp(argv[1], "--get")) + return get_value(argv[2], NULL); + else if (!strcmp(argv[1], "--get-all")) { + do_all = 1; + return get_value(argv[2], NULL); + } else if (!strcmp(argv[1], "--get-regexp")) { + show_keys = 1; + use_key_regexp = 1; + do_all = 1; + return get_value(argv[2], NULL); + } else + + return git_config_set(argv[1], argv[2]); + case 4: + if (!strcmp(argv[1], "--unset")) + return git_config_set_multivar(argv[2], NULL, argv[3], 0); + else if (!strcmp(argv[1], "--unset-all")) + return git_config_set_multivar(argv[2], NULL, argv[3], 1); + else if (!strcmp(argv[1], "--get")) + return get_value(argv[2], argv[3]); + else if (!strcmp(argv[1], "--get-all")) { + do_all = 1; + return get_value(argv[2], argv[3]); + } else if (!strcmp(argv[1], "--get-regexp")) { + show_keys = 1; + use_key_regexp = 1; + do_all = 1; + return get_value(argv[2], argv[3]); + } else if (!strcmp(argv[1], "--add")) + return git_config_set_multivar(argv[2], argv[3], "^$", 0); + else if (!strcmp(argv[1], "--replace-all")) + + return git_config_set_multivar(argv[2], argv[3], NULL, 1); + else + + return git_config_set_multivar(argv[1], argv[2], argv[3], 0); + case 5: + if (!strcmp(argv[1], "--replace-all")) + return git_config_set_multivar(argv[2], argv[3], argv[4], 1); + case 1: + default: + usage(git_config_set_usage); + } + return 0; +} diff --git a/builtin-count-objects.c b/builtin-count-objects.c new file mode 100644 index 0000000000..f5b22bb80e --- /dev/null +++ b/builtin-count-objects.c @@ -0,0 +1,128 @@ +/* + * Builtin "git count-objects". + * + * Copyright (c) 2006 Junio C Hamano + */ + +#include "cache.h" +#include "builtin.h" + +static const char count_objects_usage[] = "git-count-objects [-v]"; + +static void count_objects(DIR *d, char *path, int len, int verbose, + unsigned long *loose, + unsigned long *loose_size, + unsigned long *packed_loose, + unsigned long *garbage) +{ + struct dirent *ent; + while ((ent = readdir(d)) != NULL) { + char hex[41]; + unsigned char sha1[20]; + const char *cp; + int bad = 0; + + if ((ent->d_name[0] == '.') && + (ent->d_name[1] == 0 || + ((ent->d_name[1] == '.') && (ent->d_name[2] == 0)))) + continue; + for (cp = ent->d_name; *cp; cp++) { + int ch = *cp; + if (('0' <= ch && ch <= '9') || + ('a' <= ch && ch <= 'f')) + continue; + bad = 1; + break; + } + if (cp - ent->d_name != 38) + bad = 1; + else { + struct stat st; + memcpy(path + len + 3, ent->d_name, 38); + path[len + 2] = '/'; + path[len + 41] = 0; + if (lstat(path, &st) || !S_ISREG(st.st_mode)) + bad = 1; + else + (*loose_size) += st.st_blocks; + } + if (bad) { + if (verbose) { + error("garbage found: %.*s/%s", + len + 2, path, ent->d_name); + (*garbage)++; + } + continue; + } + (*loose)++; + if (!verbose) + continue; + memcpy(hex, path+len, 2); + memcpy(hex+2, ent->d_name, 38); + hex[40] = 0; + if (get_sha1_hex(hex, sha1)) + die("internal error"); + if (has_sha1_pack(sha1, NULL)) + (*packed_loose)++; + } +} + +int cmd_count_objects(int ac, const char **av, const char *prefix) +{ + int i; + int verbose = 0; + const char *objdir = get_object_directory(); + int len = strlen(objdir); + char *path = xmalloc(len + 50); + unsigned long loose = 0, packed = 0, packed_loose = 0, garbage = 0; + unsigned long loose_size = 0; + + for (i = 1; i < ac; i++) { + const char *arg = av[i]; + if (*arg != '-') + break; + else if (!strcmp(arg, "-v")) + verbose = 1; + else + usage(count_objects_usage); + } + + /* we do not take arguments other than flags for now */ + if (i < ac) + usage(count_objects_usage); + memcpy(path, objdir, len); + if (len && objdir[len-1] != '/') + path[len++] = '/'; + for (i = 0; i < 256; i++) { + DIR *d; + sprintf(path + len, "%02x", i); + d = opendir(path); + if (!d) + continue; + count_objects(d, path, len, verbose, + &loose, &loose_size, &packed_loose, &garbage); + closedir(d); + } + if (verbose) { + struct packed_git *p; + unsigned long num_pack = 0; + if (!packed_git) + prepare_packed_git(); + for (p = packed_git; p; p = p->next) { + if (!p->pack_local) + continue; + packed += num_packed_objects(p); + num_pack++; + } + printf("count: %lu\n", loose); + printf("size: %lu\n", loose_size / 2); + printf("in-pack: %lu\n", packed); + printf("packs: %lu\n", num_pack); + printf("prune-packable: %lu\n", packed_loose); + printf("garbage: %lu\n", garbage); + } + else + printf("%lu objects, %lu kilobytes\n", + loose, loose_size / 2); + return 0; +} diff --git a/builtin-describe.c b/builtin-describe.c new file mode 100644 index 0000000000..bcc645622a --- /dev/null +++ b/builtin-describe.c @@ -0,0 +1,284 @@ +#include "cache.h" +#include "commit.h" +#include "tag.h" +#include "refs.h" +#include "builtin.h" + +#define SEEN (1u<<0) +#define MAX_TAGS (FLAG_BITS - 1) + +static const char describe_usage[] = +"git-describe [--all] [--tags] [--abbrev=<n>] <committish>*"; + +static int debug; /* Display lots of verbose info */ +static int all; /* Default to annotated tags only */ +static int tags; /* But allow any tags if --tags is specified */ +static int abbrev = DEFAULT_ABBREV; +static int max_candidates = 10; + +struct commit_name { + int prio; /* annotated tag = 2, tag = 1, head = 0 */ + char path[FLEX_ARRAY]; /* more */ +}; +static const char *prio_names[] = { + "head", "lightweight", "annotated", +}; + +static void add_to_known_names(const char *path, + struct commit *commit, + int prio) +{ + struct commit_name *e = commit->util; + if (!e || e->prio < prio) { + size_t len = strlen(path)+1; + free(e); + e = xmalloc(sizeof(struct commit_name) + len); + e->prio = prio; + memcpy(e->path, path, len); + commit->util = e; + } +} + +static int get_name(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + struct commit *commit = lookup_commit_reference_gently(sha1, 1); + struct object *object; + int prio; + + if (!commit) + return 0; + object = parse_object(sha1); + /* If --all, then any refs are used. + * If --tags, then any tags are used. + * Otherwise only annotated tags are used. + */ + if (!strncmp(path, "refs/tags/", 10)) { + if (object->type == OBJ_TAG) + prio = 2; + else + prio = 1; + } + else + prio = 0; + + if (!all) { + if (!prio) + return 0; + if (!tags && prio < 2) + return 0; + } + add_to_known_names(all ? path + 5 : path + 10, commit, prio); + return 0; +} + +struct possible_tag { + struct commit_name *name; + int depth; + int found_order; + unsigned flag_within; +}; + +static int compare_pt(const void *a_, const void *b_) +{ + struct possible_tag *a = (struct possible_tag *)a_; + struct possible_tag *b = (struct possible_tag *)b_; + if (a->name->prio != b->name->prio) + return b->name->prio - a->name->prio; + if (a->depth != b->depth) + return a->depth - b->depth; + if (a->found_order != b->found_order) + return a->found_order - b->found_order; + return 0; +} + +static unsigned long finish_depth_computation( + struct commit_list **list, + struct possible_tag *best) +{ + unsigned long seen_commits = 0; + while (*list) { + struct commit *c = pop_commit(list); + struct commit_list *parents = c->parents; + seen_commits++; + if (c->object.flags & best->flag_within) { + struct commit_list *a = *list; + while (a) { + struct commit *i = a->item; + if (!(i->object.flags & best->flag_within)) + break; + a = a->next; + } + if (!a) + break; + } else + best->depth++; + while (parents) { + struct commit *p = parents->item; + parse_commit(p); + if (!(p->object.flags & SEEN)) + insert_by_date(p, list); + p->object.flags |= c->object.flags; + parents = parents->next; + } + } + return seen_commits; +} + +static void describe(const char *arg, int last_one) +{ + unsigned char sha1[20]; + struct commit *cmit, *gave_up_on = NULL; + struct commit_list *list; + static int initialized = 0; + struct commit_name *n; + struct possible_tag all_matches[MAX_TAGS]; + unsigned int match_cnt = 0, annotated_cnt = 0, cur_match; + unsigned long seen_commits = 0; + + if (get_sha1(arg, sha1)) + die("Not a valid object name %s", arg); + cmit = lookup_commit_reference(sha1); + if (!cmit) + die("%s is not a valid '%s' object", arg, commit_type); + + if (!initialized) { + initialized = 1; + for_each_ref(get_name, NULL); + } + + n = cmit->util; + if (n) { + printf("%s\n", n->path); + return; + } + + if (debug) + fprintf(stderr, "searching to describe %s\n", arg); + + list = NULL; + cmit->object.flags = SEEN; + commit_list_insert(cmit, &list); + while (list) { + struct commit *c = pop_commit(&list); + struct commit_list *parents = c->parents; + seen_commits++; + n = c->util; + if (n) { + if (match_cnt < max_candidates) { + struct possible_tag *t = &all_matches[match_cnt++]; + t->name = n; + t->depth = seen_commits - 1; + t->flag_within = 1u << match_cnt; + t->found_order = match_cnt; + c->object.flags |= t->flag_within; + if (n->prio == 2) + annotated_cnt++; + } + else { + gave_up_on = c; + break; + } + } + for (cur_match = 0; cur_match < match_cnt; cur_match++) { + struct possible_tag *t = &all_matches[cur_match]; + if (!(c->object.flags & t->flag_within)) + t->depth++; + } + if (annotated_cnt && !list) { + if (debug) + fprintf(stderr, "finished search at %s\n", + sha1_to_hex(c->object.sha1)); + break; + } + while (parents) { + struct commit *p = parents->item; + parse_commit(p); + if (!(p->object.flags & SEEN)) + insert_by_date(p, &list); + p->object.flags |= c->object.flags; + parents = parents->next; + } + } + + if (!match_cnt) + die("cannot describe '%s'", sha1_to_hex(cmit->object.sha1)); + + qsort(all_matches, match_cnt, sizeof(all_matches[0]), compare_pt); + + if (gave_up_on) { + insert_by_date(gave_up_on, &list); + seen_commits--; + } + seen_commits += finish_depth_computation(&list, &all_matches[0]); + free_commit_list(list); + + if (debug) { + for (cur_match = 0; cur_match < match_cnt; cur_match++) { + struct possible_tag *t = &all_matches[cur_match]; + fprintf(stderr, " %-11s %8d %s\n", + prio_names[t->name->prio], + t->depth, t->name->path); + } + fprintf(stderr, "traversed %lu commits\n", seen_commits); + if (gave_up_on) { + fprintf(stderr, + "more than %i tags found; listed %i most recent\n" + "gave up search at %s\n", + max_candidates, max_candidates, + sha1_to_hex(gave_up_on->object.sha1)); + } + } + if (abbrev == 0) + printf("%s\n", all_matches[0].name->path ); + else + printf("%s-%d-g%s\n", all_matches[0].name->path, + all_matches[0].depth, + find_unique_abbrev(cmit->object.sha1, abbrev)); + + if (!last_one) + clear_commit_marks(cmit, -1); +} + +int cmd_describe(int argc, const char **argv, const char *prefix) +{ + int i; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg != '-') + break; + else if (!strcmp(arg, "--debug")) + debug = 1; + else if (!strcmp(arg, "--all")) + all = 1; + else if (!strcmp(arg, "--tags")) + tags = 1; + else if (!strncmp(arg, "--abbrev=", 9)) { + abbrev = strtoul(arg + 9, NULL, 10); + if (abbrev != 0 && (abbrev < MINIMUM_ABBREV || 40 < abbrev)) + abbrev = DEFAULT_ABBREV; + } + else if (!strncmp(arg, "--candidates=", 13)) { + max_candidates = strtoul(arg + 13, NULL, 10); + if (max_candidates < 1) + max_candidates = 1; + else if (max_candidates > MAX_TAGS) + max_candidates = MAX_TAGS; + } + else + usage(describe_usage); + } + + save_commit_buffer = 0; + + if (argc <= i) + describe("HEAD", 1); + else + while (i < argc) { + describe(argv[i], (i == argc - 1)); + i++; + } + + return 0; +} diff --git a/builtin-diff-files.c b/builtin-diff-files.c new file mode 100644 index 0000000000..5d4a5c5828 --- /dev/null +++ b/builtin-diff-files.c @@ -0,0 +1,51 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "diff.h" +#include "commit.h" +#include "revision.h" +#include "builtin.h" + +static const char diff_files_usage[] = +"git-diff-files [-q] [-0/-1/2/3 |-c|--cc] [<common diff options>] [<path>...]" +COMMON_DIFF_OPTIONS_HELP; + +int cmd_diff_files(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + int silent = 0; + + init_revisions(&rev, prefix); + git_config(git_default_config); /* no "diff" UI options */ + rev.abbrev = 0; + + argc = setup_revisions(argc, argv, &rev, NULL); + while (1 < argc && argv[1][0] == '-') { + if (!strcmp(argv[1], "--base")) + rev.max_count = 1; + else if (!strcmp(argv[1], "--ours")) + rev.max_count = 2; + else if (!strcmp(argv[1], "--theirs")) + rev.max_count = 3; + else if (!strcmp(argv[1], "-q")) + silent = 1; + else + usage(diff_files_usage); + argv++; argc--; + } + if (!rev.diffopt.output_format) + rev.diffopt.output_format = DIFF_FORMAT_RAW; + + /* + * Make sure there are NO revision (i.e. pending object) parameter, + * rev.max_count is reasonable (0 <= n <= 3), + * there is no other revision filtering parameters. + */ + if (rev.pending.nr || + rev.min_age != -1 || rev.max_age != -1) + usage(diff_files_usage); + return run_diff_files(&rev, silent); +} diff --git a/builtin-diff-index.c b/builtin-diff-index.c new file mode 100644 index 0000000000..95a3db156b --- /dev/null +++ b/builtin-diff-index.c @@ -0,0 +1,42 @@ +#include "cache.h" +#include "diff.h" +#include "commit.h" +#include "revision.h" +#include "builtin.h" + +static const char diff_cache_usage[] = +"git-diff-index [-m] [--cached] " +"[<common diff options>] <tree-ish> [<path>...]" +COMMON_DIFF_OPTIONS_HELP; + +int cmd_diff_index(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + int cached = 0; + int i; + + init_revisions(&rev, prefix); + git_config(git_default_config); /* no "diff" UI options */ + rev.abbrev = 0; + + argc = setup_revisions(argc, argv, &rev, NULL); + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (!strcmp(arg, "--cached")) + cached = 1; + else + usage(diff_cache_usage); + } + if (!rev.diffopt.output_format) + rev.diffopt.output_format = DIFF_FORMAT_RAW; + + /* + * Make sure there is one revision (i.e. pending object), + * and there is no revision filtering parameters. + */ + if (rev.pending.nr != 1 || + rev.max_count != -1 || rev.min_age != -1 || rev.max_age != -1) + usage(diff_cache_usage); + return run_diff_index(&rev, cached); +} diff --git a/builtin-diff-stages.c b/builtin-diff-stages.c new file mode 100644 index 0000000000..70bb89808d --- /dev/null +++ b/builtin-diff-stages.c @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2005 Junio C Hamano + */ + +#include "cache.h" +#include "diff.h" +#include "builtin.h" + +static struct diff_options diff_options; + +static const char diff_stages_usage[] = +"git-diff-stages [<common diff options>] <stage1> <stage2> [<path>...]" +COMMON_DIFF_OPTIONS_HELP; + +static void diff_stages(int stage1, int stage2, const char **pathspec) +{ + int i = 0; + while (i < active_nr) { + struct cache_entry *ce, *stages[4] = { NULL, }; + struct cache_entry *one, *two; + const char *name; + int len, skip; + + ce = active_cache[i]; + skip = !ce_path_match(ce, pathspec); + len = ce_namelen(ce); + name = ce->name; + for (;;) { + int stage = ce_stage(ce); + stages[stage] = ce; + if (active_nr <= ++i) + break; + ce = active_cache[i]; + if (ce_namelen(ce) != len || + memcmp(name, ce->name, len)) + break; + } + one = stages[stage1]; + two = stages[stage2]; + + if (skip || (!one && !two)) + continue; + if (!one) + diff_addremove(&diff_options, '+', ntohl(two->ce_mode), + two->sha1, name, NULL); + else if (!two) + diff_addremove(&diff_options, '-', ntohl(one->ce_mode), + one->sha1, name, NULL); + else if (hashcmp(one->sha1, two->sha1) || + (one->ce_mode != two->ce_mode) || + diff_options.find_copies_harder) + diff_change(&diff_options, + ntohl(one->ce_mode), ntohl(two->ce_mode), + one->sha1, two->sha1, name, NULL); + } +} + +int cmd_diff_stages(int ac, const char **av, const char *prefix) +{ + int stage1, stage2; + const char **pathspec = NULL; + + git_config(git_default_config); /* no "diff" UI options */ + read_cache(); + diff_setup(&diff_options); + while (1 < ac && av[1][0] == '-') { + const char *arg = av[1]; + if (!strcmp(arg, "-r")) + ; /* as usual */ + else { + int diff_opt_cnt; + diff_opt_cnt = diff_opt_parse(&diff_options, + av+1, ac-1); + if (diff_opt_cnt < 0) + usage(diff_stages_usage); + else if (diff_opt_cnt) { + av += diff_opt_cnt; + ac -= diff_opt_cnt; + continue; + } + else + usage(diff_stages_usage); + } + ac--; av++; + } + + if (!diff_options.output_format) + diff_options.output_format = DIFF_FORMAT_RAW; + + if (ac < 3 || + sscanf(av[1], "%d", &stage1) != 1 || + ! (0 <= stage1 && stage1 <= 3) || + sscanf(av[2], "%d", &stage2) != 1 || + ! (0 <= stage2 && stage2 <= 3)) + usage(diff_stages_usage); + + av += 3; /* The rest from av[0] are for paths restriction. */ + pathspec = get_pathspec(prefix, av); + + if (diff_setup_done(&diff_options) < 0) + usage(diff_stages_usage); + + diff_stages(stage1, stage2, pathspec); + diffcore_std(&diff_options); + diff_flush(&diff_options); + return 0; +} diff --git a/builtin-diff-tree.c b/builtin-diff-tree.c new file mode 100644 index 0000000000..24cb2d7f84 --- /dev/null +++ b/builtin-diff-tree.c @@ -0,0 +1,137 @@ +#include "cache.h" +#include "diff.h" +#include "commit.h" +#include "log-tree.h" +#include "builtin.h" + +static struct rev_info log_tree_opt; + +static int diff_tree_commit_sha1(const unsigned char *sha1) +{ + struct commit *commit = lookup_commit_reference(sha1); + if (!commit) + return -1; + return log_tree_commit(&log_tree_opt, commit); +} + +static int diff_tree_stdin(char *line) +{ + int len = strlen(line); + unsigned char sha1[20]; + struct commit *commit; + + if (!len || line[len-1] != '\n') + return -1; + line[len-1] = 0; + if (get_sha1_hex(line, sha1)) + return -1; + commit = lookup_commit(sha1); + if (!commit || parse_commit(commit)) + return -1; + if (isspace(line[40]) && !get_sha1_hex(line+41, sha1)) { + /* Graft the fake parents locally to the commit */ + int pos = 41; + struct commit_list **pptr, *parents; + + /* Free the real parent list */ + for (parents = commit->parents; parents; ) { + struct commit_list *tmp = parents->next; + free(parents); + parents = tmp; + } + commit->parents = NULL; + pptr = &(commit->parents); + while (line[pos] && !get_sha1_hex(line + pos, sha1)) { + struct commit *parent = lookup_commit(sha1); + if (parent) { + pptr = &commit_list_insert(parent, pptr)->next; + } + pos += 41; + } + } + return log_tree_commit(&log_tree_opt, commit); +} + +static const char diff_tree_usage[] = +"git-diff-tree [--stdin] [-m] [-c] [--cc] [-s] [-v] [--pretty] [-t] [-r] [--root] " +"[<common diff options>] <tree-ish> [<tree-ish>] [<path>...]\n" +" -r diff recursively\n" +" --root include the initial commit as diff against /dev/null\n" +COMMON_DIFF_OPTIONS_HELP; + +int cmd_diff_tree(int argc, const char **argv, const char *prefix) +{ + int nr_sha1; + char line[1000]; + struct object *tree1, *tree2; + static struct rev_info *opt = &log_tree_opt; + int read_stdin = 0; + + init_revisions(opt, prefix); + git_config(git_default_config); /* no "diff" UI options */ + nr_sha1 = 0; + opt->abbrev = 0; + opt->diff = 1; + argc = setup_revisions(argc, argv, opt, NULL); + + while (--argc > 0) { + const char *arg = *++argv; + + if (!strcmp(arg, "--stdin")) { + read_stdin = 1; + continue; + } + usage(diff_tree_usage); + } + + if (!opt->diffopt.output_format) + opt->diffopt.output_format = DIFF_FORMAT_RAW; + + /* + * NOTE! We expect "a ^b" to be equal to "a..b", so we + * reverse the order of the objects if the second one + * is marked UNINTERESTING. + */ + nr_sha1 = opt->pending.nr; + switch (nr_sha1) { + case 0: + if (!read_stdin) + usage(diff_tree_usage); + break; + case 1: + tree1 = opt->pending.objects[0].item; + diff_tree_commit_sha1(tree1->sha1); + break; + case 2: + tree1 = opt->pending.objects[0].item; + tree2 = opt->pending.objects[1].item; + if (tree2->flags & UNINTERESTING) { + struct object *tmp = tree2; + tree2 = tree1; + tree1 = tmp; + } + diff_tree_sha1(tree1->sha1, + tree2->sha1, + "", &opt->diffopt); + log_tree_diff_flush(opt); + break; + } + + if (!read_stdin) + return 0; + + if (opt->diffopt.detect_rename) + opt->diffopt.setup |= (DIFF_SETUP_USE_SIZE_CACHE | + DIFF_SETUP_USE_CACHE); + 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; +} diff --git a/builtin-diff.c b/builtin-diff.c new file mode 100644 index 0000000000..a6590205e8 --- /dev/null +++ b/builtin-diff.c @@ -0,0 +1,351 @@ +/* + * Builtin "git diff" + * + * Copyright (c) 2006 Junio C Hamano + */ +#include "cache.h" +#include "commit.h" +#include "blob.h" +#include "tag.h" +#include "diff.h" +#include "diffcore.h" +#include "revision.h" +#include "log-tree.h" +#include "builtin.h" + +/* NEEDSWORK: struct object has place for name but we _do_ + * know mode when we extracted the blob out of a tree, which + * we currently lose. + */ +struct blobinfo { + unsigned char sha1[20]; + const char *name; +}; + +static const char builtin_diff_usage[] = +"git-diff <options> <rev>{0,2} -- <path>*"; + +static int builtin_diff_files(struct rev_info *revs, + int argc, const char **argv) +{ + int silent = 0; + while (1 < argc) { + const char *arg = argv[1]; + if (!strcmp(arg, "--base")) + revs->max_count = 1; + else if (!strcmp(arg, "--ours")) + revs->max_count = 2; + else if (!strcmp(arg, "--theirs")) + revs->max_count = 3; + else if (!strcmp(arg, "-q")) + silent = 1; + else + usage(builtin_diff_usage); + argv++; argc--; + } + /* + * Make sure there are NO revision (i.e. pending object) parameter, + * specified rev.max_count is reasonable (0 <= n <= 3), and + * there is no other revision filtering parameter. + */ + if (revs->pending.nr || + revs->min_age != -1 || + revs->max_age != -1 || + 3 < revs->max_count) + usage(builtin_diff_usage); + if (revs->max_count < 0 && + (revs->diffopt.output_format & DIFF_FORMAT_PATCH)) + revs->combine_merges = revs->dense_combined_merges = 1; + return run_diff_files(revs, silent); +} + +static void stuff_change(struct diff_options *opt, + unsigned old_mode, unsigned new_mode, + const unsigned char *old_sha1, + const unsigned char *new_sha1, + const char *old_name, + const char *new_name) +{ + struct diff_filespec *one, *two; + + if (!is_null_sha1(old_sha1) && !is_null_sha1(new_sha1) && + !hashcmp(old_sha1, new_sha1)) + return; + + if (opt->reverse_diff) { + unsigned tmp; + const unsigned char *tmp_u; + const char *tmp_c; + tmp = old_mode; old_mode = new_mode; new_mode = tmp; + tmp_u = old_sha1; old_sha1 = new_sha1; new_sha1 = tmp_u; + tmp_c = old_name; old_name = new_name; new_name = tmp_c; + } + one = alloc_filespec(old_name); + two = alloc_filespec(new_name); + fill_filespec(one, old_sha1, old_mode); + fill_filespec(two, new_sha1, new_mode); + + /* NEEDSWORK: shouldn't this part of diffopt??? */ + diff_queue(&diff_queued_diff, one, two); +} + +static int builtin_diff_b_f(struct rev_info *revs, + int argc, const char **argv, + struct blobinfo *blob, + const char *path) +{ + /* Blob vs file in the working tree*/ + struct stat st; + + if (argc > 1) + usage(builtin_diff_usage); + + if (lstat(path, &st)) + die("'%s': %s", path, strerror(errno)); + if (!(S_ISREG(st.st_mode) || S_ISLNK(st.st_mode))) + die("'%s': not a regular file or symlink", path); + stuff_change(&revs->diffopt, + canon_mode(st.st_mode), canon_mode(st.st_mode), + blob[0].sha1, null_sha1, + path, path); + diffcore_std(&revs->diffopt); + diff_flush(&revs->diffopt); + return 0; +} + +static int builtin_diff_blobs(struct rev_info *revs, + int argc, const char **argv, + struct blobinfo *blob) +{ + unsigned mode = canon_mode(S_IFREG | 0644); + + if (argc > 1) + usage(builtin_diff_usage); + + stuff_change(&revs->diffopt, + mode, mode, + blob[0].sha1, blob[1].sha1, + blob[0].name, blob[1].name); + diffcore_std(&revs->diffopt); + diff_flush(&revs->diffopt); + return 0; +} + +static int builtin_diff_index(struct rev_info *revs, + int argc, const char **argv) +{ + int cached = 0; + while (1 < argc) { + const char *arg = argv[1]; + if (!strcmp(arg, "--cached")) + cached = 1; + else + usage(builtin_diff_usage); + argv++; argc--; + } + /* + * Make sure there is one revision (i.e. pending object), + * and there is no revision filtering parameters. + */ + if (revs->pending.nr != 1 || + revs->max_count != -1 || revs->min_age != -1 || + revs->max_age != -1) + usage(builtin_diff_usage); + return run_diff_index(revs, cached); +} + +static int builtin_diff_tree(struct rev_info *revs, + int argc, const char **argv, + struct object_array_entry *ent) +{ + const unsigned char *(sha1[2]); + int swap = 0; + + if (argc > 1) + usage(builtin_diff_usage); + + /* We saw two trees, ent[0] and ent[1]. + * if ent[1] is uninteresting, they are swapped + */ + if (ent[1].item->flags & UNINTERESTING) + swap = 1; + sha1[swap] = ent[0].item->sha1; + sha1[1-swap] = ent[1].item->sha1; + diff_tree_sha1(sha1[0], sha1[1], "", &revs->diffopt); + log_tree_diff_flush(revs); + return 0; +} + +static int builtin_diff_combined(struct rev_info *revs, + int argc, const char **argv, + struct object_array_entry *ent, + int ents) +{ + const unsigned char (*parent)[20]; + int i; + + if (argc > 1) + usage(builtin_diff_usage); + + if (!revs->dense_combined_merges && !revs->combine_merges) + revs->dense_combined_merges = revs->combine_merges = 1; + parent = xmalloc(ents * sizeof(*parent)); + /* Again, the revs are all reverse */ + for (i = 0; i < ents; i++) + hashcpy((unsigned char*)parent + i, ent[ents - 1 - i].item->sha1); + diff_tree_combined(parent[0], parent + 1, ents - 1, + revs->dense_combined_merges, revs); + return 0; +} + +void add_head(struct rev_info *revs) +{ + unsigned char sha1[20]; + struct object *obj; + if (get_sha1("HEAD", sha1)) + return; + obj = parse_object(sha1); + if (!obj) + return; + add_pending_object(revs, obj, "HEAD"); +} + +int cmd_diff(int argc, const char **argv, const char *prefix) +{ + int i; + struct rev_info rev; + struct object_array_entry ent[100]; + int ents = 0, blobs = 0, paths = 0; + const char *path = NULL; + struct blobinfo blob[2]; + + /* + * We could get N tree-ish in the rev.pending_objects list. + * Also there could be M blobs there, and P pathspecs. + * + * N=0, M=0: + * cache vs files (diff-files) + * N=0, M=2: + * compare two random blobs. P must be zero. + * N=0, M=1, P=1: + * compare a blob with a working tree file. + * + * N=1, M=0: + * tree vs cache (diff-index --cached) + * + * N=2, M=0: + * tree vs tree (diff-tree) + * + * Other cases are errors. + */ + + git_config(git_diff_ui_config); + init_revisions(&rev, prefix); + + argc = setup_revisions(argc, argv, &rev, NULL); + if (!rev.diffopt.output_format) { + rev.diffopt.output_format = DIFF_FORMAT_PATCH; + if (diff_setup_done(&rev.diffopt) < 0) + die("diff_setup_done failed"); + } + + /* Do we have --cached and not have a pending object, then + * default to HEAD by hand. Eek. + */ + if (!rev.pending.nr) { + int i; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--")) + break; + else if (!strcmp(arg, "--cached")) { + add_head(&rev); + break; + } + } + } + + for (i = 0; i < rev.pending.nr; i++) { + struct object_array_entry *list = rev.pending.objects+i; + struct object *obj = list->item; + const char *name = list->name; + int flags = (obj->flags & UNINTERESTING); + if (!obj->parsed) + obj = parse_object(obj->sha1); + obj = deref_tag(obj, NULL, 0); + if (!obj) + die("invalid object '%s' given.", name); + if (obj->type == OBJ_COMMIT) + obj = &((struct commit *)obj)->tree->object; + if (obj->type == OBJ_TREE) { + if (ARRAY_SIZE(ent) <= ents) + die("more than %d trees given: '%s'", + (int) ARRAY_SIZE(ent), name); + obj->flags |= flags; + ent[ents].item = obj; + ent[ents].name = name; + ents++; + continue; + } + if (obj->type == OBJ_BLOB) { + if (2 <= blobs) + die("more than two blobs given: '%s'", name); + hashcpy(blob[blobs].sha1, obj->sha1); + blob[blobs].name = name; + blobs++; + continue; + + } + die("unhandled object '%s' given.", name); + } + if (rev.prune_data) { + const char **pathspec = rev.prune_data; + while (*pathspec) { + if (!path) + path = *pathspec; + paths++; + pathspec++; + } + } + + /* + * Now, do the arguments look reasonable? + */ + if (!ents) { + switch (blobs) { + case 0: + return builtin_diff_files(&rev, argc, argv); + break; + case 1: + if (paths != 1) + usage(builtin_diff_usage); + return builtin_diff_b_f(&rev, argc, argv, blob, path); + break; + case 2: + if (paths) + usage(builtin_diff_usage); + return builtin_diff_blobs(&rev, argc, argv, blob); + break; + default: + usage(builtin_diff_usage); + } + } + else if (blobs) + usage(builtin_diff_usage); + else if (ents == 1) + return builtin_diff_index(&rev, argc, argv); + else if (ents == 2) + return builtin_diff_tree(&rev, argc, argv, ent); + else if ((ents == 3) && (ent[0].item->flags & UNINTERESTING)) { + /* diff A...B where there is one sane merge base between + * A and B. We have ent[0] == merge-base, ent[1] == A, + * and ent[2] == B. Show diff between the base and B. + */ + ent[1] = ent[2]; + return builtin_diff_tree(&rev, argc, argv, ent); + } + else + return builtin_diff_combined(&rev, argc, argv, + ent, ents); + usage(builtin_diff_usage); +} diff --git a/builtin-fmt-merge-msg.c b/builtin-fmt-merge-msg.c new file mode 100644 index 0000000000..87d3d63ec7 --- /dev/null +++ b/builtin-fmt-merge-msg.c @@ -0,0 +1,358 @@ +#include "builtin.h" +#include "cache.h" +#include "commit.h" +#include "diff.h" +#include "revision.h" +#include "tag.h" + +static const char *fmt_merge_msg_usage = + "git-fmt-merge-msg [--summary] [--no-summary] [--file <file>]"; + +static int merge_summary; + +static int fmt_merge_msg_config(const char *key, const char *value) +{ + if (!strcmp("merge.summary", key)) + merge_summary = git_config_bool(key, value); + return 0; +} + +struct list { + char **list; + void **payload; + unsigned nr, alloc; +}; + +static void append_to_list(struct list *list, char *value, void *payload) +{ + if (list->nr == list->alloc) { + list->alloc += 32; + list->list = xrealloc(list->list, sizeof(char *) * list->alloc); + list->payload = xrealloc(list->payload, + sizeof(char *) * list->alloc); + } + list->payload[list->nr] = payload; + list->list[list->nr++] = value; +} + +static int find_in_list(struct list *list, char *value) +{ + int i; + + for (i = 0; i < list->nr; i++) + if (!strcmp(list->list[i], value)) + return i; + + return -1; +} + +static void free_list(struct list *list) +{ + int i; + + if (list->alloc == 0) + return; + + for (i = 0; i < list->nr; i++) { + free(list->list[i]); + free(list->payload[i]); + } + free(list->list); + free(list->payload); + list->nr = list->alloc = 0; +} + +struct src_data { + struct list branch, tag, r_branch, generic; + int head_status; +}; + +static struct list srcs = { NULL, NULL, 0, 0}; +static struct list origins = { NULL, NULL, 0, 0}; + +static int handle_line(char *line) +{ + int i, len = strlen(line); + unsigned char *sha1; + char *src, *origin; + struct src_data *src_data; + int pulling_head = 0; + + if (len < 43 || line[40] != '\t') + return 1; + + if (!strncmp(line + 41, "not-for-merge", 13)) + return 0; + + if (line[41] != '\t') + return 2; + + line[40] = 0; + sha1 = xmalloc(20); + i = get_sha1(line, sha1); + line[40] = '\t'; + if (i) + return 3; + + if (line[len - 1] == '\n') + line[len - 1] = 0; + line += 42; + + src = strstr(line, " of "); + if (src) { + *src = 0; + src += 4; + pulling_head = 0; + } else { + src = line; + pulling_head = 1; + } + + i = find_in_list(&srcs, src); + if (i < 0) { + i = srcs.nr; + append_to_list(&srcs, xstrdup(src), + xcalloc(1, sizeof(struct src_data))); + } + src_data = srcs.payload[i]; + + if (pulling_head) { + origin = xstrdup(src); + src_data->head_status |= 1; + } else if (!strncmp(line, "branch ", 7)) { + origin = xstrdup(line + 7); + append_to_list(&src_data->branch, origin, NULL); + src_data->head_status |= 2; + } else if (!strncmp(line, "tag ", 4)) { + origin = line; + append_to_list(&src_data->tag, xstrdup(origin + 4), NULL); + src_data->head_status |= 2; + } else if (!strncmp(line, "remote branch ", 14)) { + origin = xstrdup(line + 14); + append_to_list(&src_data->r_branch, origin, NULL); + src_data->head_status |= 2; + } else { + origin = xstrdup(src); + append_to_list(&src_data->generic, xstrdup(line), NULL); + src_data->head_status |= 2; + } + + if (!strcmp(".", src) || !strcmp(src, origin)) { + int len = strlen(origin); + if (origin[0] == '\'' && origin[len - 1] == '\'') { + char *new_origin = xmalloc(len - 1); + memcpy(new_origin, origin + 1, len - 2); + new_origin[len - 2] = 0; + origin = new_origin; + } else + origin = xstrdup(origin); + } else { + char *new_origin = xmalloc(strlen(origin) + strlen(src) + 5); + sprintf(new_origin, "%s of %s", origin, src); + origin = new_origin; + } + append_to_list(&origins, origin, sha1); + return 0; +} + +static void print_joined(const char *singular, const char *plural, + struct list *list) +{ + if (list->nr == 0) + return; + if (list->nr == 1) { + printf("%s%s", singular, list->list[0]); + } else { + int i; + printf("%s", plural); + for (i = 0; i < list->nr - 1; i++) + printf("%s%s", i > 0 ? ", " : "", list->list[i]); + printf(" and %s", list->list[list->nr - 1]); + } +} + +static void shortlog(const char *name, unsigned char *sha1, + struct commit *head, struct rev_info *rev, int limit) +{ + int i, count = 0; + struct commit *commit; + struct object *branch; + struct list subjects = { NULL, NULL, 0, 0 }; + int flags = UNINTERESTING | TREECHANGE | SEEN | SHOWN | ADDED; + + branch = deref_tag(parse_object(sha1), sha1_to_hex(sha1), 40); + if (!branch || branch->type != OBJ_COMMIT) + return; + + setup_revisions(0, NULL, rev, NULL); + rev->ignore_merges = 1; + add_pending_object(rev, branch, name); + add_pending_object(rev, &head->object, "^HEAD"); + head->object.flags |= UNINTERESTING; + prepare_revision_walk(rev); + while ((commit = get_revision(rev)) != NULL) { + char *oneline, *bol, *eol; + + /* ignore merges */ + if (commit->parents && commit->parents->next) + continue; + + count++; + if (subjects.nr > limit) + continue; + + bol = strstr(commit->buffer, "\n\n"); + if (!bol) { + append_to_list(&subjects, xstrdup(sha1_to_hex( + commit->object.sha1)), + NULL); + continue; + } + + bol += 2; + eol = strchr(bol, '\n'); + + if (eol) { + int len = eol - bol; + oneline = xmalloc(len + 1); + memcpy(oneline, bol, len); + oneline[len] = 0; + } else + oneline = xstrdup(bol); + append_to_list(&subjects, oneline, NULL); + } + + if (count > limit) + printf("\n* %s: (%d commits)\n", name, count); + else + printf("\n* %s:\n", name); + + for (i = 0; i < subjects.nr; i++) + if (i >= limit) + printf(" ...\n"); + else + printf(" %s\n", subjects.list[i]); + + clear_commit_marks((struct commit *)branch, flags); + clear_commit_marks(head, flags); + free_commit_list(rev->commits); + rev->commits = NULL; + rev->pending.nr = 0; + + free_list(&subjects); +} + +int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix) +{ + int limit = 20, i = 0; + char line[1024]; + FILE *in = stdin; + const char *sep = ""; + unsigned char head_sha1[20]; + const char *current_branch; + + git_config(fmt_merge_msg_config); + + while (argc > 1) { + if (!strcmp(argv[1], "--summary")) + merge_summary = 1; + else if (!strcmp(argv[1], "--no-summary")) + merge_summary = 0; + else if (!strcmp(argv[1], "-F") || !strcmp(argv[1], "--file")) { + if (argc < 2) + die ("Which file?"); + if (!strcmp(argv[2], "-")) + in = stdin; + else { + fclose(in); + in = fopen(argv[2], "r"); + } + argc--; argv++; + } else + break; + argc--; argv++; + } + + if (argc > 1) + usage(fmt_merge_msg_usage); + + /* get current branch */ + current_branch = resolve_ref("HEAD", head_sha1, 1, NULL); + if (!current_branch) + die("No current branch"); + if (!strncmp(current_branch, "refs/heads/", 11)) + current_branch += 11; + + while (fgets(line, sizeof(line), in)) { + i++; + if (line[0] == 0) + continue; + if (handle_line(line)) + die ("Error in line %d: %s", i, line); + } + + printf("Merge "); + for (i = 0; i < srcs.nr; i++) { + struct src_data *src_data = srcs.payload[i]; + const char *subsep = ""; + + printf(sep); + sep = "; "; + + if (src_data->head_status == 1) { + printf(srcs.list[i]); + continue; + } + if (src_data->head_status == 3) { + subsep = ", "; + printf("HEAD"); + } + if (src_data->branch.nr) { + printf(subsep); + subsep = ", "; + print_joined("branch ", "branches ", &src_data->branch); + } + if (src_data->r_branch.nr) { + printf(subsep); + subsep = ", "; + print_joined("remote branch ", "remote branches ", + &src_data->r_branch); + } + if (src_data->tag.nr) { + printf(subsep); + subsep = ", "; + print_joined("tag ", "tags ", &src_data->tag); + } + if (src_data->generic.nr) { + printf(subsep); + print_joined("commit ", "commits ", &src_data->generic); + } + if (strcmp(".", srcs.list[i])) + printf(" of %s", srcs.list[i]); + } + + if (!strcmp("master", current_branch)) + putchar('\n'); + else + printf(" into %s\n", current_branch); + + if (merge_summary) { + struct commit *head; + struct rev_info rev; + + head = lookup_commit(head_sha1); + init_revisions(&rev, prefix); + rev.commit_format = CMIT_FMT_ONELINE; + rev.ignore_merges = 1; + rev.limited = 1; + + for (i = 0; i < origins.nr; i++) + shortlog(origins.list[i], origins.payload[i], + head, &rev, limit); + } + + /* No cleanup yet; is standalone anyway */ + + return 0; +} + diff --git a/builtin-for-each-ref.c b/builtin-for-each-ref.c new file mode 100644 index 0000000000..16c785f047 --- /dev/null +++ b/builtin-for-each-ref.c @@ -0,0 +1,905 @@ +#include "cache.h" +#include "refs.h" +#include "object.h" +#include "tag.h" +#include "commit.h" +#include "tree.h" +#include "blob.h" +#include "quote.h" + +/* Quoting styles */ +#define QUOTE_NONE 0 +#define QUOTE_SHELL 1 +#define QUOTE_PERL 2 +#define QUOTE_PYTHON 3 +#define QUOTE_TCL 4 + +typedef enum { FIELD_STR, FIELD_ULONG, FIELD_TIME } cmp_type; + +struct atom_value { + const char *s; + unsigned long ul; /* used for sorting when not FIELD_STR */ +}; + +struct ref_sort { + struct ref_sort *next; + int atom; /* index into used_atom array */ + unsigned reverse : 1; +}; + +struct refinfo { + char *refname; + unsigned char objectname[20]; + struct atom_value *value; +}; + +static struct { + const char *name; + cmp_type cmp_type; +} valid_atom[] = { + { "refname" }, + { "objecttype" }, + { "objectsize", FIELD_ULONG }, + { "objectname" }, + { "tree" }, + { "parent" }, /* NEEDSWORK: how to address 2nd and later parents? */ + { "numparent", FIELD_ULONG }, + { "object" }, + { "type" }, + { "tag" }, + { "author" }, + { "authorname" }, + { "authoremail" }, + { "authordate", FIELD_TIME }, + { "committer" }, + { "committername" }, + { "committeremail" }, + { "committerdate", FIELD_TIME }, + { "tagger" }, + { "taggername" }, + { "taggeremail" }, + { "taggerdate", FIELD_TIME }, + { "creator" }, + { "creatordate", FIELD_TIME }, + { "subject" }, + { "body" }, + { "contents" }, +}; + +/* + * An atom is a valid field atom listed above, possibly prefixed with + * a "*" to denote deref_tag(). + * + * We parse given format string and sort specifiers, and make a list + * of properties that we need to extract out of objects. refinfo + * structure will hold an array of values extracted that can be + * indexed with the "atom number", which is an index into this + * array. + */ +static const char **used_atom; +static cmp_type *used_atom_type; +static int used_atom_cnt, sort_atom_limit, need_tagged; + +/* + * Used to parse format string and sort specifiers + */ +static int parse_atom(const char *atom, const char *ep) +{ + const char *sp; + char *n; + int i, at; + + sp = atom; + if (*sp == '*' && sp < ep) + sp++; /* deref */ + if (ep <= sp) + die("malformed field name: %.*s", (int)(ep-atom), atom); + + /* Do we have the atom already used elsewhere? */ + for (i = 0; i < used_atom_cnt; i++) { + int len = strlen(used_atom[i]); + if (len == ep - atom && !memcmp(used_atom[i], atom, len)) + return i; + } + + /* Is the atom a valid one? */ + for (i = 0; i < ARRAY_SIZE(valid_atom); i++) { + int len = strlen(valid_atom[i].name); + if (len == ep - sp && !memcmp(valid_atom[i].name, sp, len)) + break; + } + + if (ARRAY_SIZE(valid_atom) <= i) + die("unknown field name: %.*s", (int)(ep-atom), atom); + + /* Add it in, including the deref prefix */ + at = used_atom_cnt; + used_atom_cnt++; + used_atom = xrealloc(used_atom, + (sizeof *used_atom) * used_atom_cnt); + used_atom_type = xrealloc(used_atom_type, + (sizeof(*used_atom_type) * used_atom_cnt)); + n = xmalloc(ep - atom + 1); + memcpy(n, atom, ep - atom); + n[ep-atom] = 0; + used_atom[at] = n; + used_atom_type[at] = valid_atom[i].cmp_type; + return at; +} + +/* + * In a format string, find the next occurrence of %(atom). + */ +static const char *find_next(const char *cp) +{ + while (*cp) { + if (*cp == '%') { + /* %( is the start of an atom; + * %% is a quoted per-cent. + */ + if (cp[1] == '(') + return cp; + else if (cp[1] == '%') + cp++; /* skip over two % */ + /* otherwise this is a singleton, literal % */ + } + cp++; + } + return NULL; +} + +/* + * Make sure the format string is well formed, and parse out + * the used atoms. + */ +static void verify_format(const char *format) +{ + const char *cp, *sp; + for (cp = format; *cp && (sp = find_next(cp)); ) { + const char *ep = strchr(sp, ')'); + if (!ep) + die("malformatted format string %s", sp); + /* sp points at "%(" and ep points at the closing ")" */ + parse_atom(sp + 2, ep); + cp = ep + 1; + } +} + +/* + * Given an object name, read the object data and size, and return a + * "struct object". If the object data we are returning is also borrowed + * by the "struct object" representation, set *eaten as well---it is a + * signal from parse_object_buffer to us not to free the buffer. + */ +static void *get_obj(const unsigned char *sha1, struct object **obj, unsigned long *sz, int *eaten) +{ + char type[20]; + void *buf = read_sha1_file(sha1, type, sz); + + if (buf) + *obj = parse_object_buffer(sha1, type, *sz, buf, eaten); + else + *obj = NULL; + return buf; +} + +/* See grab_values */ +static void grab_common_values(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz) +{ + int i; + + for (i = 0; i < used_atom_cnt; i++) { + const char *name = used_atom[i]; + struct atom_value *v = &val[i]; + if (!!deref != (*name == '*')) + continue; + if (deref) + name++; + if (!strcmp(name, "objecttype")) + v->s = type_names[obj->type]; + else if (!strcmp(name, "objectsize")) { + char *s = xmalloc(40); + sprintf(s, "%lu", sz); + v->ul = sz; + v->s = s; + } + else if (!strcmp(name, "objectname")) { + char *s = xmalloc(41); + strcpy(s, sha1_to_hex(obj->sha1)); + v->s = s; + } + } +} + +/* See grab_values */ +static void grab_tag_values(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz) +{ + int i; + struct tag *tag = (struct tag *) obj; + + for (i = 0; i < used_atom_cnt; i++) { + const char *name = used_atom[i]; + struct atom_value *v = &val[i]; + if (!!deref != (*name == '*')) + continue; + if (deref) + name++; + if (!strcmp(name, "tag")) + v->s = tag->tag; + } +} + +static int num_parents(struct commit *commit) +{ + struct commit_list *parents; + int i; + + for (i = 0, parents = commit->parents; + parents; + parents = parents->next) + i++; + return i; +} + +/* See grab_values */ +static void grab_commit_values(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz) +{ + int i; + struct commit *commit = (struct commit *) obj; + + for (i = 0; i < used_atom_cnt; i++) { + const char *name = used_atom[i]; + struct atom_value *v = &val[i]; + if (!!deref != (*name == '*')) + continue; + if (deref) + name++; + if (!strcmp(name, "tree")) { + char *s = xmalloc(41); + strcpy(s, sha1_to_hex(commit->tree->object.sha1)); + v->s = s; + } + if (!strcmp(name, "numparent")) { + char *s = xmalloc(40); + sprintf(s, "%lu", v->ul); + v->s = s; + v->ul = num_parents(commit); + } + else if (!strcmp(name, "parent")) { + int num = num_parents(commit); + int i; + struct commit_list *parents; + char *s = xmalloc(42 * num); + v->s = s; + for (i = 0, parents = commit->parents; + parents; + parents = parents->next, i = i + 42) { + struct commit *parent = parents->item; + strcpy(s+i, sha1_to_hex(parent->object.sha1)); + if (parents->next) + s[i+40] = ' '; + } + } + } +} + +static const char *find_wholine(const char *who, int wholen, const char *buf, unsigned long sz) +{ + const char *eol; + while (*buf) { + if (!strncmp(buf, who, wholen) && + buf[wholen] == ' ') + return buf + wholen + 1; + eol = strchr(buf, '\n'); + if (!eol) + return ""; + eol++; + if (eol[1] == '\n') + return ""; /* end of header */ + buf = eol; + } + return ""; +} + +static char *copy_line(const char *buf) +{ + const char *eol = strchr(buf, '\n'); + char *line; + int len; + if (!eol) + return ""; + len = eol - buf; + line = xmalloc(len + 1); + memcpy(line, buf, len); + line[len] = 0; + return line; +} + +static char *copy_name(const char *buf) +{ + const char *eol = strchr(buf, '\n'); + const char *eoname = strstr(buf, " <"); + char *line; + int len; + if (!(eoname && eol && eoname < eol)) + return ""; + len = eoname - buf; + line = xmalloc(len + 1); + memcpy(line, buf, len); + line[len] = 0; + return line; +} + +static char *copy_email(const char *buf) +{ + const char *email = strchr(buf, '<'); + const char *eoemail = strchr(email, '>'); + char *line; + int len; + if (!email || !eoemail) + return ""; + eoemail++; + len = eoemail - email; + line = xmalloc(len + 1); + memcpy(line, email, len); + line[len] = 0; + return line; +} + +static void grab_date(const char *buf, struct atom_value *v) +{ + const char *eoemail = strstr(buf, "> "); + char *zone; + unsigned long timestamp; + long tz; + + if (!eoemail) + goto bad; + timestamp = strtoul(eoemail + 2, &zone, 10); + if (timestamp == ULONG_MAX) + goto bad; + tz = strtol(zone, NULL, 10); + if ((tz == LONG_MIN || tz == LONG_MAX) && errno == ERANGE) + goto bad; + v->s = xstrdup(show_date(timestamp, tz, 0)); + v->ul = timestamp; + return; + bad: + v->s = ""; + v->ul = 0; +} + +/* See grab_values */ +static void grab_person(const char *who, struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz) +{ + int i; + int wholen = strlen(who); + const char *wholine = NULL; + + for (i = 0; i < used_atom_cnt; i++) { + const char *name = used_atom[i]; + struct atom_value *v = &val[i]; + if (!!deref != (*name == '*')) + continue; + if (deref) + name++; + if (strncmp(who, name, wholen)) + continue; + if (name[wholen] != 0 && + strcmp(name + wholen, "name") && + strcmp(name + wholen, "email") && + strcmp(name + wholen, "date")) + continue; + if (!wholine) + wholine = find_wholine(who, wholen, buf, sz); + if (!wholine) + return; /* no point looking for it */ + if (name[wholen] == 0) + v->s = copy_line(wholine); + else if (!strcmp(name + wholen, "name")) + v->s = copy_name(wholine); + else if (!strcmp(name + wholen, "email")) + v->s = copy_email(wholine); + else if (!strcmp(name + wholen, "date")) + grab_date(wholine, v); + } + + /* For a tag or a commit object, if "creator" or "creatordate" is + * requested, do something special. + */ + if (strcmp(who, "tagger") && strcmp(who, "committer")) + return; /* "author" for commit object is not wanted */ + if (!wholine) + wholine = find_wholine(who, wholen, buf, sz); + if (!wholine) + return; + for (i = 0; i < used_atom_cnt; i++) { + const char *name = used_atom[i]; + struct atom_value *v = &val[i]; + if (!!deref != (*name == '*')) + continue; + if (deref) + name++; + + if (!strcmp(name, "creatordate")) + grab_date(wholine, v); + else if (!strcmp(name, "creator")) + v->s = copy_line(wholine); + } +} + +static void find_subpos(const char *buf, unsigned long sz, const char **sub, const char **body) +{ + while (*buf) { + const char *eol = strchr(buf, '\n'); + if (!eol) + return; + if (eol[1] == '\n') { + buf = eol + 1; + break; /* found end of header */ + } + buf = eol + 1; + } + while (*buf == '\n') + buf++; + if (!*buf) + return; + *sub = buf; /* first non-empty line */ + buf = strchr(buf, '\n'); + if (!buf) + return; /* no body */ + while (*buf == '\n') + buf++; /* skip blank between subject and body */ + *body = buf; +} + +/* See grab_values */ +static void grab_sub_body_contents(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz) +{ + int i; + const char *subpos = NULL, *bodypos = NULL; + + for (i = 0; i < used_atom_cnt; i++) { + const char *name = used_atom[i]; + struct atom_value *v = &val[i]; + if (!!deref != (*name == '*')) + continue; + if (deref) + name++; + if (strcmp(name, "subject") && + strcmp(name, "body") && + strcmp(name, "contents")) + continue; + if (!subpos) + find_subpos(buf, sz, &subpos, &bodypos); + if (!subpos) + return; + + if (!strcmp(name, "subject")) + v->s = copy_line(subpos); + else if (!strcmp(name, "body")) + v->s = xstrdup(bodypos); + else if (!strcmp(name, "contents")) + v->s = xstrdup(subpos); + } +} + +/* We want to have empty print-string for field requests + * that do not apply (e.g. "authordate" for a tag object) + */ +static void fill_missing_values(struct atom_value *val) +{ + int i; + for (i = 0; i < used_atom_cnt; i++) { + struct atom_value *v = &val[i]; + if (v->s == NULL) + v->s = ""; + } +} + +/* + * val is a list of atom_value to hold returned values. Extract + * the values for atoms in used_atom array out of (obj, buf, sz). + * when deref is false, (obj, buf, sz) is the object that is + * pointed at by the ref itself; otherwise it is the object the + * ref (which is a tag) refers to. + */ +static void grab_values(struct atom_value *val, int deref, struct object *obj, void *buf, unsigned long sz) +{ + grab_common_values(val, deref, obj, buf, sz); + switch (obj->type) { + case OBJ_TAG: + grab_tag_values(val, deref, obj, buf, sz); + grab_sub_body_contents(val, deref, obj, buf, sz); + grab_person("tagger", val, deref, obj, buf, sz); + break; + case OBJ_COMMIT: + grab_commit_values(val, deref, obj, buf, sz); + grab_sub_body_contents(val, deref, obj, buf, sz); + grab_person("author", val, deref, obj, buf, sz); + grab_person("committer", val, deref, obj, buf, sz); + break; + case OBJ_TREE: + // grab_tree_values(val, deref, obj, buf, sz); + break; + case OBJ_BLOB: + // grab_blob_values(val, deref, obj, buf, sz); + break; + default: + die("Eh? Object of type %d?", obj->type); + } +} + +/* + * Parse the object referred by ref, and grab needed value. + */ +static void populate_value(struct refinfo *ref) +{ + void *buf; + struct object *obj; + int eaten, i; + unsigned long size; + const unsigned char *tagged; + + ref->value = xcalloc(sizeof(struct atom_value), used_atom_cnt); + + buf = get_obj(ref->objectname, &obj, &size, &eaten); + if (!buf) + die("missing object %s for %s", + sha1_to_hex(ref->objectname), ref->refname); + if (!obj) + die("parse_object_buffer failed on %s for %s", + sha1_to_hex(ref->objectname), ref->refname); + + /* Fill in specials first */ + for (i = 0; i < used_atom_cnt; i++) { + const char *name = used_atom[i]; + struct atom_value *v = &ref->value[i]; + if (!strcmp(name, "refname")) + v->s = ref->refname; + else if (!strcmp(name, "*refname")) { + int len = strlen(ref->refname); + char *s = xmalloc(len + 4); + sprintf(s, "%s^{}", ref->refname); + v->s = s; + } + } + + grab_values(ref->value, 0, obj, buf, size); + if (!eaten) + free(buf); + + /* If there is no atom that wants to know about tagged + * object, we are done. + */ + if (!need_tagged || (obj->type != OBJ_TAG)) + return; + + /* If it is a tag object, see if we use a value that derefs + * the object, and if we do grab the object it refers to. + */ + tagged = ((struct tag *)obj)->tagged->sha1; + + /* NEEDSWORK: This derefs tag only once, which + * is good to deal with chains of trust, but + * is not consistent with what deref_tag() does + * which peels the onion to the core. + */ + buf = get_obj(tagged, &obj, &size, &eaten); + if (!buf) + die("missing object %s for %s", + sha1_to_hex(tagged), ref->refname); + if (!obj) + die("parse_object_buffer failed on %s for %s", + sha1_to_hex(tagged), ref->refname); + grab_values(ref->value, 1, obj, buf, size); + if (!eaten) + free(buf); +} + +/* + * Given a ref, return the value for the atom. This lazily gets value + * out of the object by calling populate value. + */ +static void get_value(struct refinfo *ref, int atom, struct atom_value **v) +{ + if (!ref->value) { + populate_value(ref); + fill_missing_values(ref->value); + } + *v = &ref->value[atom]; +} + +struct grab_ref_cbdata { + struct refinfo **grab_array; + const char **grab_pattern; + int grab_cnt; +}; + +/* + * A call-back given to for_each_ref(). It is unfortunate that we + * need to use global variables to pass extra information to this + * function. + */ +static int grab_single_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + struct grab_ref_cbdata *cb = cb_data; + struct refinfo *ref; + int cnt; + + if (*cb->grab_pattern) { + const char **pattern; + int namelen = strlen(refname); + for (pattern = cb->grab_pattern; *pattern; pattern++) { + const char *p = *pattern; + int plen = strlen(p); + + if ((plen <= namelen) && + !strncmp(refname, p, plen) && + (refname[plen] == '\0' || + refname[plen] == '/')) + break; + if (!fnmatch(p, refname, FNM_PATHNAME)) + break; + } + if (!*pattern) + return 0; + } + + /* We do not open the object yet; sort may only need refname + * to do its job and the resulting list may yet to be pruned + * by maxcount logic. + */ + ref = xcalloc(1, sizeof(*ref)); + ref->refname = xstrdup(refname); + hashcpy(ref->objectname, sha1); + + cnt = cb->grab_cnt; + cb->grab_array = xrealloc(cb->grab_array, + sizeof(*cb->grab_array) * (cnt + 1)); + cb->grab_array[cnt++] = ref; + cb->grab_cnt = cnt; + return 0; +} + +static int cmp_ref_sort(struct ref_sort *s, struct refinfo *a, struct refinfo *b) +{ + struct atom_value *va, *vb; + int cmp; + cmp_type cmp_type = used_atom_type[s->atom]; + + get_value(a, s->atom, &va); + get_value(b, s->atom, &vb); + switch (cmp_type) { + case FIELD_STR: + cmp = strcmp(va->s, vb->s); + break; + default: + if (va->ul < vb->ul) + cmp = -1; + else if (va->ul == vb->ul) + cmp = 0; + else + cmp = 1; + break; + } + return (s->reverse) ? -cmp : cmp; +} + +static struct ref_sort *ref_sort; +static int compare_refs(const void *a_, const void *b_) +{ + struct refinfo *a = *((struct refinfo **)a_); + struct refinfo *b = *((struct refinfo **)b_); + struct ref_sort *s; + + for (s = ref_sort; s; s = s->next) { + int cmp = cmp_ref_sort(s, a, b); + if (cmp) + return cmp; + } + return 0; +} + +static void sort_refs(struct ref_sort *sort, struct refinfo **refs, int num_refs) +{ + ref_sort = sort; + qsort(refs, num_refs, sizeof(struct refinfo *), compare_refs); +} + +static void print_value(struct refinfo *ref, int atom, int quote_style) +{ + struct atom_value *v; + get_value(ref, atom, &v); + switch (quote_style) { + case QUOTE_NONE: + fputs(v->s, stdout); + break; + case QUOTE_SHELL: + sq_quote_print(stdout, v->s); + break; + case QUOTE_PERL: + perl_quote_print(stdout, v->s); + break; + case QUOTE_PYTHON: + python_quote_print(stdout, v->s); + break; + case QUOTE_TCL: + tcl_quote_print(stdout, v->s); + break; + } +} + +static int hex1(char ch) +{ + if ('0' <= ch && ch <= '9') + return ch - '0'; + else if ('a' <= ch && ch <= 'f') + return ch - 'a' + 10; + else if ('A' <= ch && ch <= 'F') + return ch - 'A' + 10; + return -1; +} +static int hex2(const char *cp) +{ + if (cp[0] && cp[1]) + return (hex1(cp[0]) << 4) | hex1(cp[1]); + else + return -1; +} + +static void emit(const char *cp, const char *ep) +{ + while (*cp && (!ep || cp < ep)) { + if (*cp == '%') { + if (cp[1] == '%') + cp++; + else { + int ch = hex2(cp + 1); + if (0 <= ch) { + putchar(ch); + cp += 3; + continue; + } + } + } + putchar(*cp); + cp++; + } +} + +static void show_ref(struct refinfo *info, const char *format, int quote_style) +{ + const char *cp, *sp, *ep; + + for (cp = format; *cp && (sp = find_next(cp)); cp = ep + 1) { + ep = strchr(sp, ')'); + if (cp < sp) + emit(cp, sp); + print_value(info, parse_atom(sp + 2, ep), quote_style); + } + if (*cp) { + sp = cp + strlen(cp); + emit(cp, sp); + } + putchar('\n'); +} + +static struct ref_sort *default_sort(void) +{ + static const char cstr_name[] = "refname"; + + struct ref_sort *sort = xcalloc(1, sizeof(*sort)); + + sort->next = NULL; + sort->atom = parse_atom(cstr_name, cstr_name + strlen(cstr_name)); + return sort; +} + +int cmd_for_each_ref(int ac, const char **av, char *prefix) +{ + int i, num_refs; + const char *format = NULL; + struct ref_sort *sort = NULL, **sort_tail = &sort; + int maxcount = 0; + int quote_style = -1; /* unspecified yet */ + struct refinfo **refs; + struct grab_ref_cbdata cbdata; + + for (i = 1; i < ac; i++) { + const char *arg = av[i]; + if (arg[0] != '-') + break; + if (!strcmp(arg, "--")) { + i++; + break; + } + if (!strncmp(arg, "--format=", 9)) { + if (format) + die("more than one --format?"); + format = arg + 9; + continue; + } + if (!strcmp(arg, "-s") || !strcmp(arg, "--shell") ) { + if (0 <= quote_style) + die("more than one quoting style?"); + quote_style = QUOTE_SHELL; + continue; + } + if (!strcmp(arg, "-p") || !strcmp(arg, "--perl") ) { + if (0 <= quote_style) + die("more than one quoting style?"); + quote_style = QUOTE_PERL; + continue; + } + if (!strcmp(arg, "--python") ) { + if (0 <= quote_style) + die("more than one quoting style?"); + quote_style = QUOTE_PYTHON; + continue; + } + if (!strcmp(arg, "--tcl") ) { + if (0 <= quote_style) + die("more than one quoting style?"); + quote_style = QUOTE_TCL; + continue; + } + if (!strncmp(arg, "--count=", 8)) { + if (maxcount) + die("more than one --count?"); + maxcount = atoi(arg + 8); + if (maxcount <= 0) + die("The number %s did not parse", arg); + continue; + } + if (!strncmp(arg, "--sort=", 7)) { + struct ref_sort *s = xcalloc(1, sizeof(*s)); + int len; + + s->next = NULL; + *sort_tail = s; + sort_tail = &s->next; + + arg += 7; + if (*arg == '-') { + s->reverse = 1; + arg++; + } + len = strlen(arg); + sort->atom = parse_atom(arg, arg+len); + continue; + } + break; + } + if (quote_style < 0) + quote_style = QUOTE_NONE; + + if (!sort) + sort = default_sort(); + sort_atom_limit = used_atom_cnt; + if (!format) + format = "%(objectname) %(objecttype)\t%(refname)"; + + verify_format(format); + + memset(&cbdata, 0, sizeof(cbdata)); + cbdata.grab_pattern = av + i; + for_each_ref(grab_single_ref, &cbdata); + refs = cbdata.grab_array; + num_refs = cbdata.grab_cnt; + + for (i = 0; i < used_atom_cnt; i++) { + if (used_atom[i][0] == '*') { + need_tagged = 1; + break; + } + } + + sort_refs(sort, refs, num_refs); + + if (!maxcount || num_refs < maxcount) + maxcount = num_refs; + for (i = 0; i < maxcount; i++) + show_ref(refs[i], format, quote_style); + return 0; +} diff --git a/builtin-fsck.c b/builtin-fsck.c new file mode 100644 index 0000000000..6da3814d59 --- /dev/null +++ b/builtin-fsck.c @@ -0,0 +1,694 @@ +#include "cache.h" +#include "commit.h" +#include "tree.h" +#include "blob.h" +#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; +static int show_tags; +static int show_unreachable; +static int check_full; +static int check_strict; +static int keep_cache_objects; +static unsigned char head_sha1[20]; + +#ifdef NO_D_INO_IN_DIRENT +#define SORT_DIRENT 0 +#define DIRENT_SORT_HINT(de) 0 +#else +#define SORT_DIRENT 1 +#define DIRENT_SORT_HINT(de) ((de)->d_ino) +#endif + +static void objreport(struct object *obj, const char *severity, + const char *err, va_list params) +{ + fprintf(stderr, "%s in %s %s: ", + severity, typename(obj->type), sha1_to_hex(obj->sha1)); + vfprintf(stderr, err, params); + fputs("\n", stderr); +} + +static int objerror(struct object *obj, const char *err, ...) +{ + va_list params; + va_start(params, err); + objreport(obj, "error", err, params); + va_end(params); + return -1; +} + +static int objwarning(struct object *obj, const char *err, ...) +{ + va_list params; + va_start(params, err); + objreport(obj, "warning", err, params); + va_end(params); + return -1; +} + +/* + * Check a single reachable object + */ +static void check_reachable_object(struct object *obj) +{ + const struct object_refs *refs; + + /* + * We obviously want the object to be parsed, + * except if it was in a pack-file and we didn't + * do a full fsck + */ + if (!obj->parsed) { + if (has_sha1_file(obj->sha1)) + return; /* it is in pack - forget about it */ + printf("missing %s %s\n", typename(obj->type), sha1_to_hex(obj->sha1)); + return; + } + + /* + * Check that everything that we try to reference is also good. + */ + refs = lookup_object_refs(obj); + if (refs) { + unsigned j; + for (j = 0; j < refs->count; j++) { + struct object *ref = refs->ref[j]; + if (ref->parsed || + (has_sha1_file(ref->sha1))) + continue; + printf("broken link from %7s %s\n", + typename(obj->type), sha1_to_hex(obj->sha1)); + printf(" to %7s %s\n", + typename(ref->type), sha1_to_hex(ref->sha1)); + } + } +} + +/* + * Check a single unreachable object + */ +static void check_unreachable_object(struct object *obj) +{ + /* + * Missing unreachable object? Ignore it. It's not like + * we miss it (since it can't be reached), nor do we want + * to complain about it being unreachable (since it does + * not exist). + */ + if (!obj->parsed) + return; + + /* + * Unreachable object that exists? Show it if asked to, + * since this is something that is prunable. + */ + if (show_unreachable) { + printf("unreachable %s %s\n", typename(obj->type), sha1_to_hex(obj->sha1)); + return; + } + + /* + * "!used" means that nothing at all points to it, including + * other unreachable objects. In other words, it's the "tip" + * of some set of unreachable objects, usually a commit that + * got dropped. + * + * Such starting points are more interesting than some random + * set of unreachable objects, so we show them even if the user + * hasn't asked for _all_ unreachable objects. If you have + * deleted a branch by mistake, this is a prime candidate to + * start looking at, for example. + */ + if (!obj->used) { + printf("dangling %s %s\n", typename(obj->type), + sha1_to_hex(obj->sha1)); + return; + } + + /* + * Otherwise? It's there, it's unreachable, and some other unreachable + * object points to it. Ignore it - it's not interesting, and we showed + * all the interesting cases above. + */ +} + +static void check_object(struct object *obj) +{ + if (obj->flags & REACHABLE) + check_reachable_object(obj); + else + check_unreachable_object(obj); +} + +static void check_connectivity(void) +{ + int i, max; + + /* Look up all the requirements, warn about missing objects.. */ + max = get_max_object_index(); + for (i = 0; i < max; i++) { + struct object *obj = get_indexed_object(i); + + if (obj) + check_object(obj); + } +} + +/* + * The entries in a tree are ordered in the _path_ order, + * which means that a directory entry is ordered by adding + * a slash to the end of it. + * + * So a directory called "a" is ordered _after_ a file + * called "a.c", because "a/" sorts after "a.c". + */ +#define TREE_UNORDERED (-1) +#define TREE_HAS_DUPS (-2) + +static int verify_ordered(unsigned mode1, const char *name1, unsigned mode2, const char *name2) +{ + int len1 = strlen(name1); + int len2 = strlen(name2); + int len = len1 < len2 ? len1 : len2; + unsigned char c1, c2; + int cmp; + + cmp = memcmp(name1, name2, len); + if (cmp < 0) + return 0; + if (cmp > 0) + return TREE_UNORDERED; + + /* + * Ok, the first <len> characters are the same. + * Now we need to order the next one, but turn + * a '\0' into a '/' for a directory entry. + */ + c1 = name1[len]; + c2 = name2[len]; + if (!c1 && !c2) + /* + * git-write-tree used to write out a nonsense tree that has + * entries with the same name, one blob and one tree. Make + * sure we do not have duplicate entries. + */ + return TREE_HAS_DUPS; + if (!c1 && S_ISDIR(mode1)) + c1 = '/'; + if (!c2 && S_ISDIR(mode2)) + c2 = '/'; + return c1 < c2 ? 0 : TREE_UNORDERED; +} + +static int fsck_tree(struct tree *item) +{ + int retval; + int has_full_path = 0; + int has_zero_pad = 0; + int has_bad_modes = 0; + int has_dup_entries = 0; + int not_properly_sorted = 0; + struct tree_desc desc; + unsigned o_mode; + const char *o_name; + const unsigned char *o_sha1; + + 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 |= *(char *)desc.buf == '0'; + update_tree_entry(&desc); + + switch (mode) { + /* + * Standard modes.. + */ + case S_IFREG | 0755: + case S_IFREG | 0644: + case S_IFLNK: + case S_IFDIR: + break; + /* + * This is nonstandard, but we had a few of these + * early on when we honored the full set of mode + * bits.. + */ + case S_IFREG | 0664: + if (!check_strict) + break; + default: + has_bad_modes = 1; + } + + if (o_name) { + switch (verify_ordered(o_mode, o_name, mode, name)) { + case TREE_UNORDERED: + not_properly_sorted = 1; + break; + case TREE_HAS_DUPS: + has_dup_entries = 1; + break; + default: + break; + } + } + + o_mode = mode; + o_name = name; + o_sha1 = sha1; + } + free(item->buffer); + item->buffer = NULL; + + retval = 0; + if (has_full_path) { + objwarning(&item->object, "contains full pathnames"); + } + if (has_zero_pad) { + objwarning(&item->object, "contains zero-padded file modes"); + } + if (has_bad_modes) { + objwarning(&item->object, "contains bad file modes"); + } + if (has_dup_entries) { + retval = objerror(&item->object, "contains duplicate file entries"); + } + if (not_properly_sorted) { + retval = objerror(&item->object, "not properly sorted"); + } + return retval; +} + +static int fsck_commit(struct commit *commit) +{ + char *buffer = commit->buffer; + unsigned char tree_sha1[20], sha1[20]; + + if (memcmp(buffer, "tree ", 5)) + return objerror(&commit->object, "invalid format - expected 'tree' line"); + if (get_sha1_hex(buffer+5, tree_sha1) || buffer[45] != '\n') + return objerror(&commit->object, "invalid 'tree' line format - bad sha1"); + buffer += 46; + while (!memcmp(buffer, "parent ", 7)) { + if (get_sha1_hex(buffer+7, sha1) || buffer[47] != '\n') + return objerror(&commit->object, "invalid 'parent' line format - bad sha1"); + buffer += 48; + } + if (memcmp(buffer, "author ", 7)) + return objerror(&commit->object, "invalid format - expected 'author' line"); + free(commit->buffer); + commit->buffer = NULL; + if (!commit->tree) + return objerror(&commit->object, "could not load commit's tree %s", tree_sha1); + if (!commit->parents && show_root) + printf("root %s\n", sha1_to_hex(commit->object.sha1)); + if (!commit->date) + printf("bad commit date in %s\n", + sha1_to_hex(commit->object.sha1)); + return 0; +} + +static int fsck_tag(struct tag *tag) +{ + struct object *tagged = tag->tagged; + + if (!tagged) { + return objerror(&tag->object, "could not load tagged object"); + } + if (!show_tags) + return 0; + + printf("tagged %s %s", typename(tagged->type), sha1_to_hex(tagged->sha1)); + printf(" (%s) in %s\n", tag->tag, sha1_to_hex(tag->object.sha1)); + return 0; +} + +static int fsck_sha1(unsigned char *sha1) +{ + struct object *obj = parse_object(sha1); + if (!obj) + return error("%s: object corrupt or missing", sha1_to_hex(sha1)); + if (obj->flags & SEEN) + return 0; + obj->flags |= SEEN; + if (obj->type == OBJ_BLOB) + return 0; + if (obj->type == OBJ_TREE) + return fsck_tree((struct tree *) obj); + if (obj->type == OBJ_COMMIT) + return fsck_commit((struct commit *) obj); + if (obj->type == OBJ_TAG) + return fsck_tag((struct tag *) obj); + /* By now, parse_object() would've returned NULL instead. */ + return objerror(obj, "unknown type '%d' (internal fsck error)", obj->type); +} + +/* + * This is the sorting chunk size: make it reasonably + * big so that we can sort well.. + */ +#define MAX_SHA1_ENTRIES (1024) + +struct sha1_entry { + unsigned long ino; + unsigned char sha1[20]; +}; + +static struct { + unsigned long nr; + struct sha1_entry *entry[MAX_SHA1_ENTRIES]; +} sha1_list; + +static int ino_compare(const void *_a, const void *_b) +{ + const struct sha1_entry *a = _a, *b = _b; + unsigned long ino1 = a->ino, ino2 = b->ino; + return ino1 < ino2 ? -1 : ino1 > ino2 ? 1 : 0; +} + +static void fsck_sha1_list(void) +{ + int i, nr = sha1_list.nr; + + if (SORT_DIRENT) + qsort(sha1_list.entry, nr, + sizeof(struct sha1_entry *), ino_compare); + for (i = 0; i < nr; i++) { + struct sha1_entry *entry = sha1_list.entry[i]; + unsigned char *sha1 = entry->sha1; + + sha1_list.entry[i] = NULL; + fsck_sha1(sha1); + free(entry); + } + sha1_list.nr = 0; +} + +static void add_sha1_list(unsigned char *sha1, unsigned long ino) +{ + struct sha1_entry *entry = xmalloc(sizeof(*entry)); + int nr; + + entry->ino = ino; + hashcpy(entry->sha1, sha1); + nr = sha1_list.nr; + if (nr == MAX_SHA1_ENTRIES) { + fsck_sha1_list(); + nr = 0; + } + sha1_list.entry[nr] = entry; + sha1_list.nr = ++nr; +} + +static void fsck_dir(int i, char *path) +{ + DIR *dir = opendir(path); + struct dirent *de; + + if (!dir) + return; + + while ((de = readdir(dir)) != NULL) { + char name[100]; + unsigned char sha1[20]; + int len = strlen(de->d_name); + + switch (len) { + case 2: + if (de->d_name[1] != '.') + break; + case 1: + if (de->d_name[0] != '.') + break; + continue; + case 38: + sprintf(name, "%02x", i); + memcpy(name+2, de->d_name, len+1); + if (get_sha1_hex(name, sha1) < 0) + break; + add_sha1_list(sha1, DIRENT_SORT_HINT(de)); + continue; + } + fprintf(stderr, "bad sha1 file: %s/%s\n", path, de->d_name); + } + closedir(dir); +} + +static int default_refs; + +static int fsck_handle_reflog_ent(unsigned char *osha1, unsigned char *nsha1, + const char *email, unsigned long timestamp, int tz, + const char *message, void *cb_data) +{ + struct object *obj; + + if (!is_null_sha1(osha1)) { + obj = lookup_object(osha1); + if (obj) { + obj->used = 1; + mark_reachable(obj, REACHABLE); + } + } + obj = lookup_object(nsha1); + if (obj) { + obj->used = 1; + mark_reachable(obj, REACHABLE); + } + return 0; +} + +static int fsck_handle_reflog(const char *logname, const unsigned char *sha1, int flag, void *cb_data) +{ + for_each_reflog_ent(logname, fsck_handle_reflog_ent, NULL); + return 0; +} + +static int fsck_handle_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + struct object *obj; + + obj = lookup_object(sha1); + if (!obj) { + if (has_sha1_file(sha1)) { + default_refs++; + return 0; /* it is in a pack */ + } + error("%s: invalid sha1 pointer %s", refname, sha1_to_hex(sha1)); + /* We'll continue with the rest despite the error.. */ + return 0; + } + default_refs++; + obj->used = 1; + mark_reachable(obj, REACHABLE); + + return 0; +} + +static void get_default_heads(void) +{ + for_each_ref(fsck_handle_ref, NULL); + for_each_reflog(fsck_handle_reflog, NULL); + + /* + * Not having any default heads isn't really fatal, but + * it does mean that "--unreachable" no longer makes any + * sense (since in this case everything will obviously + * be unreachable by definition. + * + * Showing dangling objects is valid, though (as those + * dangling objects are likely lost heads). + * + * So we just print a warning about it, and clear the + * "show_unreachable" flag. + */ + if (!default_refs) { + error("No default references"); + show_unreachable = 0; + } +} + +static void fsck_object_dir(const char *path) +{ + int i; + for (i = 0; i < 256; i++) { + static char dir[4096]; + sprintf(dir, "%s/%02x", path, i); + fsck_dir(i, dir); + } + fsck_sha1_list(); +} + +static int fsck_head_link(void) +{ + unsigned char sha1[20]; + int flag; + const char *head_points_at = resolve_ref("HEAD", sha1, 1, &flag); + + if (!head_points_at || !(flag & REF_ISSYMREF)) + return error("HEAD is not a symbolic ref"); + if (strncmp(head_points_at, "refs/heads/", 11)) + return error("HEAD points to something strange (%s)", + head_points_at); + if (is_null_sha1(sha1)) + return error("HEAD: not a valid git pointer"); + 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 != OBJ_TREE) + 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 cmd_fsck(int argc, char **argv, const char *prefix) +{ + int i, heads; + + track_object_refs = 1; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (!strcmp(arg, "--unreachable")) { + show_unreachable = 1; + continue; + } + if (!strcmp(arg, "--tags")) { + show_tags = 1; + continue; + } + if (!strcmp(arg, "--root")) { + show_root = 1; + continue; + } + if (!strcmp(arg, "--cache")) { + keep_cache_objects = 1; + continue; + } + if (!strcmp(arg, "--full")) { + check_full = 1; + continue; + } + if (!strcmp(arg, "--strict")) { + check_strict = 1; + continue; + } + if (*arg == '-') + usage("git-fsck [--tags] [--root] [[--unreachable] [--cache] [--full] [--strict] <head-sha1>*]"); + } + + fsck_head_link(); + fsck_object_dir(get_object_directory()); + if (check_full) { + struct alternate_object_database *alt; + struct packed_git *p; + prepare_alt_odb(); + for (alt = alt_odb_list; alt; alt = alt->next) { + char namebuf[PATH_MAX]; + int namelen = alt->name - alt->base; + memcpy(namebuf, alt->base, namelen); + namebuf[namelen - 1] = 0; + fsck_object_dir(namebuf); + } + prepare_packed_git(); + for (p = packed_git; p; p = p->next) + /* verify gives error messages itself */ + verify_pack(p, 0); + + for (p = packed_git; p; p = p->next) { + int num = num_packed_objects(p); + for (i = 0; i < num; i++) { + unsigned char sha1[20]; + nth_packed_object_sha1(p, i, sha1); + fsck_sha1(sha1); + } + } + } + + heads = 0; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg == '-') + continue; + + if (!get_sha1(arg, head_sha1)) { + struct object *obj = lookup_object(head_sha1); + + /* Error is printed by lookup_object(). */ + if (!obj) + continue; + + obj->used = 1; + mark_reachable(obj, REACHABLE); + heads++; + continue; + } + error("invalid parameter: expected sha1, got '%s'", arg); + } + + /* + * If we've not been given any explicit head information, do the + * default ones from .git/refs. We also consider the index file + * in this case (ie this implies --cache). + */ + if (!heads) { + get_default_heads(); + keep_cache_objects = 1; + } + + if (keep_cache_objects) { + int i; + read_cache(); + for (i = 0; i < active_nr; i++) { + struct blob *blob = lookup_blob(active_cache[i]->sha1); + struct object *obj; + if (!blob) + continue; + obj = &blob->object; + obj->used = 1; + mark_reachable(obj, REACHABLE); + } + if (active_cache_tree) + fsck_cache_tree(active_cache_tree); + } + + check_connectivity(); + return 0; +} diff --git a/builtin-grep.c b/builtin-grep.c new file mode 100644 index 0000000000..2bfbdb7140 --- /dev/null +++ b/builtin-grep.c @@ -0,0 +1,718 @@ +/* + * Builtin "git grep" + * + * Copyright (c) 2006 Junio C Hamano + */ +#include "cache.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "tag.h" +#include "tree-walk.h" +#include "builtin.h" +#include "grep.h" + +/* + * git grep pathspecs are somewhat different from diff-tree pathspecs; + * pathname wildcards are allowed. + */ +static int pathspec_matches(const char **paths, const char *name) +{ + int namelen, i; + if (!paths || !*paths) + return 1; + namelen = strlen(name); + for (i = 0; paths[i]; i++) { + const char *match = paths[i]; + int matchlen = strlen(match); + const char *cp, *meta; + + if (!matchlen || + ((matchlen <= namelen) && + !strncmp(name, match, matchlen) && + (match[matchlen-1] == '/' || + name[matchlen] == '\0' || name[matchlen] == '/'))) + return 1; + if (!fnmatch(match, name, 0)) + return 1; + if (name[namelen-1] != '/') + continue; + + /* We are being asked if the directory ("name") is worth + * descending into. + * + * Find the longest leading directory name that does + * not have metacharacter in the pathspec; the name + * we are looking at must overlap with that directory. + */ + for (cp = match, meta = NULL; cp - match < matchlen; cp++) { + char ch = *cp; + if (ch == '*' || ch == '[' || ch == '?') { + meta = cp; + break; + } + } + if (!meta) + meta = cp; /* fully literal */ + + if (namelen <= meta - match) { + /* Looking at "Documentation/" and + * the pattern says "Documentation/howto/", or + * "Documentation/diff*.txt". The name we + * have should match prefix. + */ + if (!memcmp(match, name, namelen)) + return 1; + continue; + } + + if (meta - match < namelen) { + /* Looking at "Documentation/howto/" and + * the pattern says "Documentation/h*"; + * match up to "Do.../h"; this avoids descending + * into "Documentation/technical/". + */ + if (!memcmp(match, name, meta - match)) + return 1; + continue; + } + } + return 0; +} + +static int grep_sha1(struct grep_opt *opt, const unsigned char *sha1, const char *name, int tree_name_len) +{ + unsigned long size; + char *data; + char type[20]; + char *to_free = NULL; + int hit; + + data = read_sha1_file(sha1, type, &size); + if (!data) { + error("'%s': unable to read %s", name, sha1_to_hex(sha1)); + return 0; + } + if (opt->relative && opt->prefix_length) { + static char name_buf[PATH_MAX]; + char *cp; + int name_len = strlen(name) - opt->prefix_length + 1; + + if (!tree_name_len) + name += opt->prefix_length; + else { + if (ARRAY_SIZE(name_buf) <= name_len) + cp = to_free = xmalloc(name_len); + else + cp = name_buf; + memcpy(cp, name, tree_name_len); + strcpy(cp + tree_name_len, + name + tree_name_len + opt->prefix_length); + name = cp; + } + } + hit = grep_buffer(opt, name, data, size); + free(data); + free(to_free); + return hit; +} + +static int grep_file(struct grep_opt *opt, const char *filename) +{ + struct stat st; + int i; + char *data; + if (lstat(filename, &st) < 0) { + err_ret: + if (errno != ENOENT) + error("'%s': %s", filename, strerror(errno)); + return 0; + } + if (!st.st_size) + return 0; /* empty file -- no grep hit */ + if (!S_ISREG(st.st_mode)) + return 0; + i = open(filename, O_RDONLY); + if (i < 0) + goto err_ret; + data = xmalloc(st.st_size + 1); + if (st.st_size != read_in_full(i, data, st.st_size)) { + error("'%s': short read %s", filename, strerror(errno)); + close(i); + free(data); + return 0; + } + close(i); + if (opt->relative && opt->prefix_length) + filename += opt->prefix_length; + i = grep_buffer(opt, filename, data, st.st_size); + free(data); + return i; +} + +static int exec_grep(int argc, const char **argv) +{ + pid_t pid; + int status; + + argv[argc] = NULL; + pid = fork(); + if (pid < 0) + return pid; + if (!pid) { + execvp("grep", (char **) argv); + exit(255); + } + while (waitpid(pid, &status, 0) < 0) { + if (errno == EINTR) + continue; + return -1; + } + if (WIFEXITED(status)) { + if (!WEXITSTATUS(status)) + return 1; + return 0; + } + return -1; +} + +#define MAXARGS 1000 +#define ARGBUF 4096 +#define push_arg(a) do { \ + if (nr < MAXARGS) argv[nr++] = (a); \ + else die("maximum number of args exceeded"); \ + } while (0) + +static int external_grep(struct grep_opt *opt, const char **paths, int cached) +{ + int i, nr, argc, hit, len, status; + const char *argv[MAXARGS+1]; + char randarg[ARGBUF]; + char *argptr = randarg; + struct grep_pat *p; + + if (opt->extended || (opt->relative && opt->prefix_length)) + return -1; + len = nr = 0; + push_arg("grep"); + if (opt->fixed) + push_arg("-F"); + if (opt->linenum) + push_arg("-n"); + if (!opt->pathname) + push_arg("-h"); + 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) + push_arg("-l"); + if (opt->unmatch_name_only) + push_arg("-L"); + if (opt->count) + push_arg("-c"); + if (opt->post_context || opt->pre_context) { + if (opt->post_context != opt->pre_context) { + if (opt->pre_context) { + push_arg("-B"); + len += snprintf(argptr, sizeof(randarg)-len, + "%u", opt->pre_context); + if (sizeof(randarg) <= len) + die("maximum length of args exceeded"); + push_arg(argptr); + argptr += len; + } + if (opt->post_context) { + push_arg("-A"); + len += snprintf(argptr, sizeof(randarg)-len, + "%u", opt->post_context); + if (sizeof(randarg) <= len) + die("maximum length of args exceeded"); + push_arg(argptr); + argptr += len; + } + } + else { + push_arg("-C"); + len += snprintf(argptr, sizeof(randarg)-len, + "%u", opt->post_context); + if (sizeof(randarg) <= len) + die("maximum length of args exceeded"); + push_arg(argptr); + argptr += len; + } + } + for (p = opt->pattern_list; p; p = p->next) { + push_arg("-e"); + push_arg(p->pattern); + } + + /* + * To make sure we get the header printed out when we want it, + * add /dev/null to the paths to grep. This is unnecessary + * (and wrong) with "-l" or "-L", which always print out the + * name anyway. + * + * GNU grep has "-H", but this is portable. + */ + if (!opt->name_only && !opt->unmatch_name_only) + push_arg("/dev/null"); + + hit = 0; + argc = nr; + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + char *name; + if (!S_ISREG(ntohl(ce->ce_mode))) + continue; + if (!pathspec_matches(paths, ce->name)) + continue; + name = ce->name; + if (name[0] == '-') { + int len = ce_namelen(ce); + name = xmalloc(len + 3); + memcpy(name, "./", 2); + memcpy(name + 2, ce->name, len + 1); + } + argv[argc++] = name; + if (argc < MAXARGS && !ce_stage(ce)) + continue; + status = exec_grep(argc, argv); + if (0 < status) + hit = 1; + argc = nr; + if (ce_stage(ce)) { + do { + i++; + } while (i < active_nr && + !strcmp(ce->name, active_cache[i]->name)); + i--; /* compensate for loop control */ + } + } + if (argc > nr) { + status = exec_grep(argc, argv); + if (0 < status) + hit = 1; + } + return hit; +} + +static int grep_cache(struct grep_opt *opt, const char **paths, int cached) +{ + int hit = 0; + int nr; + read_cache(); + +#ifdef __unix__ + /* + * Use the external "grep" command for the case where + * we grep through the checked-out files. It tends to + * be a lot more optimized + */ + if (!cached) { + hit = external_grep(opt, paths, cached); + if (hit >= 0) + return hit; + } +#endif + + for (nr = 0; nr < active_nr; nr++) { + struct cache_entry *ce = active_cache[nr]; + if (!S_ISREG(ntohl(ce->ce_mode))) + continue; + if (!pathspec_matches(paths, ce->name)) + continue; + if (cached) { + if (ce_stage(ce)) + continue; + hit |= grep_sha1(opt, ce->sha1, ce->name, 0); + } + else + hit |= grep_file(opt, ce->name); + if (ce_stage(ce)) { + do { + nr++; + } while (nr < active_nr && + !strcmp(ce->name, active_cache[nr]->name)); + nr--; /* compensate for loop control */ + } + } + free_grep_patterns(opt); + return hit; +} + +static int grep_tree(struct grep_opt *opt, const char **paths, + struct tree_desc *tree, + const char *tree_name, const char *base) +{ + int len; + int hit = 0; + struct name_entry entry; + char *down; + int tn_len = strlen(tree_name); + char *path_buf = xmalloc(PATH_MAX + tn_len + 100); + + if (tn_len) { + tn_len = sprintf(path_buf, "%s:", tree_name); + down = path_buf + tn_len; + strcat(down, base); + } + else { + down = path_buf; + strcpy(down, base); + } + len = strlen(path_buf); + + while (tree_entry(tree, &entry)) { + strcpy(path_buf + len, entry.path); + + if (S_ISDIR(entry.mode)) + /* Match "abc/" against pathspec to + * decide if we want to descend into "abc" + * directory. + */ + strcpy(path_buf + len + entry.pathlen, "/"); + + if (!pathspec_matches(paths, down)) + ; + else if (S_ISREG(entry.mode)) + hit |= grep_sha1(opt, entry.sha1, path_buf, tn_len); + else if (S_ISDIR(entry.mode)) { + char type[20]; + struct tree_desc sub; + void *data; + data = read_sha1_file(entry.sha1, type, &sub.size); + if (!data) + die("unable to read tree (%s)", + sha1_to_hex(entry.sha1)); + sub.buf = data; + hit |= grep_tree(opt, paths, &sub, tree_name, down); + free(data); + } + } + return hit; +} + +static int grep_object(struct grep_opt *opt, const char **paths, + struct object *obj, const char *name) +{ + if (obj->type == OBJ_BLOB) + return grep_sha1(opt, obj->sha1, name, 0); + if (obj->type == OBJ_COMMIT || obj->type == OBJ_TREE) { + struct tree_desc tree; + void *data; + int hit; + data = read_object_with_reference(obj->sha1, tree_type, + &tree.size, NULL); + if (!data) + die("unable to read tree (%s)", sha1_to_hex(obj->sha1)); + tree.buf = data; + hit = grep_tree(opt, paths, &tree, name, ""); + free(data); + return hit; + } + die("unable to grep from object of type %s", typename(obj->type)); +} + +static const char builtin_grep_usage[] = +"git-grep <option>* <rev>* [-e] <pattern> [<path>...]"; + +static const char emsg_invalid_context_len[] = +"%s: invalid context length argument"; +static const char emsg_missing_context_len[] = +"missing context length argument"; +static const char emsg_missing_argument[] = +"option requires an argument -%s"; + +int cmd_grep(int argc, const char **argv, const char *prefix) +{ + int hit = 0; + int cached = 0; + int seen_dashdash = 0; + struct grep_opt opt; + struct object_array list = { 0, 0, NULL }; + const char **paths = NULL; + int i; + + memset(&opt, 0, sizeof(opt)); + opt.prefix_length = (prefix && *prefix) ? strlen(prefix) : 0; + opt.relative = 1; + opt.pathname = 1; + opt.pattern_tail = &opt.pattern_list; + opt.regflags = REG_NEWLINE; + + /* + * If there is no -- then the paths must exist in the working + * tree. If there is no explicit pattern specified with -e or + * -f, we take the first unrecognized non option to be the + * pattern, but then what follows it must be zero or more + * valid refs up to the -- (if exists), and then existing + * paths. If there is an explicit pattern, then the first + * unrecognized non option is the beginning of the refs list + * that continues up to the -- (if exists), and then paths. + */ + + while (1 < argc) { + const char *arg = argv[1]; + argc--; argv++; + if (!strcmp("--cached", arg)) { + cached = 1; + continue; + } + if (!strcmp("-a", arg) || + !strcmp("--text", arg)) { + opt.binary = GREP_BINARY_TEXT; + continue; + } + if (!strcmp("-i", arg) || + !strcmp("--ignore-case", arg)) { + opt.regflags |= REG_ICASE; + continue; + } + if (!strcmp("-I", arg)) { + opt.binary = GREP_BINARY_NOMATCH; + continue; + } + if (!strcmp("-v", arg) || + !strcmp("--invert-match", arg)) { + opt.invert = 1; + continue; + } + if (!strcmp("-E", arg) || + !strcmp("--extended-regexp", arg)) { + opt.regflags |= REG_EXTENDED; + continue; + } + if (!strcmp("-F", arg) || + !strcmp("--fixed-strings", arg)) { + opt.fixed = 1; + continue; + } + if (!strcmp("-G", arg) || + !strcmp("--basic-regexp", arg)) { + opt.regflags &= ~REG_EXTENDED; + continue; + } + if (!strcmp("-n", arg)) { + opt.linenum = 1; + continue; + } + if (!strcmp("-h", arg)) { + opt.pathname = 0; + continue; + } + if (!strcmp("-H", arg)) { + opt.pathname = 1; + continue; + } + if (!strcmp("-l", arg) || + !strcmp("--files-with-matches", arg)) { + opt.name_only = 1; + continue; + } + if (!strcmp("-L", arg) || + !strcmp("--files-without-match", arg)) { + opt.unmatch_name_only = 1; + continue; + } + if (!strcmp("-c", arg) || + !strcmp("--count", arg)) { + opt.count = 1; + continue; + } + if (!strcmp("-w", arg) || + !strcmp("--word-regexp", arg)) { + opt.word_regexp = 1; + continue; + } + if (!strncmp("-A", arg, 2) || + !strncmp("-B", arg, 2) || + !strncmp("-C", arg, 2) || + (arg[0] == '-' && '1' <= arg[1] && arg[1] <= '9')) { + unsigned num; + const char *scan; + switch (arg[1]) { + case 'A': case 'B': case 'C': + if (!arg[2]) { + if (argc <= 1) + die(emsg_missing_context_len); + scan = *++argv; + argc--; + } + else + scan = arg + 2; + break; + default: + scan = arg + 1; + break; + } + if (sscanf(scan, "%u", &num) != 1) + die(emsg_invalid_context_len, scan); + switch (arg[1]) { + case 'A': + opt.post_context = num; + break; + default: + case 'C': + opt.post_context = num; + case 'B': + opt.pre_context = num; + break; + } + continue; + } + if (!strcmp("-f", arg)) { + FILE *patterns; + int lno = 0; + char buf[1024]; + if (argc <= 1) + die(emsg_missing_argument, arg); + patterns = fopen(argv[1], "r"); + if (!patterns) + die("'%s': %s", argv[1], strerror(errno)); + while (fgets(buf, sizeof(buf), patterns)) { + int len = strlen(buf); + if (buf[len-1] == '\n') + buf[len-1] = 0; + /* ignore empty line like grep does */ + if (!buf[0]) + continue; + append_grep_pattern(&opt, xstrdup(buf), + argv[1], ++lno, + GREP_PATTERN); + } + fclose(patterns); + argv++; + argc--; + continue; + } + if (!strcmp("--not", arg)) { + append_grep_pattern(&opt, arg, "command line", 0, + GREP_NOT); + continue; + } + if (!strcmp("--and", arg)) { + append_grep_pattern(&opt, arg, "command line", 0, + GREP_AND); + continue; + } + if (!strcmp("--or", arg)) + continue; /* no-op */ + if (!strcmp("(", arg)) { + append_grep_pattern(&opt, arg, "command line", 0, + GREP_OPEN_PAREN); + continue; + } + if (!strcmp(")", arg)) { + append_grep_pattern(&opt, arg, "command line", 0, + GREP_CLOSE_PAREN); + continue; + } + if (!strcmp("--all-match", arg)) { + opt.all_match = 1; + continue; + } + if (!strcmp("-e", arg)) { + if (1 < argc) { + append_grep_pattern(&opt, argv[1], + "-e option", 0, + GREP_PATTERN); + argv++; + argc--; + continue; + } + die(emsg_missing_argument, arg); + } + if (!strcmp("--full-name", arg)) { + opt.relative = 0; + continue; + } + if (!strcmp("--", arg)) { + /* later processing wants to have this at argv[1] */ + argv--; + argc++; + break; + } + if (*arg == '-') + usage(builtin_grep_usage); + + /* First unrecognized non-option token */ + if (!opt.pattern_list) { + append_grep_pattern(&opt, arg, "command line", 0, + GREP_PATTERN); + break; + } + else { + /* We are looking at the first path or rev; + * it is found at argv[1] after leaving the + * loop. + */ + argc++; argv--; + break; + } + } + + if (!opt.pattern_list) + die("no pattern given."); + if ((opt.regflags != REG_NEWLINE) && opt.fixed) + die("cannot mix --fixed-strings and regexp"); + compile_grep_patterns(&opt); + + /* Check revs and then paths */ + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + unsigned char sha1[20]; + /* Is it a rev? */ + if (!get_sha1(arg, sha1)) { + struct object *object = parse_object(sha1); + if (!object) + die("bad object %s", arg); + add_object_array(object, arg, &list); + continue; + } + if (!strcmp(arg, "--")) { + i++; + seen_dashdash = 1; + } + break; + } + + /* The rest are paths */ + if (!seen_dashdash) { + int j; + for (j = i; j < argc; j++) + verify_filename(prefix, argv[j]); + } + + if (i < argc) { + paths = get_pathspec(prefix, argv + i); + if (opt.prefix_length && opt.relative) { + /* Make sure we do not get outside of paths */ + for (i = 0; paths[i]; i++) + if (strncmp(prefix, paths[i], opt.prefix_length)) + die("git-grep: cannot generate relative filenames containing '..'"); + } + } + else if (prefix) { + paths = xcalloc(2, sizeof(const char *)); + paths[0] = prefix; + paths[1] = NULL; + } + + if (!list.nr) + return !grep_cache(&opt, paths, cached); + + if (cached) + die("both --cached and trees are given."); + + for (i = 0; i < list.nr; i++) { + struct object *real_obj; + real_obj = deref_tag(list.objects[i].item, NULL, 0); + if (grep_object(&opt, paths, real_obj, list.objects[i].name)) + hit = 1; + } + free_grep_patterns(&opt); + return !hit; +} diff --git a/builtin-init-db.c b/builtin-init-db.c new file mode 100644 index 0000000000..12e43d0db4 --- /dev/null +++ b/builtin-init-db.c @@ -0,0 +1,344 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "builtin.h" + +#ifndef DEFAULT_GIT_TEMPLATE_DIR +#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates/" +#endif + +#ifdef NO_TRUSTABLE_FILEMODE +#define TEST_FILEMODE 0 +#else +#define TEST_FILEMODE 1 +#endif + +static void safe_create_dir(const char *dir, int share) +{ + if (mkdir(dir, 0777) < 0) { + if (errno != EEXIST) { + perror(dir); + exit(1); + } + } + else if (share && adjust_shared_perm(dir)) + die("Could not make %s writable by group\n", dir); +} + +static int copy_file(const char *dst, const char *src, int mode) +{ + int fdi, fdo, status; + + mode = (mode & 0111) ? 0777 : 0666; + if ((fdi = open(src, O_RDONLY)) < 0) + return fdi; + if ((fdo = open(dst, O_WRONLY | O_CREAT | O_EXCL, mode)) < 0) { + close(fdi); + return fdo; + } + status = copy_fd(fdi, fdo); + close(fdo); + + if (!status && adjust_shared_perm(dst)) + return -1; + + return status; +} + +static void copy_templates_1(char *path, int baselen, + char *template, int template_baselen, + DIR *dir) +{ + struct dirent *de; + + /* Note: if ".git/hooks" file exists in the repository being + * re-initialized, /etc/core-git/templates/hooks/update would + * cause git-init to fail here. I think this is sane but + * it means that the set of templates we ship by default, along + * with the way the namespace under .git/ is organized, should + * be really carefully chosen. + */ + safe_create_dir(path, 1); + while ((de = readdir(dir)) != NULL) { + struct stat st_git, st_template; + int namelen; + int exists = 0; + + if (de->d_name[0] == '.') + continue; + namelen = strlen(de->d_name); + if ((PATH_MAX <= baselen + namelen) || + (PATH_MAX <= template_baselen + namelen)) + die("insanely long template name %s", de->d_name); + memcpy(path + baselen, de->d_name, namelen+1); + memcpy(template + template_baselen, de->d_name, namelen+1); + if (lstat(path, &st_git)) { + if (errno != ENOENT) + die("cannot stat %s", path); + } + else + exists = 1; + + if (lstat(template, &st_template)) + die("cannot stat template %s", template); + + if (S_ISDIR(st_template.st_mode)) { + DIR *subdir = opendir(template); + int baselen_sub = baselen + namelen; + int template_baselen_sub = template_baselen + namelen; + if (!subdir) + die("cannot opendir %s", template); + path[baselen_sub++] = + template[template_baselen_sub++] = '/'; + path[baselen_sub] = + template[template_baselen_sub] = 0; + copy_templates_1(path, baselen_sub, + template, template_baselen_sub, + subdir); + closedir(subdir); + } + else if (exists) + continue; + else if (S_ISLNK(st_template.st_mode)) { + char lnk[256]; + int len; + len = readlink(template, lnk, sizeof(lnk)); + if (len < 0) + die("cannot readlink %s", template); + if (sizeof(lnk) <= len) + die("insanely long symlink %s", template); + lnk[len] = 0; + if (symlink(lnk, path)) + die("cannot symlink %s %s", lnk, path); + } + else if (S_ISREG(st_template.st_mode)) { + if (copy_file(path, template, st_template.st_mode)) + die("cannot copy %s to %s", template, path); + } + else + error("ignoring template %s", template); + } +} + +static void copy_templates(const char *git_dir, int len, const char *template_dir) +{ + char path[PATH_MAX]; + char template_path[PATH_MAX]; + int template_len; + DIR *dir; + + if (!template_dir) { + template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT); + if (!template_dir) + template_dir = DEFAULT_GIT_TEMPLATE_DIR; + } + strcpy(template_path, template_dir); + template_len = strlen(template_path); + if (template_path[template_len-1] != '/') { + template_path[template_len++] = '/'; + template_path[template_len] = 0; + } + dir = opendir(template_path); + if (!dir) { + fprintf(stderr, "warning: templates not found %s\n", + template_dir); + return; + } + + /* Make sure that template is from the correct vintage */ + strcpy(template_path + template_len, "config"); + repository_format_version = 0; + git_config_from_file(check_repository_format_version, + template_path); + template_path[template_len] = 0; + + if (repository_format_version && + repository_format_version != GIT_REPO_VERSION) { + fprintf(stderr, "warning: not copying templates of " + "a wrong format version %d from '%s'\n", + repository_format_version, + template_dir); + closedir(dir); + return; + } + + memcpy(path, git_dir, len); + path[len] = 0; + copy_templates_1(path, len, + template_path, template_len, + dir); + closedir(dir); +} + +static int create_default_files(const char *git_dir, const char *template_path) +{ + unsigned len = strlen(git_dir); + static char path[PATH_MAX]; + unsigned char sha1[20]; + struct stat st1; + char repo_version_string[10]; + int reinit; + int filemode; + + if (len > sizeof(path)-50) + die("insane git directory %s", git_dir); + memcpy(path, git_dir, len); + + if (len && path[len-1] != '/') + path[len++] = '/'; + + /* + * Create .git/refs/{heads,tags} + */ + strcpy(path + len, "refs"); + safe_create_dir(path, 1); + strcpy(path + len, "refs/heads"); + safe_create_dir(path, 1); + strcpy(path + len, "refs/tags"); + safe_create_dir(path, 1); + + /* First copy the templates -- we might have the default + * config file there, in which case we would want to read + * from it after installing. + */ + path[len] = 0; + copy_templates(path, len, template_path); + + 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. + */ + strcpy(path + len, "HEAD"); + reinit = !read_ref("HEAD", sha1); + if (!reinit) { + if (create_symref("HEAD", "refs/heads/master", NULL) < 0) + exit(1); + } + + /* This forces creation of new config file */ + sprintf(repo_version_string, "%d", GIT_REPO_VERSION); + git_config_set("core.repositoryformatversion", repo_version_string); + + path[len] = 0; + strcpy(path + len, "config"); + + /* Check filemode trustability */ + filemode = TEST_FILEMODE; + if (TEST_FILEMODE && !lstat(path, &st1)) { + struct stat st2; + filemode = (!chmod(path, st1.st_mode ^ S_IXUSR) && + !lstat(path, &st2) && + st1.st_mode != st2.st_mode); + } + git_config_set("core.filemode", filemode ? "true" : "false"); + + if (is_bare_repository()) { + git_config_set("core.bare", "true"); + } + else { + git_config_set("core.bare", "false"); + /* allow template config file to override the default */ + if (log_all_ref_updates == -1) + git_config_set("core.logallrefupdates", "true"); + } + return reinit; +} + +static const char init_db_usage[] = +"git-init [--template=<template-directory>] [--shared]"; + +/* + * If you want to, you can share the DB area with any number of branches. + * That has advantages: you can save space by sharing all the SHA1 objects. + * On the other hand, it might just make lookup slower and messier. You + * be the judge. The default case is to have one DB per managed directory. + */ +int cmd_init_db(int argc, const char **argv, const char *prefix) +{ + const char *git_dir; + const char *sha1_dir; + const char *template_dir = NULL; + char *path; + int len, i, reinit; + + for (i = 1; i < argc; i++, argv++) { + const char *arg = argv[1]; + if (!strncmp(arg, "--template=", 11)) + template_dir = arg+11; + else if (!strcmp(arg, "--shared")) + shared_repository = PERM_GROUP; + else if (!strncmp(arg, "--shared=", 9)) + shared_repository = git_config_perm("arg", arg+9); + else + usage(init_db_usage); + } + + /* + * Set up the default .git directory contents + */ + git_dir = getenv(GIT_DIR_ENVIRONMENT); + if (!git_dir) + git_dir = DEFAULT_GIT_DIR_ENVIRONMENT; + safe_create_dir(git_dir, 0); + + /* Check to see if the repository version is right. + * Note that a newly created repository does not have + * config file, so this will not fail. What we are catching + * is an attempt to reinitialize new repository with an old tool. + */ + check_repository_format(); + + reinit = create_default_files(git_dir, template_dir); + + /* + * And set up the object store. + */ + sha1_dir = get_object_directory(); + len = strlen(sha1_dir); + path = xmalloc(len + 40); + memcpy(path, sha1_dir, len); + + safe_create_dir(sha1_dir, 1); + strcpy(path+len, "/pack"); + safe_create_dir(path, 1); + strcpy(path+len, "/info"); + safe_create_dir(path, 1); + + if (shared_repository) { + char buf[10]; + /* We do not spell "group" and such, so that + * the configuration can be read by older version + * of git. + */ + sprintf(buf, "%d", shared_repository); + git_config_set("core.sharedrepository", buf); + git_config_set("receive.denyNonFastforwards", "true"); + } + + printf("%s%s Git repository in %s/\n", + reinit ? "Reinitialized existing" : "Initialized empty", + shared_repository ? " shared" : "", + git_dir); + + return 0; +} diff --git a/builtin-log.c b/builtin-log.c new file mode 100644 index 0000000000..af2de54371 --- /dev/null +++ b/builtin-log.c @@ -0,0 +1,709 @@ +/* + * Builtin "git log" and related commands (show, whatchanged) + * + * (C) Copyright 2006 Linus Torvalds + * 2006 Junio Hamano + */ +#include "cache.h" +#include "commit.h" +#include "diff.h" +#include "revision.h" +#include "log-tree.h" +#include "builtin.h" +#include "tag.h" +#include "reflog-walk.h" + +static int default_show_root = 1; + +/* this is in builtin-diff.c */ +void add_head(struct rev_info *revs); + +static void cmd_log_init(int argc, const char **argv, const char *prefix, + struct rev_info *rev) +{ + int i; + + rev->abbrev = DEFAULT_ABBREV; + rev->commit_format = CMIT_FMT_DEFAULT; + rev->verbose_header = 1; + rev->show_root_diff = default_show_root; + argc = setup_revisions(argc, argv, rev, "HEAD"); + if (rev->diffopt.pickaxe || rev->diffopt.filter) + rev->always_show_header = 0; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strncmp(arg, "--encoding=", 11)) { + arg += 11; + if (strcmp(arg, "none")) + git_log_output_encoding = strdup(arg); + else + git_log_output_encoding = ""; + } + else + die("unrecognized argument: %s", arg); + } +} + +static int cmd_log_walk(struct rev_info *rev) +{ + struct commit *commit; + + prepare_revision_walk(rev); + while ((commit = get_revision(rev)) != NULL) { + log_tree_commit(rev, commit); + if (!rev->reflog_info) { + /* we allow cycles in reflog ancestry */ + free(commit->buffer); + commit->buffer = NULL; + } + free_commit_list(commit->parents); + commit->parents = NULL; + } + return 0; +} + +static int git_log_config(const char *var, const char *value) +{ + if (!strcmp(var, "log.showroot")) { + default_show_root = git_config_bool(var, value); + return 0; + } + return git_diff_ui_config(var, value); +} + +int cmd_whatchanged(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + + git_config(git_log_config); + init_revisions(&rev, prefix); + rev.diff = 1; + rev.diffopt.recursive = 1; + rev.simplify_history = 0; + cmd_log_init(argc, argv, prefix, &rev); + if (!rev.diffopt.output_format) + rev.diffopt.output_format = DIFF_FORMAT_RAW; + return cmd_log_walk(&rev); +} + +static int show_object(const unsigned char *sha1, int suppress_header) +{ + unsigned long size; + char type[20]; + char *buf = read_sha1_file(sha1, type, &size); + int offset = 0; + + if (!buf) + return error("Could not read object %s", sha1_to_hex(sha1)); + + if (suppress_header) + while (offset < size && buf[offset++] != '\n') { + int new_offset = offset; + while (new_offset < size && buf[new_offset++] != '\n') + ; /* do nothing */ + offset = new_offset; + } + + if (offset < size) + fwrite(buf + offset, size - offset, 1, stdout); + free(buf); + return 0; +} + +static int show_tree_object(const unsigned char *sha1, + const char *base, int baselen, + const char *pathname, unsigned mode, int stage) +{ + printf("%s%s\n", pathname, S_ISDIR(mode) ? "/" : ""); + return 0; +} + +int cmd_show(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + struct object_array_entry *objects; + int i, count, ret = 0; + + git_config(git_log_config); + init_revisions(&rev, prefix); + rev.diff = 1; + rev.diffopt.recursive = 1; + rev.combine_merges = 1; + rev.dense_combined_merges = 1; + rev.always_show_header = 1; + rev.ignore_merges = 0; + rev.no_walk = 1; + cmd_log_init(argc, argv, prefix, &rev); + + count = rev.pending.nr; + objects = rev.pending.objects; + for (i = 0; i < count && !ret; i++) { + struct object *o = objects[i].item; + const char *name = objects[i].name; + switch (o->type) { + case OBJ_BLOB: + ret = show_object(o->sha1, 0); + break; + case OBJ_TAG: { + struct tag *t = (struct tag *)o; + + printf("%stag %s%s\n\n", + diff_get_color(rev.diffopt.color_diff, + DIFF_COMMIT), + t->tag, + diff_get_color(rev.diffopt.color_diff, + DIFF_RESET)); + ret = show_object(o->sha1, 1); + objects[i].item = (struct object *)t->tagged; + i--; + break; + } + case OBJ_TREE: + printf("%stree %s%s\n\n", + diff_get_color(rev.diffopt.color_diff, + DIFF_COMMIT), + name, + diff_get_color(rev.diffopt.color_diff, + DIFF_RESET)); + read_tree_recursive((struct tree *)o, "", 0, 0, NULL, + show_tree_object); + break; + case OBJ_COMMIT: + rev.pending.nr = rev.pending.alloc = 0; + rev.pending.objects = NULL; + add_object_array(o, name, &rev.pending); + ret = cmd_log_walk(&rev); + break; + default: + ret = error("Unknown type: %d", o->type); + } + } + free(objects); + return ret; +} + +/* + * This is equivalent to "git log -g --abbrev-commit --pretty=oneline" + */ +int cmd_log_reflog(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + + git_config(git_log_config); + init_revisions(&rev, prefix); + init_reflog_walk(&rev.reflog_info); + rev.abbrev_commit = 1; + rev.verbose_header = 1; + cmd_log_init(argc, argv, prefix, &rev); + + /* + * This means that we override whatever commit format the user gave + * on the cmd line. Sad, but cmd_log_init() currently doesn't + * allow us to set a different default. + */ + rev.commit_format = CMIT_FMT_ONELINE; + rev.always_show_header = 1; + + /* + * We get called through "git reflog", so unlike the other log + * routines, we need to set up our pager manually.. + */ + setup_pager(); + + return cmd_log_walk(&rev); +} + +int cmd_log(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + + git_config(git_log_config); + init_revisions(&rev, prefix); + rev.always_show_header = 1; + cmd_log_init(argc, argv, prefix, &rev); + return cmd_log_walk(&rev); +} + +static int istitlechar(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '.' || c == '_'; +} + +static char *extra_headers = NULL; +static int extra_headers_size = 0; +static const char *fmt_patch_suffix = ".patch"; + +static int git_format_config(const char *var, const char *value) +{ + if (!strcmp(var, "format.headers")) { + int len; + + if (!value) + die("format.headers without value"); + len = strlen(value); + extra_headers_size += len + 1; + extra_headers = xrealloc(extra_headers, extra_headers_size); + extra_headers[extra_headers_size - len - 1] = 0; + strcat(extra_headers, value); + return 0; + } + if (!strcmp(var, "format.suffix")) { + if (!value) + die("format.suffix without value"); + fmt_patch_suffix = xstrdup(value); + return 0; + } + if (!strcmp(var, "diff.color") || !strcmp(var, "color.diff")) { + return 0; + } + return git_log_config(var, value); +} + + +static FILE *realstdout = NULL; +static const char *output_directory = NULL; + +static void reopen_stdout(struct commit *commit, int nr, int keep_subject) +{ + char filename[1024]; + char *sol; + int len = 0; + int suffix_len = strlen(fmt_patch_suffix) + 10; /* ., NUL and slop */ + + if (output_directory) { + strlcpy(filename, output_directory, 1000); + len = strlen(filename); + if (filename[len - 1] != '/') + filename[len++] = '/'; + } + + sprintf(filename + len, "%04d", nr); + len = strlen(filename); + + sol = strstr(commit->buffer, "\n\n"); + if (sol) { + int j, space = 1; + + sol += 2; + /* strip [PATCH] or [PATCH blabla] */ + if (!keep_subject && !strncmp(sol, "[PATCH", 6)) { + char *eos = strchr(sol + 6, ']'); + if (eos) { + while (isspace(*eos)) + eos++; + sol = eos; + } + } + + for (j = 0; + len < sizeof(filename) - suffix_len && + sol[j] && sol[j] != '\n'; + j++) { + if (istitlechar(sol[j])) { + if (space) { + filename[len++] = '-'; + space = 0; + } + filename[len++] = sol[j]; + if (sol[j] == '.') + while (sol[j + 1] == '.') + j++; + } else + space = 1; + } + while (filename[len - 1] == '.' || filename[len - 1] == '-') + len--; + } + strcpy(filename + len, fmt_patch_suffix); + fprintf(realstdout, "%s\n", filename); + freopen(filename, "w", stdout); +} + +static int get_patch_id(struct commit *commit, struct diff_options *options, + unsigned char *sha1) +{ + if (commit->parents) + diff_tree_sha1(commit->parents->item->object.sha1, + commit->object.sha1, "", options); + else + diff_root_tree_sha1(commit->object.sha1, "", options); + diffcore_std(options); + return diff_flush_patch_id(options, sha1); +} + +static void get_patch_ids(struct rev_info *rev, struct diff_options *options, const char *prefix) +{ + struct rev_info check_rev; + struct commit *commit; + struct object *o1, *o2; + unsigned flags1, flags2; + unsigned char sha1[20]; + + if (rev->pending.nr != 2) + die("Need exactly one range."); + + o1 = rev->pending.objects[0].item; + flags1 = o1->flags; + o2 = rev->pending.objects[1].item; + flags2 = o2->flags; + + if ((flags1 & UNINTERESTING) == (flags2 & UNINTERESTING)) + die("Not a range."); + + diff_setup(options); + options->recursive = 1; + if (diff_setup_done(options) < 0) + die("diff_setup_done failed"); + + /* given a range a..b get all patch ids for b..a */ + init_revisions(&check_rev, prefix); + o1->flags ^= UNINTERESTING; + o2->flags ^= UNINTERESTING; + add_pending_object(&check_rev, o1, "o1"); + add_pending_object(&check_rev, o2, "o2"); + prepare_revision_walk(&check_rev); + + while ((commit = get_revision(&check_rev)) != NULL) { + /* ignore merges */ + if (commit->parents && commit->parents->next) + continue; + + if (!get_patch_id(commit, options, sha1)) + created_object(sha1, xcalloc(1, sizeof(struct object))); + } + + /* reset for next revision walk */ + clear_commit_marks((struct commit *)o1, + SEEN | UNINTERESTING | SHOWN | ADDED); + clear_commit_marks((struct commit *)o2, + SEEN | UNINTERESTING | SHOWN | ADDED); + o1->flags = flags1; + o2->flags = flags2; +} + +static void gen_message_id(char *dest, unsigned int length, char *base) +{ + const char *committer = git_committer_info(-1); + const char *email_start = strrchr(committer, '<'); + const char *email_end = strrchr(committer, '>'); + if(!email_start || !email_end || email_start > email_end - 1) + die("Could not extract email from committer identity."); + snprintf(dest, length, "%s.%lu.git.%.*s", base, + (unsigned long) time(NULL), + (int)(email_end - email_start - 1), email_start + 1); +} + +int cmd_format_patch(int argc, const char **argv, const char *prefix) +{ + struct commit *commit; + struct commit **list = NULL; + struct rev_info rev; + int nr = 0, total, i, j; + int use_stdout = 0; + int numbered = 0; + int start_number = -1; + int keep_subject = 0; + int ignore_if_in_upstream = 0; + int thread = 0; + const char *in_reply_to = NULL; + struct diff_options patch_id_opts; + char *add_signoff = NULL; + char message_id[1024]; + char ref_message_id[1024]; + + git_config(git_format_config); + init_revisions(&rev, prefix); + rev.commit_format = CMIT_FMT_EMAIL; + rev.verbose_header = 1; + rev.diff = 1; + rev.combine_merges = 0; + rev.ignore_merges = 1; + rev.diffopt.msg_sep = ""; + rev.diffopt.recursive = 1; + + rev.extra_headers = extra_headers; + + /* + * Parse the arguments before setup_revisions(), or something + * like "git fmt-patch -o a123 HEAD^.." may fail; a123 is + * possibly a valid SHA1. + */ + for (i = 1, j = 1; i < argc; i++) { + if (!strcmp(argv[i], "--stdout")) + use_stdout = 1; + 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], "--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; + const char *endpos; + committer = git_committer_info(1); + 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; + else if (!strncmp(argv[i], "--attach=", 9)) + rev.mime_boundary = argv[i] + 9; + else if (!strcmp(argv[i], "--ignore-if-in-upstream")) + ignore_if_in_upstream = 1; + else if (!strcmp(argv[i], "--thread")) + thread = 1; + else if (!strncmp(argv[i], "--in-reply-to=", 14)) + in_reply_to = argv[i] + 14; + else if (!strcmp(argv[i], "--in-reply-to")) { + i++; + if (i == argc) + die("Need a Message-Id for --in-reply-to"); + in_reply_to = argv[i]; + } + else if (!strncmp(argv[i], "--suffix=", 9)) + fmt_patch_suffix = argv[i] + 9; + else + argv[j++] = argv[i]; + } + argc = j; + + 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 (!rev.diffopt.output_format) + rev.diffopt.output_format = DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_SUMMARY | DIFF_FORMAT_PATCH; + + if (!rev.diffopt.text) + rev.diffopt.binary = 1; + + if (!output_directory && !use_stdout) + output_directory = prefix; + + 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.nr == 1) { + if (rev.max_count < 0) { + rev.pending.objects[0].item->flags |= UNINTERESTING; + add_head(&rev); + } + /* Otherwise, it is "format-patch -22 HEAD", and + * get_revision() would return only the specified count. + */ + } + + if (ignore_if_in_upstream) + get_patch_ids(&rev, &patch_id_opts, prefix); + + if (!use_stdout) + realstdout = fdopen(dup(1), "w"); + + prepare_revision_walk(&rev); + while ((commit = get_revision(&rev)) != NULL) { + unsigned char sha1[20]; + + /* ignore merges */ + if (commit->parents && commit->parents->next) + continue; + + if (ignore_if_in_upstream && + !get_patch_id(commit, &patch_id_opts, sha1) && + lookup_object(sha1)) + continue; + + nr++; + list = xrealloc(list, nr * sizeof(list[0])); + list[nr - 1] = commit; + } + total = nr; + if (numbered) + rev.total = total + start_number - 1; + rev.add_signoff = add_signoff; + rev.ref_message_id = in_reply_to; + while (0 <= --nr) { + int shown; + commit = list[nr]; + rev.nr = total - nr + (start_number - 1); + /* Make the second and subsequent mails replies to the first */ + if (thread) { + if (nr == (total - 2)) { + strncpy(ref_message_id, message_id, + sizeof(ref_message_id)); + ref_message_id[sizeof(ref_message_id)-1]='\0'; + rev.ref_message_id = ref_message_id; + } + gen_message_id(message_id, sizeof(message_id), + sha1_to_hex(commit->object.sha1)); + rev.message_id = message_id; + } + 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", + mime_boundary_leader, + rev.mime_boundary); + else + printf("-- \n%s\n\n", git_version_string); + } + if (!use_stdout) + fclose(stdout); + } + free(list); + return 0; +} + +static int add_pending_commit(const char *arg, struct rev_info *revs, int flags) +{ + unsigned char sha1[20]; + if (get_sha1(arg, sha1) == 0) { + struct commit *commit = lookup_commit_reference(sha1); + if (commit) { + commit->object.flags |= flags; + add_pending_object(revs, &commit->object, arg); + return 0; + } + } + return -1; +} + +static const char cherry_usage[] = +"git-cherry [-v] <upstream> [<head>] [<limit>]"; +int cmd_cherry(int argc, const char **argv, const char *prefix) +{ + struct rev_info revs; + struct diff_options patch_id_opts; + struct commit *commit; + struct commit_list *list = NULL; + const char *upstream; + const char *head = "HEAD"; + const char *limit = NULL; + int verbose = 0; + + if (argc > 1 && !strcmp(argv[1], "-v")) { + verbose = 1; + argc--; + argv++; + } + + switch (argc) { + case 4: + limit = argv[3]; + /* FALLTHROUGH */ + case 3: + head = argv[2]; + /* FALLTHROUGH */ + case 2: + upstream = argv[1]; + break; + default: + usage(cherry_usage); + } + + init_revisions(&revs, prefix); + revs.diff = 1; + revs.combine_merges = 0; + revs.ignore_merges = 1; + revs.diffopt.recursive = 1; + + if (add_pending_commit(head, &revs, 0)) + die("Unknown commit %s", head); + if (add_pending_commit(upstream, &revs, UNINTERESTING)) + die("Unknown commit %s", upstream); + + /* Don't say anything if head and upstream are the same. */ + if (revs.pending.nr == 2) { + struct object_array_entry *o = revs.pending.objects; + if (hashcmp(o[0].item->sha1, o[1].item->sha1) == 0) + return 0; + } + + get_patch_ids(&revs, &patch_id_opts, prefix); + + if (limit && add_pending_commit(limit, &revs, UNINTERESTING)) + die("Unknown commit %s", limit); + + /* reverse the list of commits */ + prepare_revision_walk(&revs); + while ((commit = get_revision(&revs)) != NULL) { + /* ignore merges */ + if (commit->parents && commit->parents->next) + continue; + + commit_list_insert(commit, &list); + } + + while (list) { + unsigned char sha1[20]; + char sign = '+'; + + commit = list->item; + if (!get_patch_id(commit, &patch_id_opts, sha1) && + lookup_object(sha1)) + sign = '-'; + + if (verbose) { + static char buf[16384]; + pretty_print_commit(CMIT_FMT_ONELINE, commit, ~0, + buf, sizeof(buf), 0, NULL, NULL, 0); + printf("%c %s %s\n", sign, + sha1_to_hex(commit->object.sha1), buf); + } + else { + printf("%c %s\n", sign, + sha1_to_hex(commit->object.sha1)); + } + + list = list->next; + } + + return 0; +} diff --git a/builtin-ls-files.c b/builtin-ls-files.c new file mode 100644 index 0000000000..ac89eb2f77 --- /dev/null +++ b/builtin-ls-files.c @@ -0,0 +1,508 @@ +/* + * This merges the file listing in the directory cache index + * with the actual working directory list, and shows different + * combinations of the two. + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "quote.h" +#include "dir.h" +#include "builtin.h" + +static int abbrev; +static int show_deleted; +static int show_cached; +static int show_others; +static int show_stage; +static int show_unmerged; +static int show_modified; +static int show_killed; +static int show_valid_bit; +static int line_terminator = '\n'; + +static int prefix_len; +static int prefix_offset; +static const char **pathspec; +static int error_unmatch; +static char *ps_matched; + +static const char *tag_cached = ""; +static const char *tag_unmerged = ""; +static const char *tag_removed = ""; +static const char *tag_other = ""; +static const char *tag_killed = ""; +static const char *tag_modified = ""; + + +/* + * Match a pathspec against a filename. The first "len" characters + * are the common prefix + */ +static int match(const char **spec, char *ps_matched, + const char *filename, int len) +{ + const char *m; + + while ((m = *spec++) != NULL) { + int matchlen = strlen(m + len); + + if (!matchlen) + goto matched; + if (!strncmp(m + len, filename + len, matchlen)) { + if (m[len + matchlen - 1] == '/') + goto matched; + switch (filename[len + matchlen]) { + case '/': case '\0': + goto matched; + } + } + if (!fnmatch(m + len, filename + len, 0)) + goto matched; + if (ps_matched) + ps_matched++; + continue; + matched: + if (ps_matched) + *ps_matched = 1; + return 1; + } + return 0; +} + +static void show_dir_entry(const char *tag, struct dir_entry *ent) +{ + int len = prefix_len; + int offset = prefix_offset; + + if (len >= ent->len) + die("git-ls-files: internal error - directory entry not superset of prefix"); + + if (pathspec && !match(pathspec, ps_matched, ent->name, len)) + return; + + fputs(tag, stdout); + write_name_quoted("", 0, ent->name + offset, line_terminator, stdout); + putchar(line_terminator); +} + +static void show_other_files(struct dir_struct *dir) +{ + int i; + for (i = 0; i < dir->nr; i++) { + /* We should not have a matching entry, but we + * may have an unmerged entry for this path. + */ + struct dir_entry *ent = dir->entries[i]; + int pos = cache_name_pos(ent->name, ent->len); + struct cache_entry *ce; + if (0 <= pos) + die("bug in show-other-files"); + pos = -pos - 1; + if (pos < active_nr) { + ce = active_cache[pos]; + if (ce_namelen(ce) == ent->len && + !memcmp(ce->name, ent->name, ent->len)) + continue; /* Yup, this one exists unmerged */ + } + show_dir_entry(tag_other, ent); + } +} + +static void show_killed_files(struct dir_struct *dir) +{ + int i; + for (i = 0; i < dir->nr; i++) { + struct dir_entry *ent = dir->entries[i]; + char *cp, *sp; + int pos, len, killed = 0; + + for (cp = ent->name; cp - ent->name < ent->len; cp = sp + 1) { + sp = strchr(cp, '/'); + if (!sp) { + /* If ent->name is prefix of an entry in the + * cache, it will be killed. + */ + pos = cache_name_pos(ent->name, ent->len); + if (0 <= pos) + die("bug in show-killed-files"); + pos = -pos - 1; + while (pos < active_nr && + ce_stage(active_cache[pos])) + pos++; /* skip unmerged */ + if (active_nr <= pos) + break; + /* pos points at a name immediately after + * ent->name in the cache. Does it expect + * ent->name to be a directory? + */ + len = ce_namelen(active_cache[pos]); + if ((ent->len < len) && + !strncmp(active_cache[pos]->name, + ent->name, ent->len) && + active_cache[pos]->name[ent->len] == '/') + killed = 1; + break; + } + if (0 <= cache_name_pos(ent->name, sp - ent->name)) { + /* If any of the leading directories in + * ent->name is registered in the cache, + * ent->name will be killed. + */ + killed = 1; + break; + } + } + if (killed) + show_dir_entry(tag_killed, dir->entries[i]); + } +} + +static void show_ce_entry(const char *tag, struct cache_entry *ce) +{ + int len = prefix_len; + int offset = prefix_offset; + + if (len >= ce_namelen(ce)) + die("git-ls-files: internal error - cache entry not superset of prefix"); + + if (pathspec && !match(pathspec, ps_matched, ce->name, len)) + return; + + if (tag && *tag && show_valid_bit && + (ce->ce_flags & htons(CE_VALID))) { + static char alttag[4]; + memcpy(alttag, tag, 3); + if (isalpha(tag[0])) + alttag[0] = tolower(tag[0]); + else if (tag[0] == '?') + alttag[0] = '!'; + else { + alttag[0] = 'v'; + alttag[1] = tag[0]; + alttag[2] = ' '; + alttag[3] = 0; + } + tag = alttag; + } + + if (!show_stage) { + fputs(tag, stdout); + write_name_quoted("", 0, ce->name + offset, + line_terminator, stdout); + putchar(line_terminator); + } + else { + printf("%s%06o %s %d\t", + tag, + ntohl(ce->ce_mode), + abbrev ? find_unique_abbrev(ce->sha1,abbrev) + : sha1_to_hex(ce->sha1), + ce_stage(ce)); + write_name_quoted("", 0, ce->name + offset, + line_terminator, stdout); + putchar(line_terminator); + } +} + +static void show_files(struct dir_struct *dir, const char *prefix) +{ + int i; + + /* For cached/deleted files we don't need to even do the readdir */ + if (show_others || show_killed) { + const char *path = ".", *base = ""; + int baselen = prefix_len; + + if (baselen) + path = base = prefix; + read_directory(dir, path, base, baselen); + if (show_others) + show_other_files(dir); + if (show_killed) + show_killed_files(dir); + } + if (show_cached | show_stage) { + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (excluded(dir, ce->name) != dir->show_ignored) + continue; + if (show_unmerged && !ce_stage(ce)) + continue; + show_ce_entry(ce_stage(ce) ? tag_unmerged : tag_cached, ce); + } + } + if (show_deleted | show_modified) { + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + struct stat st; + int err; + if (excluded(dir, ce->name) != dir->show_ignored) + continue; + err = lstat(ce->name, &st); + if (show_deleted && err) + show_ce_entry(tag_removed, ce); + if (show_modified && ce_modified(ce, &st, 0)) + show_ce_entry(tag_modified, ce); + } + } +} + +/* + * Prune the index to only contain stuff starting with "prefix" + */ +static void prune_cache(const char *prefix) +{ + int pos = cache_name_pos(prefix, prefix_len); + unsigned int first, last; + + if (pos < 0) + pos = -pos-1; + active_cache += pos; + active_nr -= pos; + first = 0; + last = active_nr; + while (last > first) { + int next = (last + first) >> 1; + struct cache_entry *ce = active_cache[next]; + if (!strncmp(ce->name, prefix, prefix_len)) { + first = next+1; + continue; + } + last = next; + } + active_nr = last; +} + +static const char *verify_pathspec(const char *prefix) +{ + const char **p, *n, *prev; + char *real_prefix; + unsigned long max; + + prev = NULL; + max = PATH_MAX; + for (p = pathspec; (n = *p) != NULL; p++) { + int i, len = 0; + for (i = 0; i < max; i++) { + char c = n[i]; + if (prev && prev[i] != c) + break; + if (!c || c == '*' || c == '?') + break; + if (c == '/') + len = i+1; + } + prev = n; + if (len < max) { + max = len; + if (!max) + break; + } + } + + if (prefix_offset > max || memcmp(prev, prefix, prefix_offset)) + die("git-ls-files: cannot generate relative filenames containing '..'"); + + real_prefix = NULL; + prefix_len = max; + if (max) { + real_prefix = xmalloc(max + 1); + memcpy(real_prefix, prev, max); + real_prefix[max] = 0; + } + return real_prefix; +} + +static const char ls_files_usage[] = + "git-ls-files [-z] [-t] [-v] (--[cached|deleted|others|stage|unmerged|killed|modified])* " + "[ --ignored ] [--exclude=<pattern>] [--exclude-from=<file>] " + "[ --exclude-per-directory=<filename> ] [--full-name] [--abbrev] " + "[--] [<file>]*"; + +int cmd_ls_files(int argc, const char **argv, const char *prefix) +{ + int i; + int exc_given = 0, require_work_tree = 0; + struct dir_struct dir; + + memset(&dir, 0, sizeof(dir)); + if (prefix) + prefix_offset = strlen(prefix); + git_config(git_default_config); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (!strcmp(arg, "--")) { + i++; + break; + } + if (!strcmp(arg, "-z")) { + line_terminator = 0; + continue; + } + if (!strcmp(arg, "-t") || !strcmp(arg, "-v")) { + tag_cached = "H "; + tag_unmerged = "M "; + tag_removed = "R "; + tag_modified = "C "; + tag_other = "? "; + tag_killed = "K "; + if (arg[1] == 'v') + show_valid_bit = 1; + continue; + } + if (!strcmp(arg, "-c") || !strcmp(arg, "--cached")) { + show_cached = 1; + continue; + } + if (!strcmp(arg, "-d") || !strcmp(arg, "--deleted")) { + show_deleted = 1; + continue; + } + if (!strcmp(arg, "-m") || !strcmp(arg, "--modified")) { + show_modified = 1; + require_work_tree = 1; + continue; + } + if (!strcmp(arg, "-o") || !strcmp(arg, "--others")) { + show_others = 1; + require_work_tree = 1; + continue; + } + if (!strcmp(arg, "-i") || !strcmp(arg, "--ignored")) { + dir.show_ignored = 1; + require_work_tree = 1; + continue; + } + if (!strcmp(arg, "-s") || !strcmp(arg, "--stage")) { + show_stage = 1; + continue; + } + if (!strcmp(arg, "-k") || !strcmp(arg, "--killed")) { + show_killed = 1; + require_work_tree = 1; + continue; + } + if (!strcmp(arg, "--directory")) { + dir.show_other_directories = 1; + continue; + } + if (!strcmp(arg, "--no-empty-directory")) { + dir.hide_empty_directories = 1; + continue; + } + if (!strcmp(arg, "-u") || !strcmp(arg, "--unmerged")) { + /* There's no point in showing unmerged unless + * you also show the stage information. + */ + show_stage = 1; + show_unmerged = 1; + continue; + } + if (!strcmp(arg, "-x") && i+1 < argc) { + exc_given = 1; + add_exclude(argv[++i], "", 0, &dir.exclude_list[EXC_CMDL]); + continue; + } + if (!strncmp(arg, "--exclude=", 10)) { + exc_given = 1; + add_exclude(arg+10, "", 0, &dir.exclude_list[EXC_CMDL]); + continue; + } + if (!strcmp(arg, "-X") && i+1 < argc) { + exc_given = 1; + add_excludes_from_file(&dir, argv[++i]); + continue; + } + if (!strncmp(arg, "--exclude-from=", 15)) { + exc_given = 1; + add_excludes_from_file(&dir, arg+15); + continue; + } + if (!strncmp(arg, "--exclude-per-directory=", 24)) { + exc_given = 1; + dir.exclude_per_dir = arg + 24; + continue; + } + if (!strcmp(arg, "--full-name")) { + prefix_offset = 0; + continue; + } + if (!strcmp(arg, "--error-unmatch")) { + error_unmatch = 1; + continue; + } + if (!strncmp(arg, "--abbrev=", 9)) { + abbrev = strtoul(arg+9, NULL, 10); + if (abbrev && abbrev < MINIMUM_ABBREV) + abbrev = MINIMUM_ABBREV; + else if (abbrev > 40) + abbrev = 40; + continue; + } + if (!strcmp(arg, "--abbrev")) { + abbrev = DEFAULT_ABBREV; + continue; + } + if (*arg == '-') + usage(ls_files_usage); + break; + } + + if (require_work_tree && + (is_bare_repository() || is_inside_git_dir())) + die("This operation must be run in a work tree"); + + pathspec = get_pathspec(prefix, argv + i); + + /* Verify that the pathspec matches the prefix */ + if (pathspec) + prefix = verify_pathspec(prefix); + + /* Treat unmatching pathspec elements as errors */ + if (pathspec && error_unmatch) { + int num; + for (num = 0; pathspec[num]; num++) + ; + ps_matched = xcalloc(1, num); + } + + if (dir.show_ignored && !exc_given) { + fprintf(stderr, "%s: --ignored needs some exclude pattern\n", + argv[0]); + exit(1); + } + + /* With no flags, we default to showing the cached files */ + if (!(show_stage | show_deleted | show_others | show_unmerged | + show_killed | show_modified)) + show_cached = 1; + + read_cache(); + if (prefix) + prune_cache(prefix); + show_files(&dir, prefix); + + if (ps_matched) { + /* We need to make sure all pathspec matched otherwise + * it is an error. + */ + int num, errors = 0; + for (num = 0; pathspec[num]; num++) { + if (ps_matched[num]) + continue; + error("pathspec '%s' did not match any file(s) known to git.", + pathspec[num] + prefix_offset); + errors++; + } + + if (errors) + fprintf(stderr, "Did you forget to 'git add'?\n"); + + return errors ? 1 : 0; + } + + return 0; +} diff --git a/builtin-ls-tree.c b/builtin-ls-tree.c new file mode 100644 index 0000000000..201defd934 --- /dev/null +++ b/builtin-ls-tree.c @@ -0,0 +1,156 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "blob.h" +#include "tree.h" +#include "quote.h" +#include "builtin.h" + +static int line_termination = '\n'; +#define LS_RECURSIVE 1 +#define LS_TREE_ONLY 2 +#define LS_SHOW_TREES 4 +#define LS_NAME_ONLY 8 +static int abbrev; +static int ls_options; +static const char **pathspec; +static int chomp_prefix; +static const char *ls_tree_prefix; + +static const char ls_tree_usage[] = + "git-ls-tree [-d] [-r] [-t] [-z] [--name-only] [--name-status] [--full-name] [--abbrev[=<n>]] <tree-ish> [path...]"; + +static int show_recursive(const char *base, int baselen, const char *pathname) +{ + const char **s; + + if (ls_options & LS_RECURSIVE) + return 1; + + s = pathspec; + if (!s) + return 0; + + for (;;) { + const char *spec = *s++; + int len, speclen; + + if (!spec) + return 0; + if (strncmp(base, spec, baselen)) + continue; + len = strlen(pathname); + spec += baselen; + speclen = strlen(spec); + if (speclen <= len) + continue; + if (memcmp(pathname, spec, len)) + continue; + return 1; + } +} + +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 *type = blob_type; + + if (S_ISDIR(mode)) { + if (show_recursive(base, baselen, pathname)) { + retval = READ_TREE_RECURSIVE; + if (!(ls_options & LS_SHOW_TREES)) + return retval; + } + type = tree_type; + } + else if (ls_options & LS_TREE_ONLY) + return 0; + + if (chomp_prefix && + (baselen < chomp_prefix || memcmp(ls_tree_prefix, base, chomp_prefix))) + return 0; + + if (!(ls_options & LS_NAME_ONLY)) + printf("%06o %s %s\t", mode, type, + abbrev ? find_unique_abbrev(sha1,abbrev) + : sha1_to_hex(sha1)); + write_name_quoted(base + chomp_prefix, baselen - chomp_prefix, + pathname, + line_termination, stdout); + putchar(line_termination); + return retval; +} + +int cmd_ls_tree(int argc, const char **argv, const char *prefix) +{ + unsigned char sha1[20]; + struct tree *tree; + + git_config(git_default_config); + ls_tree_prefix = prefix; + if (prefix && *prefix) + chomp_prefix = strlen(prefix); + while (1 < argc && argv[1][0] == '-') { + switch (argv[1][1]) { + case 'z': + line_termination = 0; + break; + case 'r': + ls_options |= LS_RECURSIVE; + break; + case 'd': + ls_options |= LS_TREE_ONLY; + break; + case 't': + ls_options |= LS_SHOW_TREES; + break; + case '-': + if (!strcmp(argv[1]+2, "name-only") || + !strcmp(argv[1]+2, "name-status")) { + ls_options |= LS_NAME_ONLY; + break; + } + if (!strcmp(argv[1]+2, "full-name")) { + chomp_prefix = 0; + break; + } + if (!strncmp(argv[1]+2, "abbrev=",7)) { + abbrev = strtoul(argv[1]+9, NULL, 10); + if (abbrev && abbrev < MINIMUM_ABBREV) + abbrev = MINIMUM_ABBREV; + else if (abbrev > 40) + abbrev = 40; + break; + } + if (!strcmp(argv[1]+2, "abbrev")) { + abbrev = DEFAULT_ABBREV; + break; + } + /* otherwise fallthru */ + default: + usage(ls_tree_usage); + } + argc--; argv++; + } + /* -d -r should imply -t, but -d by itself should not have to. */ + if ( (LS_TREE_ONLY|LS_RECURSIVE) == + ((LS_TREE_ONLY|LS_RECURSIVE) & ls_options)) + ls_options |= LS_SHOW_TREES; + + if (argc < 2) + usage(ls_tree_usage); + if (get_sha1(argv[1], sha1)) + die("Not a valid object name %s", argv[1]); + + pathspec = get_pathspec(prefix, argv + 2); + tree = parse_tree_indirect(sha1); + if (!tree) + die("not a tree object"); + read_tree_recursive(tree, "", 0, 0, pathspec, show_tree); + + return 0; +} diff --git a/builtin-mailinfo.c b/builtin-mailinfo.c new file mode 100644 index 0000000000..583da38b67 --- /dev/null +++ b/builtin-mailinfo.c @@ -0,0 +1,825 @@ +/* + * Another stupid program, this one parsing the headers of an + * email to figure out authorship and subject + */ +#include "cache.h" +#include "builtin.h" +#include "utf8.h" + +static FILE *cmitmsg, *patchfile, *fin, *fout; + +static int keep_subject; +static const char *metainfo_charset; +static char line[1000]; +static char date[1000]; +static char name[1000]; +static char email[1000]; +static char subject[1000]; + +static enum { + TE_DONTCARE, TE_QP, TE_BASE64, +} transfer_encoding; +static char charset[256]; + +static char multipart_boundary[1000]; +static int multipart_boundary_len; +static int patch_lines; + +static char *sanity_check(char *name, char *email) +{ + int len = strlen(name); + if (len < 3 || len > 60) + return email; + if (strchr(name, '@') || strchr(name, '<') || strchr(name, '>')) + return email; + return name; +} + +static int bogus_from(char *line) +{ + /* John Doe <johndoe> */ + char *bra, *ket, *dst, *cp; + + /* This is fallback, so do not bother if we already have an + * e-mail address. + */ + if (*email) + return 0; + + bra = strchr(line, '<'); + if (!bra) + return 0; + ket = strchr(bra, '>'); + if (!ket) + return 0; + + for (dst = email, cp = bra+1; cp < ket; ) + *dst++ = *cp++; + *dst = 0; + for (cp = line; isspace(*cp); cp++) + ; + for (bra--; isspace(*bra); bra--) + *bra = 0; + cp = sanity_check(cp, email); + strcpy(name, cp); + return 1; +} + +static int handle_from(char *in_line) +{ + char line[1000]; + char *at; + char *dst; + + strcpy(line, in_line); + at = strchr(line, '@'); + if (!at) + return bogus_from(line); + + /* + * If we already have one email, don't take any confusing lines + */ + if (*email && strchr(at+1, '@')) + return 0; + + /* Pick up the string around '@', possibly delimited with <> + * pair; that is the email part. White them out while copying. + */ + while (at > line) { + char c = at[-1]; + if (isspace(c)) + break; + if (c == '<') { + at[-1] = ' '; + break; + } + at--; + } + dst = email; + for (;;) { + unsigned char c = *at; + if (!c || c == '>' || isspace(c)) { + if (c == '>') + *at = ' '; + break; + } + *at++ = ' '; + *dst++ = c; + } + *dst++ = 0; + + /* The remainder is name. It could be "John Doe <john.doe@xz>" + * or "john.doe@xz (John Doe)", but we have whited out the + * email part, so trim from both ends, possibly removing + * the () pair at the end. + */ + at = line + strlen(line); + while (at > line) { + unsigned char c = *--at; + if (!isspace(c)) { + at[(c == ')') ? 0 : 1] = 0; + break; + } + } + + at = line; + for (;;) { + unsigned char c = *at; + if (!c || !isspace(c)) { + if (c == '(') + at++; + break; + } + at++; + } + at = sanity_check(at, email); + strcpy(name, at); + return 1; +} + +static int handle_date(char *line) +{ + strcpy(date, line); + return 0; +} + +static int handle_subject(char *line) +{ + strcpy(subject, line); + return 0; +} + +/* NOTE NOTE NOTE. We do not claim we do full MIME. We just attempt + * to have enough heuristics to grok MIME encoded patches often found + * on our mailing lists. For example, we do not even treat header lines + * case insensitively. + */ + +static int slurp_attr(const char *line, const char *name, char *attr) +{ + const char *ends, *ap = strcasestr(line, name); + size_t sz; + + if (!ap) { + *attr = 0; + return 0; + } + ap += strlen(name); + if (*ap == '"') { + ap++; + ends = "\""; + } + else + ends = "; \t"; + sz = strcspn(ap, ends); + memcpy(attr, ap, sz); + attr[sz] = 0; + return 1; +} + +static int handle_subcontent_type(char *line) +{ + /* We do not want to mess with boundary. Note that we do not + * handle nested multipart. + */ + if (strcasestr(line, "boundary=")) { + fprintf(stderr, "Not handling nested multipart message.\n"); + exit(1); + } + slurp_attr(line, "charset=", charset); + if (*charset) { + int i, c; + for (i = 0; (c = charset[i]) != 0; i++) + charset[i] = tolower(c); + } + return 0; +} + +static int handle_content_type(char *line) +{ + *multipart_boundary = 0; + if (slurp_attr(line, "boundary=", multipart_boundary + 2)) { + memcpy(multipart_boundary, "--", 2); + multipart_boundary_len = strlen(multipart_boundary); + } + slurp_attr(line, "charset=", charset); + return 0; +} + +static int handle_content_transfer_encoding(char *line) +{ + if (strcasestr(line, "base64")) + transfer_encoding = TE_BASE64; + else if (strcasestr(line, "quoted-printable")) + transfer_encoding = TE_QP; + else + transfer_encoding = TE_DONTCARE; + return 0; +} + +static int is_multipart_boundary(const char *line) +{ + return (!memcmp(line, multipart_boundary, multipart_boundary_len)); +} + +static int eatspace(char *line) +{ + int len = strlen(line); + while (len > 0 && isspace(line[len-1])) + line[--len] = 0; + return len; +} + +#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: 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; + } + } + if (!memcmp("Date:", line, 5) && isspace(line[5])) { + if (!(*seen & SEEN_DATE)) { + handle_date(line+6); + *seen |= SEEN_DATE; + return; + } + } + if (!memcmp("Subject:", line, 8) && isspace(line[8])) { + if (!(*seen & SEEN_SUBJECT)) { + handle_subject(line+9); + *seen |= SEEN_SUBJECT; + return; + } + } + if (!memcmp("[PATCH]", line, 7) && isspace(line[7])) { + if (!(*seen & SEEN_SUBJECT)) { + handle_subject(line); + *seen |= SEEN_SUBJECT; + return; + } + } + *seen |= SEEN_PREFIX; +} + +static char *cleanup_subject(char *subject) +{ + if (keep_subject) + return subject; + for (;;) { + char *p; + int len, remove; + switch (*subject) { + case 'r': case 'R': + if (!memcmp("e:", subject+1, 2)) { + subject +=3; + continue; + } + break; + case ' ': case '\t': case ':': + subject++; + continue; + + case '[': + p = strchr(subject, ']'); + if (!p) { + subject++; + continue; + } + len = strlen(p); + remove = p - subject; + if (remove <= len *2) { + subject = p+1; + continue; + } + break; + } + eatspace(subject); + return subject; + } +} + +static void cleanup_space(char *buf) +{ + unsigned char c; + while ((c = *buf) != 0) { + buf++; + if (isspace(c)) { + buf[-1] = ' '; + c = *buf; + while (isspace(c)) { + int len = strlen(buf); + memmove(buf, buf+1, len); + c = *buf; + } + } + } +} + +static void decode_header(char *it); +typedef int (*header_fn_t)(char *); +struct header_def { + const char *name; + header_fn_t func; + int namelen; +}; + +static void check_header(char *line, struct header_def *header) +{ + int i; + + if (header[0].namelen <= 0) { + for (i = 0; header[i].name; i++) + header[i].namelen = strlen(header[i].name); + } + for (i = 0; header[i].name; 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(line + len + 2); + header[i].func(line + len + 2); + break; + } + } +} + +static void check_subheader_line(char *line) +{ + static struct header_def header[] = { + { "Content-Type", handle_subcontent_type }, + { "Content-Transfer-Encoding", + handle_content_transfer_encoding }, + { NULL }, + }; + check_header(line, header); +} +static void check_header_line(char *line) +{ + static struct header_def header[] = { + { "From", handle_from }, + { "Date", handle_date }, + { "Subject", handle_subject }, + { "Content-Type", handle_content_type }, + { "Content-Transfer-Encoding", + handle_content_transfer_encoding }, + { NULL }, + }; + 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) +{ + int ofs = 0; + while (ofs < sz) { + int peek, len; + if (fgets(line + ofs, sz - ofs, in) == NULL) + break; + len = eatspace(line + ofs); + if ((len == 0) || !is_rfc2822_header(line)) { + /* Re-add the newline */ + line[ofs + len] = '\n'; + line[ofs + len + 1] = '\0'; + break; + } + 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) || !memcmp(line, ">From ", 6))) + ofs = 1; + return ofs; +} + +static int decode_q_segment(char *in, char *ot, char *ep, int rfc2047) +{ + int c; + while ((c = *in++) != 0 && (in <= ep)) { + if (c == '=') { + int d = *in++; + if (d == '\n' || !d) + break; /* drop trailing newline */ + *ot++ = ((hexval(d) << 4) | hexval(*in++)); + continue; + } + if (rfc2047 && c == '_') /* rfc2047 4.2 (2) */ + c = 0x20; + *ot++ = c; + } + *ot = 0; + return 0; +} + +static int decode_b_segment(char *in, char *ot, char *ep) +{ + /* Decode in..ep, possibly in-place to ot */ + int c, pos = 0, acc = 0; + + while ((c = *in++) != 0 && (in <= ep)) { + if (c == '+') + c = 62; + else if (c == '/') + c = 63; + else if ('A' <= c && c <= 'Z') + c -= 'A'; + else if ('a' <= c && c <= 'z') + c -= 'a' - 26; + else if ('0' <= c && c <= '9') + c -= '0' - 52; + else if (c == '=') { + /* padding is almost like (c == 0), except we do + * not output NUL resulting only from it; + * for now we just trust the data. + */ + c = 0; + } + else + continue; /* garbage */ + switch (pos++) { + case 0: + acc = (c << 2); + break; + case 1: + *ot++ = (acc | (c >> 4)); + acc = (c & 15) << 4; + break; + case 2: + *ot++ = (acc | (c >> 2)); + acc = (c & 3) << 6; + break; + case 3: + *ot++ = (acc | c); + acc = pos = 0; + break; + } + } + *ot = 0; + return 0; +} + +static void convert_to_utf8(char *line, char *charset) +{ + static char latin_one[] = "latin1"; + char *input_charset = *charset ? charset : latin_one; + char *out = reencode_string(line, metainfo_charset, input_charset); + + if (!out) + die("cannot convert from %s to %s\n", + input_charset, metainfo_charset); + strcpy(line, out); + free(out); +} + +static int decode_header_bq(char *it) +{ + char *in, *out, *ep, *cp, *sp; + char outbuf[1000]; + int rfc2047 = 0; + + in = it; + out = outbuf; + while ((ep = strstr(in, "=?")) != NULL) { + int sz, encoding; + char charset_q[256], piecebuf[256]; + rfc2047 = 1; + + if (in != ep) { + sz = ep - in; + memcpy(out, in, sz); + out += sz; + in += sz; + } + /* E.g. + * ep : "=?iso-2022-jp?B?GyR...?= foo" + * ep : "=?ISO-8859-1?Q?Foo=FCbar?= baz" + */ + ep += 2; + cp = strchr(ep, '?'); + if (!cp) + return rfc2047; /* no munging */ + for (sp = ep; sp < cp; sp++) + charset_q[sp - ep] = tolower(*sp); + charset_q[cp - ep] = 0; + encoding = cp[1]; + if (!encoding || cp[2] != '?') + return rfc2047; /* no munging */ + ep = strstr(cp + 3, "?="); + if (!ep) + return rfc2047; /* no munging */ + switch (tolower(encoding)) { + default: + return rfc2047; /* no munging */ + case 'b': + sz = decode_b_segment(cp + 3, piecebuf, ep); + break; + case 'q': + sz = decode_q_segment(cp + 3, piecebuf, ep, 1); + break; + } + if (sz < 0) + return rfc2047; + if (metainfo_charset) + convert_to_utf8(piecebuf, charset_q); + strcpy(out, piecebuf); + out += strlen(out); + in = ep + 2; + } + strcpy(out, in); + strcpy(it, outbuf); + return rfc2047; +} + +static void decode_header(char *it) +{ + + if (decode_header_bq(it)) + return; + /* otherwise "it" is a straight copy of the input. + * This can be binary guck but there is no charset specified. + */ + if (metainfo_charset) + convert_to_utf8(it, ""); +} + +static void decode_transfer_encoding(char *line) +{ + char *ep; + + switch (transfer_encoding) { + case TE_QP: + ep = line + strlen(line); + decode_q_segment(line, line, ep, 0); + break; + case TE_BASE64: + ep = line + strlen(line); + decode_b_segment(line, line, ep); + break; + case TE_DONTCARE: + break; + } +} + +static void handle_info(void) +{ + char *sub; + + sub = cleanup_subject(subject); + cleanup_space(name); + cleanup_space(date); + cleanup_space(email); + cleanup_space(sub); + + fprintf(fout, "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(int *seen) +{ + if (!cmitmsg) + return 0; + do { + if (!memcmp("diff -", line, 6) || + !memcmp("---", line, 3) || + !memcmp("Index: ", line, 7)) + break; + if ((multipart_boundary[0] && is_multipart_boundary(line))) { + /* We come here when the first part had only + * the commit message without any patch. We + * pretend we have not seen this line yet, and + * go back to the loop. + */ + return 1; + } + + /* Unwrap transfer encoding and optionally + * normalize the log message to UTF-8. + */ + 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), fin) != NULL); + fclose(cmitmsg); + cmitmsg = NULL; + return 0; +} + +/* We have done the commit message and have the first + * line of the patch in line[]. + */ +static void handle_patch(void) +{ + do { + if (multipart_boundary[0] && is_multipart_boundary(line)) + break; + /* Only unwrap transfer encoding but otherwise do not + * do anything. We do *NOT* want UTF-8 conversion + * here; we are dealing with the user payload. + */ + decode_transfer_encoding(line); + fputs(line, patchfile); + patch_lines++; + } while (fgets(line, sizeof(line), fin) != NULL); +} + +/* multipart boundary and transfer encoding are set up for us, and we + * are at the end of the sub header. do equivalent of handle_body up + * to the next boundary without closing patchfile --- we will expect + * that the first part to contain commit message and a patch, and + * handle other parts as pure patches. + */ +static int handle_multipart_one_part(int *seen) +{ + int n = 0; + + while (fgets(line, sizeof(line), fin) != NULL) { + again: + n++; + if (is_multipart_boundary(line)) + break; + if (handle_commit_msg(seen)) + goto again; + handle_patch(); + break; + } + if (n == 0) + return -1; + return 0; +} + +static void handle_multipart_body(void) +{ + int seen = 0; + int part_num = 0; + + /* Skip up to the first boundary */ + while (fgets(line, sizeof(line), fin) != NULL) + if (is_multipart_boundary(line)) { + part_num = 1; + break; + } + if (!part_num) + return; + /* We are on boundary line. Start slurping the subhead. */ + while (1) { + int hdr = read_one_header_line(line, sizeof(line), fin); + 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); + } + fclose(patchfile); + if (!patch_lines) { + fprintf(stderr, "No patch found\n"); + exit(1); + } +} + +/* Non multipart message */ +static void handle_body(void) +{ + int seen = 0; + + handle_commit_msg(&seen); + handle_patch(); + fclose(patchfile); + if (!patch_lines) { + fprintf(stderr, "No patch found\n"); + exit(1); + } +} + +int mailinfo(FILE *in, FILE *out, int ks, const char *encoding, + const char *msg, const char *patch) +{ + keep_subject = ks; + metainfo_charset = encoding; + fin = in; + fout = out; + + cmitmsg = fopen(msg, "w"); + if (!cmitmsg) { + perror(msg); + return -1; + } + patchfile = fopen(patch, "w"); + if (!patchfile) { + perror(patch); + fclose(cmitmsg); + return -1; + } + while (1) { + int hdr = read_one_header_line(line, sizeof(line), fin); + if (!hdr) { + if (multipart_boundary[0]) + handle_multipart_body(); + else + handle_body(); + handle_info(); + break; + } + check_header_line(line); + } + + return 0; +} + +static const char mailinfo_usage[] = + "git-mailinfo [-k] [-u | --encoding=<encoding>] msg patch <mail >info"; + +int cmd_mailinfo(int argc, const char **argv, const char *prefix) +{ + const char *def_charset; + + /* NEEDSWORK: might want to do the optional .git/ directory + * discovery + */ + git_config(git_default_config); + + def_charset = (git_commit_encoding ? git_commit_encoding : "utf-8"); + metainfo_charset = def_charset; + + while (1 < argc && argv[1][0] == '-') { + if (!strcmp(argv[1], "-k")) + keep_subject = 1; + else if (!strcmp(argv[1], "-u")) + metainfo_charset = def_charset; + else if (!strcmp(argv[1], "-n")) + metainfo_charset = NULL; + else if (!strncmp(argv[1], "--encoding=", 11)) + metainfo_charset = argv[1] + 11; + else + usage(mailinfo_usage); + argc--; argv++; + } + + if (argc != 3) + usage(mailinfo_usage); + + return !!mailinfo(stdin, stdout, keep_subject, metainfo_charset, argv[1], argv[2]); +} diff --git a/builtin-mailsplit.c b/builtin-mailsplit.c new file mode 100644 index 0000000000..3bca855aae --- /dev/null +++ b/builtin-mailsplit.c @@ -0,0 +1,194 @@ +/* + * Totally braindamaged mbox splitter program. + * + * It just splits a mbox into a list of files: "0001" "0002" .. + * so you can process them further from there. + */ +#include "cache.h" +#include "builtin.h" + +static const char git_mailsplit_usage[] = +"git-mailsplit [-d<prec>] [-f<n>] [-b] -o<directory> <mbox>..."; + +static int is_from_line(const char *line, int len) +{ + const char *colon; + + if (len < 20 || memcmp("From ", line, 5)) + return 0; + + colon = line + len - 2; + line += 5; + for (;;) { + if (colon < line) + return 0; + if (*--colon == ':') + break; + } + + if (!isdigit(colon[-4]) || + !isdigit(colon[-2]) || + !isdigit(colon[-1]) || + !isdigit(colon[ 1]) || + !isdigit(colon[ 2])) + return 0; + + /* year */ + if (strtol(colon+3, NULL, 10) <= 90) + return 0; + + /* Ok, close enough */ + return 1; +} + +/* Could be as small as 64, enough to hold a Unix "From " line. */ +static char buf[4096]; + +/* Called with the first line (potentially partial) + * already in buf[] -- normally that should begin with + * the Unix "From " line. Write it into the specified + * file. + */ +static int split_one(FILE *mbox, const char *name, int allow_bare) +{ + FILE *output = NULL; + int len = strlen(buf); + int fd; + int status = 0; + int is_bare = !is_from_line(buf, len); + + if (is_bare && !allow_bare) + goto corrupt; + + fd = open(name, O_WRONLY | O_CREAT | O_EXCL, 0666); + if (fd < 0) + die("cannot open output file %s", name); + output = fdopen(fd, "w"); + + /* Copy it out, while searching for a line that begins with + * "From " and having something that looks like a date format. + */ + for (;;) { + int is_partial = (buf[len-1] != '\n'); + + if (fputs(buf, output) == EOF) + die("cannot write output"); + + if (fgets(buf, sizeof(buf), mbox) == NULL) { + if (feof(mbox)) { + status = 1; + break; + } + die("cannot read mbox"); + } + len = strlen(buf); + if (!is_partial && !is_bare && is_from_line(buf, len)) + break; /* done with one message */ + } + fclose(output); + return status; + + corrupt: + if (output) + fclose(output); + unlink(name); + fprintf(stderr, "corrupt mailbox\n"); + exit(1); +} + +int split_mbox(const char **mbox, const char *dir, int allow_bare, int nr_prec, int skip) +{ + char *name = xmalloc(strlen(dir) + 2 + 3 * sizeof(skip)); + int ret = -1; + + while (*mbox) { + const char *file = *mbox++; + FILE *f = !strcmp(file, "-") ? stdin : fopen(file, "r"); + int file_done = 0; + + if ( !f ) { + error("cannot open mbox %s", file); + goto out; + } + + if (fgets(buf, sizeof(buf), f) == NULL) { + if (f == stdin) + break; /* empty stdin is OK */ + error("cannot read mbox %s", file); + goto out; + } + + while (!file_done) { + sprintf(name, "%s/%0*d", dir, nr_prec, ++skip); + file_done = split_one(f, name, allow_bare); + } + + if (f != stdin) + fclose(f); + } + ret = skip; +out: + free(name); + return ret; +} +int cmd_mailsplit(int argc, const char **argv, const char *prefix) +{ + int nr = 0, nr_prec = 4, ret; + int allow_bare = 0; + const char *dir = NULL; + const char **argp; + static const char *stdin_only[] = { "-", NULL }; + + for (argp = argv+1; *argp; argp++) { + const char *arg = *argp; + + if (arg[0] != '-') + break; + /* do flags here */ + if ( arg[1] == 'd' ) { + nr_prec = strtol(arg+2, NULL, 10); + if (nr_prec < 3 || 10 <= nr_prec) + usage(git_mailsplit_usage); + continue; + } else if ( arg[1] == 'f' ) { + nr = strtol(arg+2, NULL, 10); + } else if ( arg[1] == 'b' && !arg[2] ) { + allow_bare = 1; + } else if ( arg[1] == 'o' && arg[2] ) { + dir = arg+2; + } else if ( arg[1] == '-' && !arg[2] ) { + argp++; /* -- marks end of options */ + break; + } else { + die("unknown option: %s", arg); + } + } + + if ( !dir ) { + /* Backwards compatibility: if no -o specified, accept + <mbox> <dir> or just <dir> */ + switch (argc - (argp-argv)) { + case 1: + dir = argp[0]; + argp = stdin_only; + break; + case 2: + stdin_only[0] = argp[0]; + dir = argp[1]; + argp = stdin_only; + break; + default: + usage(git_mailsplit_usage); + } + } else { + /* New usage: if no more argument, parse stdin */ + if ( !*argp ) + argp = stdin_only; + } + + ret = split_mbox(argp, dir, allow_bare, nr_prec, nr); + if (ret != -1) + printf("%d\n", ret); + + return ret == -1; +} diff --git a/builtin-merge-file.c b/builtin-merge-file.c new file mode 100644 index 0000000000..9135773908 --- /dev/null +++ b/builtin-merge-file.c @@ -0,0 +1,63 @@ +#include "cache.h" +#include "xdiff/xdiff.h" +#include "xdiff-interface.h" + +static const char merge_file_usage[] = +"git merge-file [-p | --stdout] [-q | --quiet] [-L name1 [-L orig [-L name2]]] file1 orig_file file2"; + +int cmd_merge_file(int argc, char **argv, char **envp) +{ + char *names[3]; + mmfile_t mmfs[3]; + mmbuffer_t result = {NULL, 0}; + xpparam_t xpp = {XDF_NEED_MINIMAL}; + int ret = 0, i = 0, to_stdout = 0; + + while (argc > 4) { + if (!strcmp(argv[1], "-L") && i < 3) { + names[i++] = argv[2]; + argc--; + argv++; + } else if (!strcmp(argv[1], "-p") || + !strcmp(argv[1], "--stdout")) + to_stdout = 1; + else if (!strcmp(argv[1], "-q") || + !strcmp(argv[1], "--quiet")) + freopen("/dev/null", "w", stderr); + else + usage(merge_file_usage); + argc--; + argv++; + } + + if (argc != 4) + usage(merge_file_usage); + + for (; i < 3; i++) + names[i] = argv[i + 1]; + + for (i = 0; i < 3; i++) + if (read_mmfile(mmfs + i, argv[i + 1])) + return -1; + + ret = xdl_merge(mmfs + 1, mmfs + 0, names[0], mmfs + 2, names[2], + &xpp, XDL_MERGE_ZEALOUS, &result); + + for (i = 0; i < 3; i++) + free(mmfs[i].ptr); + + if (ret >= 0) { + char *filename = argv[1]; + FILE *f = to_stdout ? stdout : fopen(filename, "wb"); + + if (!f) + ret = error("Could not open %s for writing", filename); + else if (fwrite(result.ptr, result.size, 1, f) != 1) + ret = error("Could not write to %s", filename); + else if (fclose(f)) + ret = error("Could not close %s", filename); + free(result.ptr); + } + + return ret; +} diff --git a/builtin-mv.c b/builtin-mv.c new file mode 100644 index 0000000000..737af350b8 --- /dev/null +++ b/builtin-mv.c @@ -0,0 +1,294 @@ +/* + * "git mv" builtin command + * + * Copyright (C) 2006 Johannes Schindelin + */ +#include "cache.h" +#include "builtin.h" +#include "dir.h" +#include "cache-tree.h" +#include "path-list.h" + +static const char builtin_mv_usage[] = +"git-mv [-n] [-f] (<source> <destination> | [-k] <source>... <destination>)"; + +static const char **copy_pathspec(const char *prefix, const char **pathspec, + int count, int base_name) +{ + int i; + const char **result = xmalloc((count + 1) * sizeof(const char *)); + memcpy(result, pathspec, count * sizeof(const char *)); + result[count] = NULL; + for (i = 0; i < count; i++) { + int length = strlen(result[i]); + if (length > 0 && result[i][length - 1] == '/') { + char *without_slash = xmalloc(length); + memcpy(without_slash, result[i], length - 1); + without_slash[length - 1] = '\0'; + result[i] = without_slash; + } + if (base_name) { + const char *last_slash = strrchr(result[i], '/'); + if (last_slash) + result[i] = last_slash + 1; + } + } + return get_pathspec(prefix, result); +} + +static void show_list(const char *label, struct path_list *list) +{ + if (list->nr > 0) { + int i; + printf("%s", label); + for (i = 0; i < list->nr; i++) + printf("%s%s", i > 0 ? ", " : "", list->items[i].path); + putchar('\n'); + } +} + +static const char *add_slash(const char *path) +{ + int len = strlen(path); + if (path[len - 1] != '/') { + char *with_slash = xmalloc(len + 2); + memcpy(with_slash, path, len); + with_slash[len++] = '/'; + with_slash[len] = 0; + return with_slash; + } + return path; +} + +static struct lock_file lock_file; + +int cmd_mv(int argc, const char **argv, const char *prefix) +{ + int i, newfd, count; + int verbose = 0, show_only = 0, force = 0, ignore_errors = 0; + const char **source, **destination, **dest_path; + enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes; + struct stat st; + struct path_list overwritten = {NULL, 0, 0, 0}; + struct path_list src_for_dst = {NULL, 0, 0, 0}; + struct path_list added = {NULL, 0, 0, 0}; + struct path_list deleted = {NULL, 0, 0, 0}; + struct path_list changed = {NULL, 0, 0, 0}; + + git_config(git_default_config); + + newfd = hold_lock_file_for_update(&lock_file, get_index_file(), 1); + if (read_cache() < 0) + die("index file corrupt"); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (arg[0] != '-') + break; + if (!strcmp(arg, "--")) { + i++; + break; + } + if (!strcmp(arg, "-n")) { + show_only = 1; + continue; + } + if (!strcmp(arg, "-f")) { + force = 1; + continue; + } + if (!strcmp(arg, "-k")) { + ignore_errors = 1; + continue; + } + usage(builtin_mv_usage); + } + count = argc - i - 1; + if (count < 1) + usage(builtin_mv_usage); + + source = copy_pathspec(prefix, argv + i, count, 0); + modes = xcalloc(count, sizeof(enum update_mode)); + dest_path = copy_pathspec(prefix, argv + argc - 1, 1, 0); + + if (dest_path[0][0] == '\0') + /* special case: "." was normalized to "" */ + destination = copy_pathspec(dest_path[0], argv + i, count, 1); + else if (!lstat(dest_path[0], &st) && + S_ISDIR(st.st_mode)) { + dest_path[0] = add_slash(dest_path[0]); + destination = copy_pathspec(dest_path[0], argv + i, count, 1); + } else { + if (count != 1) + usage(builtin_mv_usage); + destination = dest_path; + } + + /* Checking */ + for (i = 0; i < count; i++) { + const char *src = source[i], *dst = destination[i]; + int length, src_is_dir; + const char *bad = NULL; + + if (show_only) + printf("Checking rename of '%s' to '%s'\n", src, dst); + + length = strlen(src); + if (lstat(src, &st) < 0) + bad = "bad source"; + else if (!strncmp(src, dst, length) && + (dst[length] == 0 || dst[length] == '/')) { + bad = "can not move directory into itself"; + } else if ((src_is_dir = S_ISDIR(st.st_mode)) + && lstat(dst, &st) == 0) + bad = "cannot move directory over file"; + else if (src_is_dir) { + const char *src_w_slash = add_slash(src); + int len_w_slash = length + 1; + int first, last; + + modes[i] = WORKING_DIRECTORY; + + first = cache_name_pos(src_w_slash, len_w_slash); + if (first >= 0) + die ("Huh? %.*s is in index?", + len_w_slash, src_w_slash); + + first = -1 - first; + for (last = first; last < active_nr; last++) { + const char *path = active_cache[last]->name; + if (strncmp(path, src_w_slash, len_w_slash)) + break; + } + free((char *)src_w_slash); + + if (last - first < 1) + bad = "source directory is empty"; + else { + int j, dst_len; + + if (last - first > 0) { + source = xrealloc(source, + (count + last - first) + * sizeof(char *)); + destination = xrealloc(destination, + (count + last - first) + * sizeof(char *)); + modes = xrealloc(modes, + (count + last - first) + * sizeof(enum update_mode)); + } + + dst = add_slash(dst); + dst_len = strlen(dst) - 1; + + for (j = 0; j < last - first; j++) { + const char *path = + active_cache[first + j]->name; + source[count + j] = path; + destination[count + j] = + prefix_path(dst, dst_len, + path + length); + modes[count + j] = INDEX; + } + count += last - first; + } + } else if (lstat(dst, &st) == 0) { + bad = "destination exists"; + if (force) { + /* + * only files can overwrite each other: + * check both source and destination + */ + if (S_ISREG(st.st_mode)) { + fprintf(stderr, "Warning: %s;" + " will overwrite!\n", + bad); + bad = NULL; + path_list_insert(dst, &overwritten); + } else + bad = "Cannot overwrite"; + } + } else if (cache_name_pos(src, length) < 0) + bad = "not under version control"; + else if (path_list_has_path(&src_for_dst, dst)) + bad = "multiple sources for the same target"; + else + path_list_insert(dst, &src_for_dst); + + if (bad) { + if (ignore_errors) { + if (--count > 0) { + memmove(source + i, source + i + 1, + (count - i) * sizeof(char *)); + memmove(destination + i, + destination + i + 1, + (count - i) * sizeof(char *)); + } + } else + die ("%s, source=%s, destination=%s", + bad, src, dst); + } + } + + for (i = 0; i < count; i++) { + const char *src = source[i], *dst = destination[i]; + enum update_mode mode = modes[i]; + if (show_only || verbose) + printf("Renaming %s to %s\n", src, dst); + if (!show_only && mode != INDEX && + rename(src, dst) < 0 && !ignore_errors) + die ("renaming %s failed: %s", src, strerror(errno)); + + if (mode == WORKING_DIRECTORY) + continue; + + if (cache_name_pos(src, strlen(src)) >= 0) { + path_list_insert(src, &deleted); + + /* destination can be a directory with 1 file inside */ + if (path_list_has_path(&overwritten, dst)) + path_list_insert(dst, &changed); + else + path_list_insert(dst, &added); + } else + path_list_insert(dst, &added); + } + + if (show_only) { + show_list("Changed : ", &changed); + show_list("Adding : ", &added); + show_list("Deleting : ", &deleted); + } else { + for (i = 0; i < changed.nr; i++) { + const char *path = changed.items[i].path; + int j = cache_name_pos(path, strlen(path)); + struct cache_entry *ce = active_cache[j]; + + if (j < 0) + die ("Huh? Cache entry for %s unknown?", path); + refresh_cache_entry(ce, 0); + } + + for (i = 0; i < added.nr; i++) { + const char *path = added.items[i].path; + add_file_to_index(path, verbose); + } + + for (i = 0; i < deleted.nr; i++) { + const char *path = deleted.items[i].path; + remove_file_from_cache(path); + cache_tree_invalidate_path(active_cache_tree, path); + } + + if (active_cache_changed) { + if (write_cache(newfd, active_cache, active_nr) || + close(newfd) || + commit_lock_file(&lock_file)) + die("Unable to write new index file"); + } + } + + return 0; +} diff --git a/builtin-name-rev.c b/builtin-name-rev.c new file mode 100644 index 0000000000..b4f15cc38a --- /dev/null +++ b/builtin-name-rev.c @@ -0,0 +1,255 @@ +#include "builtin.h" +#include "cache.h" +#include "commit.h" +#include "tag.h" +#include "refs.h" + +static const char name_rev_usage[] = + "git-name-rev [--tags] ( --all | --stdin | committish [committish...] )\n"; + +typedef struct rev_name { + const char *tip_name; + int merge_traversals; + int generation; +} rev_name; + +static long cutoff = LONG_MAX; + +static void name_rev(struct commit *commit, + const char *tip_name, int merge_traversals, int generation, + int deref) +{ + struct rev_name *name = (struct rev_name *)commit->util; + struct commit_list *parents; + int parent_number = 1; + + if (!commit->object.parsed) + parse_commit(commit); + + if (commit->date < cutoff) + return; + + if (deref) { + char *new_name = xmalloc(strlen(tip_name)+3); + strcpy(new_name, tip_name); + strcat(new_name, "^0"); + tip_name = new_name; + + if (generation) + die("generation: %d, but deref?", generation); + } + + if (name == NULL) { + name = xmalloc(sizeof(rev_name)); + commit->util = name; + goto copy_data; + } else if (name->merge_traversals > merge_traversals || + (name->merge_traversals == merge_traversals && + name->generation > generation)) { +copy_data: + name->tip_name = tip_name; + name->merge_traversals = merge_traversals; + name->generation = generation; + } else + return; + + for (parents = commit->parents; + parents; + parents = parents->next, parent_number++) { + if (parent_number > 1) { + char *new_name = xmalloc(strlen(tip_name)+8); + + if (generation > 0) + sprintf(new_name, "%s~%d^%d", tip_name, + generation, parent_number); + else + sprintf(new_name, "%s^%d", tip_name, parent_number); + + name_rev(parents->item, new_name, + merge_traversals + 1 , 0, 0); + } else { + name_rev(parents->item, tip_name, merge_traversals, + generation + 1, 0); + } + } +} + +static int name_ref(const char *path, const unsigned char *sha1, int flags, void *cb_data) +{ + struct object *o = parse_object(sha1); + int tags_only = *(int*)cb_data; + int deref = 0; + + if (tags_only && strncmp(path, "refs/tags/", 10)) + return 0; + + while (o && o->type == OBJ_TAG) { + struct tag *t = (struct tag *) o; + if (!t->tagged) + break; /* broken repository */ + o = parse_object(t->tagged->sha1); + deref = 1; + } + if (o && o->type == OBJ_COMMIT) { + struct commit *commit = (struct commit *)o; + + if (!strncmp(path, "refs/heads/", 11)) + path = path + 11; + else if (!strncmp(path, "refs/", 5)) + path = path + 5; + + name_rev(commit, xstrdup(path), 0, 0, deref); + } + return 0; +} + +/* returns a static buffer */ +static const char* get_rev_name(struct object *o) +{ + static char buffer[1024]; + struct rev_name *n; + struct commit *c; + + if (o->type != OBJ_COMMIT) + return "undefined"; + c = (struct commit *) o; + n = c->util; + if (!n) + return "undefined"; + + if (!n->generation) + return n->tip_name; + + snprintf(buffer, sizeof(buffer), "%s~%d", n->tip_name, n->generation); + + return buffer; +} + +int cmd_name_rev(int argc, const char **argv, const char *prefix) +{ + struct object_array revs = { 0, 0, NULL }; + int as_is = 0, all = 0, transform_stdin = 0; + int tags_only = 0; + + git_config(git_default_config); + + if (argc < 2) + usage(name_rev_usage); + + for (--argc, ++argv; argc; --argc, ++argv) { + unsigned char sha1[20]; + struct object *o; + struct commit *commit; + + if (!as_is && (*argv)[0] == '-') { + if (!strcmp(*argv, "--")) { + as_is = 1; + continue; + } else if (!strcmp(*argv, "--tags")) { + tags_only = 1; + continue; + } else if (!strcmp(*argv, "--all")) { + if (argc > 1) + die("Specify either a list, or --all, not both!"); + all = 1; + cutoff = 0; + continue; + } else if (!strcmp(*argv, "--stdin")) { + if (argc > 1) + die("Specify either a list, or --stdin, not both!"); + transform_stdin = 1; + cutoff = 0; + continue; + } + usage(name_rev_usage); + } + + if (get_sha1(*argv, sha1)) { + fprintf(stderr, "Could not get sha1 for %s. Skipping.\n", + *argv); + continue; + } + + o = deref_tag(parse_object(sha1), *argv, 0); + if (!o || o->type != OBJ_COMMIT) { + fprintf(stderr, "Could not get commit for %s. Skipping.\n", + *argv); + continue; + } + + commit = (struct commit *)o; + + if (cutoff > commit->date) + cutoff = commit->date; + + add_object_array((struct object *)commit, *argv, &revs); + } + + for_each_ref(name_ref, &tags_only); + + if (transform_stdin) { + char buffer[2048]; + char *p, *p_start; + + while (!feof(stdin)) { + int forty = 0; + p = fgets(buffer, sizeof(buffer), stdin); + if (!p) + break; + + for (p_start = p; *p; p++) { +#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f')) + if (!ishex(*p)) + forty = 0; + else if (++forty == 40 && + !ishex(*(p+1))) { + unsigned char sha1[40]; + const char *name = "undefined"; + char c = *(p+1); + + forty = 0; + + *(p+1) = 0; + if (!get_sha1(p - 39, sha1)) { + struct object *o = + lookup_object(sha1); + if (o) + name = get_rev_name(o); + } + *(p+1) = c; + + if (!strcmp(name, "undefined")) + continue; + + fwrite(p_start, p - p_start + 1, 1, + stdout); + printf(" (%s)", name); + p_start = p + 1; + } + } + + /* flush */ + if (p_start != p) + fwrite(p_start, p - p_start, 1, stdout); + } + } else if (all) { + int i, max; + + max = get_max_object_index(); + for (i = 0; i < max; i++) { + struct object * obj = get_indexed_object(i); + if (!obj) + continue; + printf("%s %s\n", sha1_to_hex(obj->sha1), get_rev_name(obj)); + } + } else { + int i; + for (i = 0; i < revs.nr; i++) + printf("%s %s\n", + revs.objects[i].name, + get_rev_name(revs.objects[i].item)); + } + + return 0; +} + diff --git a/builtin-pack-objects.c b/builtin-pack-objects.c new file mode 100644 index 0000000000..3824ee33ac --- /dev/null +++ b/builtin-pack-objects.c @@ -0,0 +1,1717 @@ +#include "builtin.h" +#include "cache.h" +#include "object.h" +#include "blob.h" +#include "commit.h" +#include "tag.h" +#include "tree.h" +#include "delta.h" +#include "pack.h" +#include "csum-file.h" +#include "tree-walk.h" +#include "diff.h" +#include "revision.h" +#include "list-objects.h" + +static const char pack_usage[] = "\ +git-pack-objects [{ -q | --progress | --all-progress }] \n\ + [--local] [--incremental] [--window=N] [--depth=N] \n\ + [--no-reuse-delta] [--delta-base-offset] [--non-empty] \n\ + [--revs [--unpacked | --all]*] [--reflog] [--stdout | base-name] \n\ + [<ref-list | <object-list]"; + +struct object_entry { + unsigned char sha1[20]; + unsigned long size; /* uncompressed size */ + unsigned long offset; /* offset into the final pack file; + * nonzero if already written. + */ + unsigned int depth; /* delta depth */ + unsigned int delta_limit; /* base adjustment for in-pack delta */ + unsigned int hash; /* name hint hash */ + enum object_type type; + enum object_type in_pack_type; /* could be delta */ + unsigned long delta_size; /* delta data size (uncompressed) */ +#define in_pack_header_size delta_size /* only when reusing pack data */ + struct object_entry *delta; /* delta base object */ + struct packed_git *in_pack; /* already in pack */ + unsigned int in_pack_offset; + struct object_entry *delta_child; /* deltified objects who bases me */ + struct object_entry *delta_sibling; /* other deltified objects who + * uses the same base as me + */ + int preferred_base; /* we do not pack this, but is encouraged to + * be used as the base objectto delta huge + * objects against. + */ +}; + +/* + * Objects we are going to pack are collected in objects array (dynamically + * expanded). nr_objects & nr_alloc controls this array. They are stored + * in the order we see -- typically rev-list --objects order that gives us + * nice "minimum seek" order. + * + * sorted-by-sha ans sorted-by-type are arrays of pointers that point at + * elements in the objects array. The former is used to build the pack + * index (lists object names in the ascending order to help offset lookup), + * and the latter is used to group similar things together by try_delta() + * heuristics. + */ + +static unsigned char object_list_sha1[20]; +static int non_empty; +static int no_reuse_delta; +static int local; +static int incremental; +static int allow_ofs_delta; + +static struct object_entry **sorted_by_sha, **sorted_by_type; +static struct object_entry *objects; +static int nr_objects, nr_alloc, nr_result; +static const char *base_name; +static unsigned char pack_file_sha1[20]; +static int progress = 1; +static volatile sig_atomic_t progress_update; +static int window = 10; +static int pack_to_stdout; +static int num_preferred_base; + +/* + * The object names in objects array are hashed with this hashtable, + * to help looking up the entry by object name. Binary search from + * sorted_by_sha is also possible but this was easier to code and faster. + * This hashtable is built after all the objects are seen. + */ +static int *object_ix; +static int object_ix_hashsz; + +/* + * Pack index for existing packs give us easy access to the offsets into + * corresponding pack file where each object's data starts, but the entries + * do not store the size of the compressed representation (uncompressed + * size is easily available by examining the pack entry header). It is + * also rather expensive to find the sha1 for an object given its offset. + * + * We build a hashtable of existing packs (pack_revindex), and keep reverse + * index here -- pack index file is sorted by object name mapping to offset; + * this pack_revindex[].revindex array is a list of offset/index_nr pairs + * ordered by offset, so if you know the offset of an object, next offset + * is where its packed representation ends and the index_nr can be used to + * get the object sha1 from the main index. + */ +struct revindex_entry { + unsigned int offset; + unsigned int nr; +}; +struct pack_revindex { + struct packed_git *p; + struct revindex_entry *revindex; +}; +static struct pack_revindex *pack_revindex; +static int pack_revindex_hashsz; + +/* + * stats + */ +static int written; +static int written_delta; +static int reused; +static int reused_delta; + +static int pack_revindex_ix(struct packed_git *p) +{ + unsigned long ui = (unsigned long)p; + int i; + + ui = ui ^ (ui >> 16); /* defeat structure alignment */ + i = (int)(ui % pack_revindex_hashsz); + while (pack_revindex[i].p) { + if (pack_revindex[i].p == p) + return i; + if (++i == pack_revindex_hashsz) + i = 0; + } + return -1 - i; +} + +static void prepare_pack_ix(void) +{ + int num; + struct packed_git *p; + for (num = 0, p = packed_git; p; p = p->next) + num++; + if (!num) + return; + pack_revindex_hashsz = num * 11; + pack_revindex = xcalloc(sizeof(*pack_revindex), pack_revindex_hashsz); + for (p = packed_git; p; p = p->next) { + num = pack_revindex_ix(p); + num = - 1 - num; + pack_revindex[num].p = p; + } + /* revindex elements are lazily initialized */ +} + +static int cmp_offset(const void *a_, const void *b_) +{ + const struct revindex_entry *a = a_; + const struct revindex_entry *b = b_; + return (a->offset < b->offset) ? -1 : (a->offset > b->offset) ? 1 : 0; +} + +/* + * Ordered list of offsets of objects in the pack. + */ +static void prepare_pack_revindex(struct pack_revindex *rix) +{ + struct packed_git *p = rix->p; + int num_ent = num_packed_objects(p); + int i; + void *index = p->index_base + 256; + + rix->revindex = xmalloc(sizeof(*rix->revindex) * (num_ent + 1)); + for (i = 0; i < num_ent; i++) { + unsigned int hl = *((unsigned int *)((char *) index + 24*i)); + rix->revindex[i].offset = ntohl(hl); + rix->revindex[i].nr = i; + } + /* This knows the pack format -- the 20-byte trailer + * follows immediately after the last object data. + */ + rix->revindex[num_ent].offset = p->pack_size - 20; + rix->revindex[num_ent].nr = -1; + qsort(rix->revindex, num_ent, sizeof(*rix->revindex), cmp_offset); +} + +static struct revindex_entry * find_packed_object(struct packed_git *p, + unsigned int ofs) +{ + int num; + int lo, hi; + struct pack_revindex *rix; + struct revindex_entry *revindex; + num = pack_revindex_ix(p); + if (num < 0) + die("internal error: pack revindex uninitialized"); + rix = &pack_revindex[num]; + if (!rix->revindex) + prepare_pack_revindex(rix); + revindex = rix->revindex; + lo = 0; + hi = num_packed_objects(p) + 1; + do { + int mi = (lo + hi) / 2; + if (revindex[mi].offset == ofs) { + return revindex + mi; + } + else if (ofs < revindex[mi].offset) + hi = mi; + else + lo = mi + 1; + } while (lo < hi); + die("internal error: pack revindex corrupt"); +} + +static unsigned long find_packed_object_size(struct packed_git *p, + unsigned long ofs) +{ + struct revindex_entry *entry = find_packed_object(p, ofs); + return entry[1].offset - ofs; +} + +static unsigned char *find_packed_object_name(struct packed_git *p, + unsigned long ofs) +{ + struct revindex_entry *entry = find_packed_object(p, ofs); + return (unsigned char *)(p->index_base + 256) + 24 * entry->nr + 4; +} + +static void *delta_against(void *buf, unsigned long size, struct object_entry *entry) +{ + unsigned long othersize, delta_size; + char type[10]; + void *otherbuf = read_sha1_file(entry->delta->sha1, type, &othersize); + void *delta_buf; + + if (!otherbuf) + die("unable to read %s", sha1_to_hex(entry->delta->sha1)); + delta_buf = diff_delta(otherbuf, othersize, + buf, size, &delta_size, 0); + if (!delta_buf || delta_size != entry->delta_size) + die("delta size changed"); + free(buf); + free(otherbuf); + return delta_buf; +} + +/* + * The per-object header is a pretty dense thing, which is + * - first byte: low four bits are "size", then three bits of "type", + * and the high bit is "size continues". + * - each byte afterwards: low seven bits are size continuation, + * with the high bit being "size continues" + */ +static int encode_header(enum object_type type, unsigned long size, unsigned char *hdr) +{ + int n = 1; + unsigned char c; + + if (type < OBJ_COMMIT || type > OBJ_REF_DELTA) + die("bad type %d", type); + + c = (type << 4) | (size & 15); + size >>= 4; + while (size) { + *hdr++ = c | 0x80; + c = size & 0x7f; + size >>= 7; + n++; + } + *hdr = c; + return n; +} + +/* + * we are going to reuse the existing object data as is. make + * sure it is not corrupt. + */ +static int check_pack_inflate(struct packed_git *p, + struct pack_window **w_curs, + unsigned long offset, + unsigned long len, + unsigned long expect) +{ + z_stream stream; + unsigned char fakebuf[4096], *in; + int st; + + memset(&stream, 0, sizeof(stream)); + inflateInit(&stream); + do { + in = use_pack(p, w_curs, offset, &stream.avail_in); + stream.next_in = in; + stream.next_out = fakebuf; + stream.avail_out = sizeof(fakebuf); + st = inflate(&stream, Z_FINISH); + offset += stream.next_in - in; + } while (st == Z_OK || st == Z_BUF_ERROR); + inflateEnd(&stream); + return (st == Z_STREAM_END && + stream.total_out == expect && + stream.total_in == len) ? 0 : -1; +} + +static void copy_pack_data(struct sha1file *f, + struct packed_git *p, + struct pack_window **w_curs, + unsigned long offset, + unsigned long len) +{ + unsigned char *in; + unsigned int avail; + + while (len) { + in = use_pack(p, w_curs, offset, &avail); + if (avail > len) + avail = len; + sha1write(f, in, avail); + offset += avail; + len -= avail; + } +} + +static int check_loose_inflate(unsigned char *data, unsigned long len, unsigned long expect) +{ + z_stream stream; + unsigned char fakebuf[4096]; + int st; + + memset(&stream, 0, sizeof(stream)); + stream.next_in = data; + stream.avail_in = len; + stream.next_out = fakebuf; + stream.avail_out = sizeof(fakebuf); + inflateInit(&stream); + + while (1) { + st = inflate(&stream, Z_FINISH); + if (st == Z_STREAM_END || st == Z_OK) { + st = (stream.total_out == expect && + stream.total_in == len) ? 0 : -1; + break; + } + if (st != Z_BUF_ERROR) { + st = -1; + break; + } + stream.next_out = fakebuf; + stream.avail_out = sizeof(fakebuf); + } + inflateEnd(&stream); + return st; +} + +static int revalidate_loose_object(struct object_entry *entry, + unsigned char *map, + unsigned long mapsize) +{ + /* we already know this is a loose object with new type header. */ + enum object_type type; + unsigned long size, used; + + if (pack_to_stdout) + return 0; + + used = unpack_object_header_gently(map, mapsize, &type, &size); + if (!used) + return -1; + map += used; + mapsize -= used; + return check_loose_inflate(map, mapsize, size); +} + +static unsigned long write_object(struct sha1file *f, + struct object_entry *entry) +{ + unsigned long size; + char type[10]; + void *buf; + unsigned char header[10]; + unsigned hdrlen, datalen; + enum object_type obj_type; + int to_reuse = 0; + + obj_type = entry->type; + if (! entry->in_pack) + to_reuse = 0; /* can't reuse what we don't have */ + else if (obj_type == OBJ_REF_DELTA || obj_type == OBJ_OFS_DELTA) + to_reuse = 1; /* check_object() decided it for us */ + else if (obj_type != entry->in_pack_type) + to_reuse = 0; /* pack has delta which is unusable */ + else if (entry->delta) + to_reuse = 0; /* we want to pack afresh */ + else + to_reuse = 1; /* we have it in-pack undeltified, + * and we do not need to deltify it. + */ + + if (!entry->in_pack && !entry->delta) { + unsigned char *map; + unsigned long mapsize; + map = map_sha1_file(entry->sha1, &mapsize); + if (map && !legacy_loose_object(map)) { + /* We can copy straight into the pack file */ + if (revalidate_loose_object(entry, map, mapsize)) + die("corrupt loose object %s", + sha1_to_hex(entry->sha1)); + sha1write(f, map, mapsize); + munmap(map, mapsize); + written++; + reused++; + return mapsize; + } + if (map) + munmap(map, mapsize); + } + + if (!to_reuse) { + buf = read_sha1_file(entry->sha1, type, &size); + if (!buf) + die("unable to read %s", sha1_to_hex(entry->sha1)); + if (size != entry->size) + die("object %s size inconsistency (%lu vs %lu)", + sha1_to_hex(entry->sha1), size, entry->size); + if (entry->delta) { + buf = delta_against(buf, size, entry); + size = entry->delta_size; + obj_type = (allow_ofs_delta && entry->delta->offset) ? + OBJ_OFS_DELTA : OBJ_REF_DELTA; + } + /* + * The object header is a byte of 'type' followed by zero or + * more bytes of length. + */ + hdrlen = encode_header(obj_type, size, header); + sha1write(f, header, hdrlen); + + if (obj_type == OBJ_OFS_DELTA) { + /* + * Deltas with relative base contain an additional + * encoding of the relative offset for the delta + * base from this object's position in the pack. + */ + unsigned long ofs = entry->offset - entry->delta->offset; + unsigned pos = sizeof(header) - 1; + header[pos] = ofs & 127; + while (ofs >>= 7) + header[--pos] = 128 | (--ofs & 127); + sha1write(f, header + pos, sizeof(header) - pos); + hdrlen += sizeof(header) - pos; + } else if (obj_type == OBJ_REF_DELTA) { + /* + * Deltas with a base reference contain + * an additional 20 bytes for the base sha1. + */ + sha1write(f, entry->delta->sha1, 20); + hdrlen += 20; + } + datalen = sha1write_compressed(f, buf, size); + free(buf); + } + else { + struct packed_git *p = entry->in_pack; + struct pack_window *w_curs = NULL; + unsigned long offset; + + if (entry->delta) { + obj_type = (allow_ofs_delta && entry->delta->offset) ? + OBJ_OFS_DELTA : OBJ_REF_DELTA; + reused_delta++; + } + hdrlen = encode_header(obj_type, entry->size, header); + sha1write(f, header, hdrlen); + if (obj_type == OBJ_OFS_DELTA) { + unsigned long ofs = entry->offset - entry->delta->offset; + unsigned pos = sizeof(header) - 1; + header[pos] = ofs & 127; + while (ofs >>= 7) + header[--pos] = 128 | (--ofs & 127); + sha1write(f, header + pos, sizeof(header) - pos); + hdrlen += sizeof(header) - pos; + } else if (obj_type == OBJ_REF_DELTA) { + sha1write(f, entry->delta->sha1, 20); + hdrlen += 20; + } + + offset = entry->in_pack_offset + entry->in_pack_header_size; + datalen = find_packed_object_size(p, entry->in_pack_offset) + - entry->in_pack_header_size; + if (!pack_to_stdout && check_pack_inflate(p, &w_curs, + offset, datalen, entry->size)) + die("corrupt delta in pack %s", sha1_to_hex(entry->sha1)); + copy_pack_data(f, p, &w_curs, offset, datalen); + unuse_pack(&w_curs); + reused++; + } + if (entry->delta) + written_delta++; + written++; + return hdrlen + datalen; +} + +static unsigned long write_one(struct sha1file *f, + struct object_entry *e, + unsigned long offset) +{ + if (e->offset || e->preferred_base) + /* offset starts from header size and cannot be zero + * if it is written already. + */ + return offset; + /* if we are deltified, write out its base object first. */ + if (e->delta) + offset = write_one(f, e->delta, offset); + e->offset = offset; + return offset + write_object(f, e); +} + +static void write_pack_file(void) +{ + int i; + struct sha1file *f; + unsigned long offset; + struct pack_header hdr; + unsigned last_percent = 999; + int do_progress = progress; + + if (!base_name) { + f = sha1fd(1, "<stdout>"); + do_progress >>= 1; + } + else + f = sha1create("%s-%s.%s", base_name, + sha1_to_hex(object_list_sha1), "pack"); + if (do_progress) + fprintf(stderr, "Writing %d objects.\n", nr_result); + + hdr.hdr_signature = htonl(PACK_SIGNATURE); + hdr.hdr_version = htonl(PACK_VERSION); + hdr.hdr_entries = htonl(nr_result); + sha1write(f, &hdr, sizeof(hdr)); + offset = sizeof(hdr); + if (!nr_result) + goto done; + for (i = 0; i < nr_objects; i++) { + offset = write_one(f, objects + i, offset); + if (do_progress) { + unsigned percent = written * 100 / nr_result; + if (progress_update || percent != last_percent) { + fprintf(stderr, "%4u%% (%u/%u) done\r", + percent, written, nr_result); + progress_update = 0; + last_percent = percent; + } + } + } + if (do_progress) + fputc('\n', stderr); + done: + if (written != nr_result) + die("wrote %d objects while expecting %d", written, nr_result); + sha1close(f, pack_file_sha1, 1); +} + +static void write_index_file(void) +{ + int i; + struct sha1file *f = sha1create("%s-%s.%s", base_name, + sha1_to_hex(object_list_sha1), "idx"); + struct object_entry **list = sorted_by_sha; + struct object_entry **last = list + nr_result; + uint32_t array[256]; + + /* + * Write the first-level table (the list is sorted, + * but we use a 256-entry lookup to be able to avoid + * having to do eight extra binary search iterations). + */ + for (i = 0; i < 256; i++) { + struct object_entry **next = list; + while (next < last) { + struct object_entry *entry = *next; + if (entry->sha1[0] != i) + break; + next++; + } + array[i] = htonl(next - sorted_by_sha); + list = next; + } + sha1write(f, array, 256 * 4); + + /* + * Write the actual SHA1 entries.. + */ + list = sorted_by_sha; + for (i = 0; i < nr_result; i++) { + struct object_entry *entry = *list++; + uint32_t offset = htonl(entry->offset); + sha1write(f, &offset, 4); + sha1write(f, entry->sha1, 20); + } + sha1write(f, pack_file_sha1, 20); + sha1close(f, NULL, 1); +} + +static int locate_object_entry_hash(const unsigned char *sha1) +{ + int i; + unsigned int ui; + memcpy(&ui, sha1, sizeof(unsigned int)); + i = ui % object_ix_hashsz; + while (0 < object_ix[i]) { + if (!hashcmp(sha1, objects[object_ix[i] - 1].sha1)) + return i; + if (++i == object_ix_hashsz) + i = 0; + } + return -1 - i; +} + +static struct object_entry *locate_object_entry(const unsigned char *sha1) +{ + int i; + + if (!object_ix_hashsz) + return NULL; + + i = locate_object_entry_hash(sha1); + if (0 <= i) + return &objects[object_ix[i]-1]; + return NULL; +} + +static void rehash_objects(void) +{ + int i; + struct object_entry *oe; + + object_ix_hashsz = nr_objects * 3; + if (object_ix_hashsz < 1024) + object_ix_hashsz = 1024; + object_ix = xrealloc(object_ix, sizeof(int) * object_ix_hashsz); + memset(object_ix, 0, sizeof(int) * object_ix_hashsz); + for (i = 0, oe = objects; i < nr_objects; i++, oe++) { + int ix = locate_object_entry_hash(oe->sha1); + if (0 <= ix) + continue; + ix = -1 - ix; + object_ix[ix] = i + 1; + } +} + +static unsigned name_hash(const char *name) +{ + unsigned char c; + unsigned hash = 0; + + /* + * 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. + */ + while ((c = *name++) != 0) { + if (isspace(c)) + continue; + hash = (hash >> 2) + (c << 24); + } + return hash; +} + +static int add_object_entry(const unsigned char *sha1, unsigned hash, int exclude) +{ + unsigned int idx = nr_objects; + struct object_entry *entry; + struct packed_git *p; + unsigned int found_offset = 0; + struct packed_git *found_pack = NULL; + int ix, status = 0; + + if (!exclude) { + for (p = packed_git; p; p = p->next) { + unsigned long offset = find_pack_entry_one(sha1, p); + if (offset) { + if (incremental) + return 0; + if (local && !p->pack_local) + return 0; + if (!found_pack) { + found_offset = offset; + found_pack = p; + } + } + } + } + if ((entry = locate_object_entry(sha1)) != NULL) + goto already_added; + + if (idx >= nr_alloc) { + unsigned int needed = (idx + 1024) * 3 / 2; + objects = xrealloc(objects, needed * sizeof(*entry)); + nr_alloc = needed; + } + entry = objects + idx; + nr_objects = idx + 1; + memset(entry, 0, sizeof(*entry)); + hashcpy(entry->sha1, sha1); + entry->hash = hash; + + if (object_ix_hashsz * 3 <= nr_objects * 4) + rehash_objects(); + else { + ix = locate_object_entry_hash(entry->sha1); + if (0 <= ix) + die("internal error in object hashing."); + object_ix[-1 - ix] = idx + 1; + } + status = 1; + + already_added: + if (progress_update) { + fprintf(stderr, "Counting objects...%d\r", nr_objects); + progress_update = 0; + } + if (exclude) + entry->preferred_base = 1; + else { + if (found_pack) { + entry->in_pack = found_pack; + entry->in_pack_offset = found_offset; + } + } + return status; +} + +struct pbase_tree_cache { + unsigned char sha1[20]; + int ref; + int temporary; + void *tree_data; + unsigned long tree_size; +}; + +static struct pbase_tree_cache *(pbase_tree_cache[256]); +static int pbase_tree_cache_ix(const unsigned char *sha1) +{ + return sha1[0] % ARRAY_SIZE(pbase_tree_cache); +} +static int pbase_tree_cache_ix_incr(int ix) +{ + return (ix+1) % ARRAY_SIZE(pbase_tree_cache); +} + +static struct pbase_tree { + struct pbase_tree *next; + /* This is a phony "cache" entry; we are not + * going to evict it nor find it through _get() + * mechanism -- this is for the toplevel node that + * would almost always change with any commit. + */ + struct pbase_tree_cache pcache; +} *pbase_tree; + +static struct pbase_tree_cache *pbase_tree_get(const unsigned char *sha1) +{ + struct pbase_tree_cache *ent, *nent; + void *data; + unsigned long size; + char type[20]; + int neigh; + int my_ix = pbase_tree_cache_ix(sha1); + int available_ix = -1; + + /* pbase-tree-cache acts as a limited hashtable. + * your object will be found at your index or within a few + * slots after that slot if it is cached. + */ + for (neigh = 0; neigh < 8; neigh++) { + ent = pbase_tree_cache[my_ix]; + if (ent && !hashcmp(ent->sha1, sha1)) { + ent->ref++; + return ent; + } + else if (((available_ix < 0) && (!ent || !ent->ref)) || + ((0 <= available_ix) && + (!ent && pbase_tree_cache[available_ix]))) + available_ix = my_ix; + if (!ent) + break; + my_ix = pbase_tree_cache_ix_incr(my_ix); + } + + /* Did not find one. Either we got a bogus request or + * we need to read and perhaps cache. + */ + data = read_sha1_file(sha1, type, &size); + if (!data) + return NULL; + if (strcmp(type, tree_type)) { + free(data); + return NULL; + } + + /* We need to either cache or return a throwaway copy */ + + if (available_ix < 0) + ent = NULL; + else { + ent = pbase_tree_cache[available_ix]; + my_ix = available_ix; + } + + if (!ent) { + nent = xmalloc(sizeof(*nent)); + nent->temporary = (available_ix < 0); + } + else { + /* evict and reuse */ + free(ent->tree_data); + nent = ent; + } + hashcpy(nent->sha1, sha1); + nent->tree_data = data; + nent->tree_size = size; + nent->ref = 1; + if (!nent->temporary) + pbase_tree_cache[my_ix] = nent; + return nent; +} + +static void pbase_tree_put(struct pbase_tree_cache *cache) +{ + if (!cache->temporary) { + cache->ref--; + return; + } + free(cache->tree_data); + free(cache); +} + +static int name_cmp_len(const char *name) +{ + int i; + for (i = 0; name[i] && name[i] != '\n' && name[i] != '/'; i++) + ; + return i; +} + +static void add_pbase_object(struct tree_desc *tree, + const char *name, + int cmplen, + const char *fullname) +{ + struct name_entry entry; + + while (tree_entry(tree,&entry)) { + unsigned long size; + char type[20]; + + 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(fullname); + add_object_entry(entry.sha1, hash, 1); + return; + } + if (!strcmp(type, tree_type)) { + struct tree_desc sub; + struct pbase_tree_cache *tree; + const char *down = name+cmplen+1; + int downlen = name_cmp_len(down); + + tree = pbase_tree_get(entry.sha1); + if (!tree) + return; + sub.buf = tree->tree_data; + sub.size = tree->tree_size; + + add_pbase_object(&sub, down, downlen, fullname); + pbase_tree_put(tree); + } + } +} + +static unsigned *done_pbase_paths; +static int done_pbase_paths_num; +static int done_pbase_paths_alloc; +static int done_pbase_path_pos(unsigned hash) +{ + int lo = 0; + int hi = done_pbase_paths_num; + while (lo < hi) { + int mi = (hi + lo) / 2; + if (done_pbase_paths[mi] == hash) + return mi; + if (done_pbase_paths[mi] < hash) + hi = mi; + else + lo = mi + 1; + } + return -lo-1; +} + +static int check_pbase_path(unsigned hash) +{ + int pos = (!done_pbase_paths) ? -1 : done_pbase_path_pos(hash); + if (0 <= pos) + return 1; + pos = -pos - 1; + if (done_pbase_paths_alloc <= done_pbase_paths_num) { + done_pbase_paths_alloc = alloc_nr(done_pbase_paths_alloc); + done_pbase_paths = xrealloc(done_pbase_paths, + done_pbase_paths_alloc * + sizeof(unsigned)); + } + done_pbase_paths_num++; + if (pos < done_pbase_paths_num) + memmove(done_pbase_paths + pos + 1, + done_pbase_paths + pos, + (done_pbase_paths_num - pos - 1) * sizeof(unsigned)); + done_pbase_paths[pos] = hash; + return 0; +} + +static void add_preferred_base_object(const char *name, unsigned hash) +{ + struct pbase_tree *it; + int cmplen = name_cmp_len(name); + + if (check_pbase_path(hash)) + return; + + for (it = pbase_tree; it; it = it->next) { + if (cmplen == 0) { + 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, name, cmplen, name); + } + } +} + +static void add_preferred_base(unsigned char *sha1) +{ + struct pbase_tree *it; + void *data; + unsigned long size; + unsigned char tree_sha1[20]; + + if (window <= num_preferred_base++) + return; + + data = read_object_with_reference(sha1, tree_type, &size, tree_sha1); + if (!data) + return; + + for (it = pbase_tree; it; it = it->next) { + if (!hashcmp(it->pcache.sha1, tree_sha1)) { + free(data); + return; + } + } + + it = xcalloc(1, sizeof(*it)); + it->next = pbase_tree; + pbase_tree = it; + + hashcpy(it->pcache.sha1, tree_sha1); + it->pcache.tree_data = data; + it->pcache.tree_size = size; +} + +static void check_object(struct object_entry *entry) +{ + char type[20]; + + if (entry->in_pack && !entry->preferred_base) { + struct packed_git *p = entry->in_pack; + struct pack_window *w_curs = NULL; + unsigned long left = p->pack_size - entry->in_pack_offset; + unsigned long size, used; + unsigned char *buf; + struct object_entry *base_entry = NULL; + + buf = use_pack(p, &w_curs, entry->in_pack_offset, NULL); + + /* We want in_pack_type even if we do not reuse delta. + * There is no point not reusing non-delta representations. + */ + used = unpack_object_header_gently(buf, left, + &entry->in_pack_type, &size); + + /* Check if it is delta, and the base is also an object + * we are going to pack. If so we will reuse the existing + * delta. + */ + if (!no_reuse_delta) { + unsigned char c, *base_name; + unsigned long ofs; + unsigned long used_0; + /* there is at least 20 bytes left in the pack */ + switch (entry->in_pack_type) { + case OBJ_REF_DELTA: + base_name = use_pack(p, &w_curs, + entry->in_pack_offset + used, NULL); + used += 20; + break; + case OBJ_OFS_DELTA: + buf = use_pack(p, &w_curs, + entry->in_pack_offset + used, NULL); + used_0 = 0; + c = buf[used_0++]; + ofs = c & 127; + while (c & 128) { + ofs += 1; + if (!ofs || ofs & ~(~0UL >> 7)) + die("delta base offset overflow in pack for %s", + sha1_to_hex(entry->sha1)); + c = buf[used_0++]; + ofs = (ofs << 7) + (c & 127); + } + if (ofs >= entry->in_pack_offset) + die("delta base offset out of bound for %s", + sha1_to_hex(entry->sha1)); + ofs = entry->in_pack_offset - ofs; + base_name = find_packed_object_name(p, ofs); + used += used_0; + break; + default: + base_name = NULL; + } + if (base_name) + base_entry = locate_object_entry(base_name); + } + unuse_pack(&w_curs); + entry->in_pack_header_size = used; + + if (base_entry) { + + /* Depth value does not matter - find_deltas() + * will never consider reused delta as the + * base object to deltify other objects + * against, in order to avoid circular deltas. + */ + + /* uncompressed size of the delta data */ + entry->size = size; + entry->delta = base_entry; + entry->type = entry->in_pack_type; + + entry->delta_sibling = base_entry->delta_child; + base_entry->delta_child = entry; + + return; + } + /* Otherwise we would do the usual */ + } + + if (sha1_object_info(entry->sha1, type, &entry->size)) + die("unable to get type of object %s", + sha1_to_hex(entry->sha1)); + + if (!strcmp(type, commit_type)) { + entry->type = OBJ_COMMIT; + } else if (!strcmp(type, tree_type)) { + entry->type = OBJ_TREE; + } else if (!strcmp(type, blob_type)) { + entry->type = OBJ_BLOB; + } else if (!strcmp(type, tag_type)) { + entry->type = OBJ_TAG; + } else + die("unable to pack object %s of type %s", + sha1_to_hex(entry->sha1), type); +} + +static unsigned int check_delta_limit(struct object_entry *me, unsigned int n) +{ + struct object_entry *child = me->delta_child; + unsigned int m = n; + while (child) { + unsigned int c = check_delta_limit(child, n + 1); + if (m < c) + m = c; + child = child->delta_sibling; + } + return m; +} + +static void get_object_details(void) +{ + int i; + struct object_entry *entry; + + prepare_pack_ix(); + for (i = 0, entry = objects; i < nr_objects; i++, entry++) + check_object(entry); + + if (nr_objects == nr_result) { + /* + * Depth of objects that depend on the entry -- this + * is subtracted from depth-max to break too deep + * delta chain because of delta data reusing. + * However, we loosen this restriction when we know we + * are creating a thin pack -- it will have to be + * expanded on the other end anyway, so do not + * artificially cut the delta chain and let it go as + * deep as it wants. + */ + for (i = 0, entry = objects; i < nr_objects; i++, entry++) + if (!entry->delta && entry->delta_child) + entry->delta_limit = + check_delta_limit(entry, 1); + } +} + +typedef int (*entry_sort_t)(const struct object_entry *, const struct object_entry *); + +static entry_sort_t current_sort; + +static int sort_comparator(const void *_a, const void *_b) +{ + struct object_entry *a = *(struct object_entry **)_a; + struct object_entry *b = *(struct object_entry **)_b; + return current_sort(a,b); +} + +static struct object_entry **create_sorted_list(entry_sort_t sort) +{ + struct object_entry **list = xmalloc(nr_objects * sizeof(struct object_entry *)); + int i; + + for (i = 0; i < nr_objects; i++) + list[i] = objects + i; + current_sort = sort; + qsort(list, nr_objects, sizeof(struct object_entry *), sort_comparator); + return list; +} + +static int sha1_sort(const struct object_entry *a, const struct object_entry *b) +{ + return hashcmp(a->sha1, b->sha1); +} + +static struct object_entry **create_final_object_list(void) +{ + struct object_entry **list; + int i, j; + + for (i = nr_result = 0; i < nr_objects; i++) + if (!objects[i].preferred_base) + nr_result++; + list = xmalloc(nr_result * sizeof(struct object_entry *)); + for (i = j = 0; i < nr_objects; i++) { + if (!objects[i].preferred_base) + list[j++] = objects + i; + } + current_sort = sha1_sort; + qsort(list, nr_result, sizeof(struct object_entry *), sort_comparator); + return list; +} + +static int type_size_sort(const struct object_entry *a, const struct object_entry *b) +{ + if (a->type < b->type) + return -1; + if (a->type > b->type) + return 1; + if (a->hash < b->hash) + return -1; + if (a->hash > b->hash) + return 1; + if (a->preferred_base < b->preferred_base) + return -1; + if (a->preferred_base > b->preferred_base) + return 1; + if (a->size < b->size) + return -1; + if (a->size > b->size) + return 1; + return a < b ? -1 : (a > b); +} + +struct unpacked { + struct object_entry *entry; + void *data; + struct delta_index *index; +}; + +/* + * We search for deltas _backwards_ in a list sorted by type and + * by size, so that we see progressively smaller and smaller files. + * That's because we prefer deltas to be from the bigger file + * to the smaller - deletes are potentially cheaper, but perhaps + * more importantly, the bigger file is likely the more recent + * one. + */ +static int try_delta(struct unpacked *trg, struct unpacked *src, + unsigned max_depth) +{ + struct object_entry *trg_entry = trg->entry; + struct object_entry *src_entry = src->entry; + unsigned long trg_size, src_size, delta_size, sizediff, max_size, sz; + char type[10]; + void *delta_buf; + + /* Don't bother doing diffs between different types */ + if (trg_entry->type != src_entry->type) + return -1; + + /* We do not compute delta to *create* objects we are not + * going to pack. + */ + if (trg_entry->preferred_base) + return -1; + + /* + * We do not bother to try a delta that we discarded + * on an earlier try, but only when reusing delta data. + */ + if (!no_reuse_delta && trg_entry->in_pack && + trg_entry->in_pack == src_entry->in_pack && + trg_entry->in_pack_type != OBJ_REF_DELTA && + trg_entry->in_pack_type != OBJ_OFS_DELTA) + return 0; + + /* + * If the current object is at pack edge, take the depth the + * objects that depend on the current object into account -- + * otherwise they would become too deep. + */ + if (trg_entry->delta_child) { + if (max_depth <= trg_entry->delta_limit) + return 0; + max_depth -= trg_entry->delta_limit; + } + if (src_entry->depth >= max_depth) + return 0; + + /* Now some size filtering heuristics. */ + trg_size = trg_entry->size; + max_size = trg_size/2 - 20; + max_size = max_size * (max_depth - src_entry->depth) / max_depth; + if (max_size == 0) + return 0; + if (trg_entry->delta && trg_entry->delta_size <= max_size) + max_size = trg_entry->delta_size-1; + src_size = src_entry->size; + sizediff = src_size < trg_size ? trg_size - src_size : 0; + if (sizediff >= max_size) + return 0; + + /* Load data if not already done */ + if (!trg->data) { + trg->data = read_sha1_file(trg_entry->sha1, type, &sz); + if (sz != trg_size) + die("object %s inconsistent object length (%lu vs %lu)", + sha1_to_hex(trg_entry->sha1), sz, trg_size); + } + if (!src->data) { + src->data = read_sha1_file(src_entry->sha1, type, &sz); + if (sz != src_size) + die("object %s inconsistent object length (%lu vs %lu)", + sha1_to_hex(src_entry->sha1), sz, src_size); + } + if (!src->index) { + src->index = create_delta_index(src->data, src_size); + if (!src->index) + die("out of memory"); + } + + delta_buf = create_delta(src->index, trg->data, trg_size, &delta_size, max_size); + if (!delta_buf) + return 0; + + trg_entry->delta = src_entry; + trg_entry->delta_size = delta_size; + trg_entry->depth = src_entry->depth + 1; + free(delta_buf); + return 1; +} + +static void progress_interval(int signum) +{ + progress_update = 1; +} + +static void find_deltas(struct object_entry **list, int window, int depth) +{ + int i, idx; + unsigned int array_size = window * sizeof(struct unpacked); + struct unpacked *array = xmalloc(array_size); + unsigned processed = 0; + unsigned last_percent = 999; + + memset(array, 0, array_size); + i = nr_objects; + idx = 0; + if (progress) + fprintf(stderr, "Deltifying %d objects.\n", nr_result); + + while (--i >= 0) { + struct object_entry *entry = list[i]; + struct unpacked *n = array + idx; + int j; + + if (!entry->preferred_base) + processed++; + + if (progress) { + unsigned percent = processed * 100 / nr_result; + if (percent != last_percent || progress_update) { + fprintf(stderr, "%4u%% (%u/%u) done\r", + percent, processed, nr_result); + progress_update = 0; + last_percent = percent; + } + } + + if (entry->delta) + /* This happens if we decided to reuse existing + * delta from a pack. "!no_reuse_delta &&" is implied. + */ + continue; + + if (entry->size < 50) + continue; + free_delta_index(n->index); + n->index = NULL; + free(n->data); + n->data = NULL; + n->entry = entry; + + j = window; + while (--j > 0) { + unsigned int other_idx = idx + j; + struct unpacked *m; + if (other_idx >= window) + other_idx -= window; + m = array + other_idx; + if (!m->entry) + break; + if (try_delta(n, m, depth) < 0) + break; + } + /* if we made n a delta, and if n is already at max + * depth, leaving it in the window is pointless. we + * should evict it first. + */ + if (entry->delta && depth <= entry->depth) + continue; + + idx++; + if (idx >= window) + idx = 0; + } + + if (progress) + fputc('\n', stderr); + + for (i = 0; i < window; ++i) { + free_delta_index(array[i].index); + free(array[i].data); + } + free(array); +} + +static void prepare_pack(int window, int depth) +{ + get_object_details(); + sorted_by_type = create_sorted_list(type_size_sort); + if (window && depth) + find_deltas(sorted_by_type, window+1, depth); +} + +static int reuse_cached_pack(unsigned char *sha1) +{ + static const char cache[] = "pack-cache/pack-%s.%s"; + char *cached_pack, *cached_idx; + int ifd, ofd, ifd_ix = -1; + + cached_pack = git_path(cache, sha1_to_hex(sha1), "pack"); + ifd = open(cached_pack, O_RDONLY); + if (ifd < 0) + return 0; + + if (!pack_to_stdout) { + cached_idx = git_path(cache, sha1_to_hex(sha1), "idx"); + ifd_ix = open(cached_idx, O_RDONLY); + if (ifd_ix < 0) { + close(ifd); + return 0; + } + } + + if (progress) + fprintf(stderr, "Reusing %d objects pack %s\n", nr_objects, + sha1_to_hex(sha1)); + + if (pack_to_stdout) { + if (copy_fd(ifd, 1)) + exit(1); + close(ifd); + } + else { + char name[PATH_MAX]; + snprintf(name, sizeof(name), + "%s-%s.%s", base_name, sha1_to_hex(sha1), "pack"); + ofd = open(name, O_CREAT | O_EXCL | O_WRONLY, 0666); + if (ofd < 0) + die("unable to open %s (%s)", name, strerror(errno)); + if (copy_fd(ifd, ofd)) + exit(1); + close(ifd); + + snprintf(name, sizeof(name), + "%s-%s.%s", base_name, sha1_to_hex(sha1), "idx"); + ofd = open(name, O_CREAT | O_EXCL | O_WRONLY, 0666); + if (ofd < 0) + die("unable to open %s (%s)", name, strerror(errno)); + if (copy_fd(ifd_ix, ofd)) + exit(1); + close(ifd_ix); + puts(sha1_to_hex(sha1)); + } + + return 1; +} + +static void setup_progress_signal(void) +{ + struct sigaction sa; + struct itimerval v; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = progress_interval; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction(SIGALRM, &sa, NULL); + + v.it_interval.tv_sec = 1; + v.it_interval.tv_usec = 0; + v.it_value = v.it_interval; + setitimer(ITIMER_REAL, &v, NULL); +} + +static int git_pack_config(const char *k, const char *v) +{ + if(!strcmp(k, "pack.window")) { + window = git_config_int(k, v); + return 0; + } + return git_default_config(k, v); +} + +static void read_object_list_from_stdin(void) +{ + char line[40 + 1 + PATH_MAX + 2]; + unsigned char sha1[20]; + unsigned hash; + + for (;;) { + if (!fgets(line, sizeof(line), stdin)) { + if (feof(stdin)) + break; + if (!ferror(stdin)) + die("fgets returned NULL, not EOF, not error!"); + if (errno != EINTR) + die("fgets: %s", strerror(errno)); + clearerr(stdin); + continue; + } + if (line[0] == '-') { + if (get_sha1_hex(line+1, sha1)) + die("expected edge sha1, got garbage:\n %s", + line); + add_preferred_base(sha1); + continue; + } + if (get_sha1_hex(line, sha1)) + die("expected sha1, got garbage:\n %s", line); + + hash = name_hash(line+41); + add_preferred_base_object(line+41, hash); + add_object_entry(sha1, hash, 0); + } +} + +static void show_commit(struct commit *commit) +{ + unsigned hash = name_hash(""); + add_preferred_base_object("", hash); + add_object_entry(commit->object.sha1, hash, 0); +} + +static void show_object(struct object_array_entry *p) +{ + unsigned hash = name_hash(p->name); + add_preferred_base_object(p->name, hash); + add_object_entry(p->item->sha1, hash, 0); +} + +static void show_edge(struct commit *commit) +{ + add_preferred_base(commit->object.sha1); +} + +static void get_object_list(int ac, const char **av) +{ + struct rev_info revs; + char line[1000]; + int flags = 0; + + init_revisions(&revs, NULL); + save_commit_buffer = 0; + track_object_refs = 0; + setup_revisions(ac, av, &revs, NULL); + + while (fgets(line, sizeof(line), stdin) != NULL) { + int len = strlen(line); + if (line[len - 1] == '\n') + line[--len] = 0; + if (!len) + break; + if (*line == '-') { + if (!strcmp(line, "--not")) { + flags ^= UNINTERESTING; + continue; + } + die("not a rev '%s'", line); + } + if (handle_revision_arg(line, &revs, flags, 1)) + die("bad revision '%s'", line); + } + + prepare_revision_walk(&revs); + mark_edges_uninteresting(revs.commits, &revs, show_edge); + traverse_commit_list(&revs, show_commit, show_object); +} + +int cmd_pack_objects(int argc, const char **argv, const char *prefix) +{ + SHA_CTX ctx; + int depth = 10; + struct object_entry **list; + int use_internal_rev_list = 0; + int thin = 0; + int i; + const char *rp_av[64]; + int rp_ac; + + rp_av[0] = "pack-objects"; + rp_av[1] = "--objects"; /* --thin will make it --objects-edge */ + rp_ac = 2; + + git_config(git_pack_config); + + progress = isatty(2); + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg != '-') + break; + + if (!strcmp("--non-empty", arg)) { + non_empty = 1; + continue; + } + if (!strcmp("--local", arg)) { + local = 1; + continue; + } + if (!strcmp("--incremental", arg)) { + incremental = 1; + continue; + } + if (!strncmp("--window=", arg, 9)) { + char *end; + window = strtoul(arg+9, &end, 0); + if (!arg[9] || *end) + usage(pack_usage); + continue; + } + if (!strncmp("--depth=", arg, 8)) { + char *end; + depth = strtoul(arg+8, &end, 0); + if (!arg[8] || *end) + usage(pack_usage); + continue; + } + if (!strcmp("--progress", arg)) { + progress = 1; + continue; + } + if (!strcmp("--all-progress", arg)) { + progress = 2; + continue; + } + if (!strcmp("-q", arg)) { + progress = 0; + continue; + } + if (!strcmp("--no-reuse-delta", arg)) { + no_reuse_delta = 1; + continue; + } + if (!strcmp("--delta-base-offset", arg)) { + allow_ofs_delta = 1; + continue; + } + if (!strcmp("--stdout", arg)) { + pack_to_stdout = 1; + continue; + } + if (!strcmp("--revs", arg)) { + use_internal_rev_list = 1; + continue; + } + if (!strcmp("--unpacked", arg) || + !strncmp("--unpacked=", arg, 11) || + !strcmp("--reflog", arg) || + !strcmp("--all", arg)) { + use_internal_rev_list = 1; + if (ARRAY_SIZE(rp_av) - 1 <= rp_ac) + die("too many internal rev-list options"); + rp_av[rp_ac++] = arg; + continue; + } + if (!strcmp("--thin", arg)) { + use_internal_rev_list = 1; + thin = 1; + rp_av[1] = "--objects-edge"; + continue; + } + usage(pack_usage); + } + + /* Traditionally "pack-objects [options] base extra" failed; + * we would however want to take refs parameter that would + * have been given to upstream rev-list ourselves, which means + * we somehow want to say what the base name is. So the + * syntax would be: + * + * pack-objects [options] base <refs...> + * + * in other words, we would treat the first non-option as the + * base_name and send everything else to the internal revision + * walker. + */ + + if (!pack_to_stdout) + base_name = argv[i++]; + + if (pack_to_stdout != !base_name) + usage(pack_usage); + + if (!pack_to_stdout && thin) + die("--thin cannot be used to build an indexable pack."); + + prepare_packed_git(); + + if (progress) { + fprintf(stderr, "Generating pack...\n"); + setup_progress_signal(); + } + + if (!use_internal_rev_list) + read_object_list_from_stdin(); + else { + rp_av[rp_ac] = NULL; + get_object_list(rp_ac, rp_av); + } + + if (progress) + fprintf(stderr, "Done counting %d objects.\n", nr_objects); + sorted_by_sha = create_final_object_list(); + if (non_empty && !nr_result) + return 0; + + SHA1_Init(&ctx); + list = sorted_by_sha; + for (i = 0; i < nr_result; i++) { + struct object_entry *entry = *list++; + SHA1_Update(&ctx, entry->sha1, 20); + } + SHA1_Final(object_list_sha1, &ctx); + if (progress && (nr_objects != nr_result)) + fprintf(stderr, "Result has %d objects.\n", nr_result); + + if (reuse_cached_pack(object_list_sha1)) + ; + else { + if (nr_result) + prepare_pack(window, depth); + if (progress == 1 && pack_to_stdout) { + /* the other end usually displays progress itself */ + struct itimerval v = {{0,},}; + setitimer(ITIMER_REAL, &v, NULL); + signal(SIGALRM, SIG_IGN ); + progress_update = 0; + } + write_pack_file(); + if (!pack_to_stdout) { + write_index_file(); + puts(sha1_to_hex(object_list_sha1)); + } + } + if (progress) + fprintf(stderr, "Total %d (delta %d), reused %d (delta %d)\n", + written, written_delta, reused, reused_delta); + return 0; +} diff --git a/builtin-pack-refs.c b/builtin-pack-refs.c new file mode 100644 index 0000000000..3de9b3eefd --- /dev/null +++ b/builtin-pack-refs.c @@ -0,0 +1,134 @@ +#include "cache.h" +#include "refs.h" +#include "object.h" +#include "tag.h" + +static const char builtin_pack_refs_usage[] = +"git-pack-refs [--all] [--prune | --no-prune]"; + +struct ref_to_prune { + struct ref_to_prune *next; + unsigned char sha1[20]; + char name[FLEX_ARRAY]; +}; + +struct pack_refs_cb_data { + int prune; + int all; + struct ref_to_prune *ref_to_prune; + FILE *refs_file; +}; + +static int do_not_prune(int flags) +{ + /* If it is already packed or if it is a symref, + * do not prune it. + */ + return (flags & (REF_ISSYMREF|REF_ISPACKED)); +} + +static int handle_one_ref(const char *path, const unsigned char *sha1, + int flags, void *cb_data) +{ + struct pack_refs_cb_data *cb = cb_data; + int is_tag_ref; + + /* Do not pack the symbolic refs */ + if ((flags & REF_ISSYMREF)) + return 0; + is_tag_ref = !strncmp(path, "refs/tags/", 10); + + /* ALWAYS pack refs that were already packed or are tags */ + if (!cb->all && !is_tag_ref && !(flags & REF_ISPACKED)) + return 0; + + fprintf(cb->refs_file, "%s %s\n", sha1_to_hex(sha1), path); + if (is_tag_ref) { + struct object *o = parse_object(sha1); + if (o->type == OBJ_TAG) { + o = deref_tag(o, path, 0); + if (o) + fprintf(cb->refs_file, "^%s\n", + sha1_to_hex(o->sha1)); + } + } + + if (cb->prune && !do_not_prune(flags)) { + int namelen = strlen(path) + 1; + struct ref_to_prune *n = xcalloc(1, sizeof(*n) + namelen); + hashcpy(n->sha1, sha1); + strcpy(n->name, path); + n->next = cb->ref_to_prune; + cb->ref_to_prune = n; + } + return 0; +} + +/* make sure nobody touched the ref, and unlink */ +static void prune_ref(struct ref_to_prune *r) +{ + struct ref_lock *lock = lock_ref_sha1(r->name + 5, r->sha1); + + if (lock) { + unlink(git_path("%s", r->name)); + unlock_ref(lock); + } +} + +static void prune_refs(struct ref_to_prune *r) +{ + while (r) { + prune_ref(r); + r = r->next; + } +} + +static struct lock_file packed; + +int cmd_pack_refs(int argc, const char **argv, const char *prefix) +{ + int fd, i; + struct pack_refs_cb_data cbdata; + + memset(&cbdata, 0, sizeof(cbdata)); + + cbdata.prune = 1; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--prune")) { + cbdata.prune = 1; /* now the default */ + continue; + } + if (!strcmp(arg, "--no-prune")) { + cbdata.prune = 0; + continue; + } + if (!strcmp(arg, "--all")) { + cbdata.all = 1; + continue; + } + /* perhaps other parameters later... */ + break; + } + if (i != argc) + usage(builtin_pack_refs_usage); + + fd = hold_lock_file_for_update(&packed, git_path("packed-refs"), 1); + cbdata.refs_file = fdopen(fd, "w"); + if (!cbdata.refs_file) + die("unable to create ref-pack file structure (%s)", + strerror(errno)); + + /* perhaps other traits later as well */ + fprintf(cbdata.refs_file, "# pack-refs with: peeled \n"); + + for_each_ref(handle_one_ref, &cbdata); + fflush(cbdata.refs_file); + fsync(fd); + fclose(cbdata.refs_file); + if (commit_lock_file(&packed) < 0) + die("unable to overwrite old ref-pack file (%s)", strerror(errno)); + if (cbdata.prune) + prune_refs(cbdata.ref_to_prune); + return 0; +} diff --git a/builtin-prune-packed.c b/builtin-prune-packed.c new file mode 100644 index 0000000000..977730064b --- /dev/null +++ b/builtin-prune-packed.c @@ -0,0 +1,87 @@ +#include "builtin.h" +#include "cache.h" + +static const char prune_packed_usage[] = +"git-prune-packed [-n] [-q]"; + +#define DRY_RUN 01 +#define VERBOSE 02 + +static void prune_dir(int i, DIR *dir, char *pathname, int len, int opts) +{ + struct dirent *de; + char hex[40]; + + sprintf(hex, "%02x", i); + while ((de = readdir(dir)) != NULL) { + unsigned char sha1[20]; + if (strlen(de->d_name) != 38) + continue; + memcpy(hex+2, de->d_name, 38); + if (get_sha1_hex(hex, sha1)) + continue; + if (!has_sha1_pack(sha1, NULL)) + continue; + memcpy(pathname + len, de->d_name, 38); + if (opts & DRY_RUN) + printf("rm -f %s\n", pathname); + else if (unlink(pathname) < 0) + error("unable to unlink %s", pathname); + } + pathname[len] = 0; + rmdir(pathname); +} + +void prune_packed_objects(int opts) +{ + int i; + static char pathname[PATH_MAX]; + const char *dir = get_object_directory(); + int len = strlen(dir); + + if (len > PATH_MAX - 42) + die("impossible object directory"); + memcpy(pathname, dir, len); + if (len && pathname[len-1] != '/') + pathname[len++] = '/'; + for (i = 0; i < 256; i++) { + DIR *d; + + sprintf(pathname + len, "%02x/", i); + d = opendir(pathname); + if (opts == VERBOSE && (d || i == 255)) + fprintf(stderr, "Removing unused objects %d%%...\015", + ((i+1) * 100) / 256); + if (!d) + continue; + prune_dir(i, d, pathname, len + 3, opts); + closedir(d); + } + if (opts == VERBOSE) + fprintf(stderr, "\nDone.\n"); +} + +int cmd_prune_packed(int argc, const char **argv, const char *prefix) +{ + int i; + int opts = VERBOSE; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg == '-') { + if (!strcmp(arg, "-n")) + opts |= DRY_RUN; + else if (!strcmp(arg, "-q")) + opts &= ~VERBOSE; + else + usage(prune_packed_usage); + continue; + } + /* Handle arguments here .. */ + usage(prune_packed_usage); + } + sync(); + prune_packed_objects(opts); + return 0; +} diff --git a/builtin-prune.c b/builtin-prune.c new file mode 100644 index 0000000000..6f0ba0d04d --- /dev/null +++ b/builtin-prune.c @@ -0,0 +1,105 @@ +#include "cache.h" +#include "commit.h" +#include "diff.h" +#include "revision.h" +#include "builtin.h" +#include "reachable.h" + +static const char prune_usage[] = "git-prune [-n]"; +static int show_only; + +static int prune_object(char *path, const char *filename, const unsigned char *sha1) +{ + char buf[20]; + const char *type; + + if (show_only) { + if (sha1_object_info(sha1, buf, NULL)) + type = "unknown"; + else + type = buf; + printf("%s %s\n", sha1_to_hex(sha1), type); + return 0; + } + unlink(mkpath("%s/%s", path, filename)); + rmdir(path); + return 0; +} + +static int prune_dir(int i, char *path) +{ + DIR *dir = opendir(path); + struct dirent *de; + + if (!dir) + return 0; + + while ((de = readdir(dir)) != NULL) { + char name[100]; + unsigned char sha1[20]; + int len = strlen(de->d_name); + + switch (len) { + case 2: + if (de->d_name[1] != '.') + break; + case 1: + if (de->d_name[0] != '.') + break; + continue; + case 38: + sprintf(name, "%02x", i); + memcpy(name+2, de->d_name, len+1); + if (get_sha1_hex(name, sha1) < 0) + break; + + /* + * Do we know about this object? + * It must have been reachable + */ + if (lookup_object(sha1)) + continue; + + prune_object(path, de->d_name, sha1); + continue; + } + fprintf(stderr, "bad sha1 file: %s/%s\n", path, de->d_name); + } + closedir(dir); + return 0; +} + +static void prune_object_dir(const char *path) +{ + int i; + for (i = 0; i < 256; i++) { + static char dir[4096]; + sprintf(dir, "%s/%02x", path, i); + prune_dir(i, dir); + } +} + +int cmd_prune(int argc, const char **argv, const char *prefix) +{ + int i; + struct rev_info revs; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "-n")) { + show_only = 1; + continue; + } + usage(prune_usage); + } + + save_commit_buffer = 0; + init_revisions(&revs, prefix); + mark_reachable_objects(&revs, 1); + + prune_object_dir(get_object_directory()); + + sync(); + prune_packed_objects(show_only); + return 0; +} diff --git a/builtin-push.c b/builtin-push.c new file mode 100644 index 0000000000..c45649e26c --- /dev/null +++ b/builtin-push.c @@ -0,0 +1,412 @@ +/* + * "git push" + */ +#include "cache.h" +#include "refs.h" +#include "run-command.h" +#include "builtin.h" + +#define MAX_URI (16) + +static const char push_usage[] = "git-push [--all] [--tags] [--receive-pack=<git-receive-pack>] [--repo=all] [-f | --force] [-v] [<repository> <refspec>...]"; + +static int all, tags, force, thin = 1, verbose; +static const char *receivepack; + +#define BUF_SIZE (2084) +static char buffer[BUF_SIZE]; + +static const char **refspec; +static int refspec_nr; + +static void add_refspec(const char *ref) +{ + int nr = refspec_nr + 1; + refspec = xrealloc(refspec, nr * sizeof(char *)); + refspec[nr-1] = ref; + refspec_nr = nr; +} + +static int expand_one_ref(const char *ref, const unsigned char *sha1, int flag, void *cb_data) +{ + /* Ignore the "refs/" at the beginning of the refname */ + ref += 5; + + if (!strncmp(ref, "tags/", 5)) + add_refspec(xstrdup(ref)); + return 0; +} + +static void expand_refspecs(void) +{ + if (all) { + if (refspec_nr) + die("cannot mix '--all' and a refspec"); + + /* + * No need to expand "--all" - we'll just use + * the "--all" flag to send-pack + */ + return; + } + if (!tags) + return; + for_each_ref(expand_one_ref, NULL); +} + +struct wildcard_cb { + const char *from_prefix; + int from_prefix_len; + const char *to_prefix; + int to_prefix_len; + int force; +}; + +static int expand_wildcard_ref(const char *ref, const unsigned char *sha1, int flag, void *cb_data) +{ + struct wildcard_cb *cb = cb_data; + int len = strlen(ref); + char *expanded, *newref; + + if (len < cb->from_prefix_len || + memcmp(cb->from_prefix, ref, cb->from_prefix_len)) + return 0; + expanded = xmalloc(len * 2 + cb->force + + (cb->to_prefix_len - cb->from_prefix_len) + 2); + newref = expanded + cb->force; + if (cb->force) + expanded[0] = '+'; + memcpy(newref, ref, len); + newref[len] = ':'; + memcpy(newref + len + 1, cb->to_prefix, cb->to_prefix_len); + strcpy(newref + len + 1 + cb->to_prefix_len, + ref + cb->from_prefix_len); + add_refspec(expanded); + return 0; +} + +static int wildcard_ref(const char *ref) +{ + int len; + const char *colon; + struct wildcard_cb cb; + + memset(&cb, 0, sizeof(cb)); + if (ref[0] == '+') { + cb.force = 1; + ref++; + } + len = strlen(ref); + colon = strchr(ref, ':'); + if (! (colon && ref < colon && + colon[-2] == '/' && colon[-1] == '*' && + /* "<mine>/<asterisk>:<yours>/<asterisk>" is at least 7 bytes */ + 7 <= len && + ref[len-2] == '/' && ref[len-1] == '*') ) + return 0 ; + cb.from_prefix = ref; + cb.from_prefix_len = colon - ref - 1; + cb.to_prefix = colon + 1; + cb.to_prefix_len = len - (colon - ref) - 2; + for_each_ref(expand_wildcard_ref, &cb); + return 1; +} + +static void set_refspecs(const char **refs, int nr) +{ + if (nr) { + int i; + for (i = 0; i < nr; i++) { + const char *ref = refs[i]; + if (!strcmp("tag", ref)) { + char *tag; + int len; + if (nr <= ++i) + die("tag shorthand without <tag>"); + len = strlen(refs[i]) + 11; + tag = xmalloc(len); + strcpy(tag, "refs/tags/"); + strcat(tag, refs[i]); + ref = tag; + } + else if (wildcard_ref(ref)) + continue; + add_refspec(ref); + } + } + expand_refspecs(); +} + +static int get_remotes_uri(const char *repo, const char *uri[MAX_URI]) +{ + int n = 0; + FILE *f = fopen(git_path("remotes/%s", repo), "r"); + int has_explicit_refspec = refspec_nr || all || tags; + + if (!f) + return -1; + while (fgets(buffer, BUF_SIZE, f)) { + int is_refspec; + char *s, *p; + + if (!strncmp("URL:", buffer, 4)) { + is_refspec = 0; + s = buffer + 4; + } else if (!strncmp("Push:", buffer, 5)) { + is_refspec = 1; + s = buffer + 5; + } else + continue; + + /* Remove whitespace at the head.. */ + while (isspace(*s)) + s++; + if (!*s) + continue; + + /* ..and at the end */ + p = s + strlen(s); + while (isspace(p[-1])) + *--p = 0; + + if (!is_refspec) { + if (n < MAX_URI) + uri[n++] = xstrdup(s); + else + error("more than %d URL's specified, ignoring the rest", MAX_URI); + } + else if (is_refspec && !has_explicit_refspec) { + if (!wildcard_ref(s)) + add_refspec(xstrdup(s)); + } + } + fclose(f); + if (!n) + die("remote '%s' has no URL", repo); + return n; +} + +static const char **config_uri; +static const char *config_repo; +static int config_repo_len; +static int config_current_uri; +static int config_get_refspecs; +static int config_get_receivepack; + +static int get_remote_config(const char* key, const char* value) +{ + if (!strncmp(key, "remote.", 7) && + !strncmp(key + 7, config_repo, config_repo_len)) { + if (!strcmp(key + 7 + config_repo_len, ".url")) { + if (config_current_uri < MAX_URI) + config_uri[config_current_uri++] = xstrdup(value); + else + error("more than %d URL's specified, ignoring the rest", MAX_URI); + } + else if (config_get_refspecs && + !strcmp(key + 7 + config_repo_len, ".push")) { + if (!wildcard_ref(value)) + add_refspec(xstrdup(value)); + } + else if (config_get_receivepack && + !strcmp(key + 7 + config_repo_len, ".receivepack")) { + if (!receivepack) { + char *rp = xmalloc(strlen(value) + 16); + sprintf(rp, "--receive-pack=%s", value); + receivepack = rp; + } else + error("more than one receivepack given, using the first"); + } + } + return 0; +} + +static int get_config_remotes_uri(const char *repo, const char *uri[MAX_URI]) +{ + config_repo_len = strlen(repo); + config_repo = repo; + config_current_uri = 0; + config_uri = uri; + config_get_refspecs = !(refspec_nr || all || tags); + config_get_receivepack = (receivepack == NULL); + + git_config(get_remote_config); + return config_current_uri; +} + +static int get_branches_uri(const char *repo, const char *uri[MAX_URI]) +{ + const char *slash = strchr(repo, '/'); + int n = slash ? slash - repo : 1000; + FILE *f = fopen(git_path("branches/%.*s", n, repo), "r"); + char *s, *p; + int len; + + if (!f) + return 0; + s = fgets(buffer, BUF_SIZE, f); + fclose(f); + if (!s) + return 0; + while (isspace(*s)) + s++; + if (!*s) + return 0; + p = s + strlen(s); + while (isspace(p[-1])) + *--p = 0; + len = p - s; + if (slash) + len += strlen(slash); + p = xmalloc(len + 1); + strcpy(p, s); + if (slash) + strcat(p, slash); + uri[0] = p; + return 1; +} + +/* + * Read remotes and branches file, fill the push target URI + * list. If there is no command line refspecs, read Push: lines + * to set up the *refspec list as well. + * return the number of push target URIs + */ +static int read_config(const char *repo, const char *uri[MAX_URI]) +{ + int n; + + if (*repo != '/') { + n = get_remotes_uri(repo, uri); + if (n > 0) + return n; + + n = get_config_remotes_uri(repo, uri); + if (n > 0) + return n; + + n = get_branches_uri(repo, uri); + if (n > 0) + return n; + } + + uri[0] = repo; + return 1; +} + +static int do_push(const char *repo) +{ + const char *uri[MAX_URI]; + int i, n; + int common_argc; + const char **argv; + int argc; + + n = read_config(repo, uri); + if (n <= 0) + die("bad repository '%s'", repo); + + argv = xmalloc((refspec_nr + 10) * sizeof(char *)); + argv[0] = "dummy-send-pack"; + argc = 1; + if (all) + argv[argc++] = "--all"; + if (force) + argv[argc++] = "--force"; + if (receivepack) + argv[argc++] = receivepack; + common_argc = argc; + + for (i = 0; i < n; i++) { + int err; + 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[dest_argc++] = dest; + while (dest_refspec_nr--) + argv[dest_argc++] = *dest_refspec++; + argv[dest_argc] = NULL; + if (verbose) + fprintf(stderr, "Pushing to %s\n", dest); + err = run_command_v(argv); + if (!err) + continue; + switch (err) { + case -ERR_RUN_COMMAND_FORK: + die("unable to fork for %s", sender); + case -ERR_RUN_COMMAND_EXEC: + die("unable to exec %s", sender); + case -ERR_RUN_COMMAND_WAITPID: + case -ERR_RUN_COMMAND_WAITPID_WRONG_PID: + case -ERR_RUN_COMMAND_WAITPID_SIGNAL: + case -ERR_RUN_COMMAND_WAITPID_NOEXIT: + die("%s died with strange error", sender); + default: + return -err; + } + } + return 0; +} + +int cmd_push(int argc, const char **argv, const char *prefix) +{ + int i; + const char *repo = "origin"; /* default repository */ + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (arg[0] != '-') { + repo = arg; + i++; + break; + } + if (!strcmp(arg, "-v")) { + verbose=1; + continue; + } + if (!strncmp(arg, "--repo=", 7)) { + repo = arg+7; + continue; + } + if (!strcmp(arg, "--all")) { + all = 1; + continue; + } + if (!strcmp(arg, "--tags")) { + tags = 1; + continue; + } + if (!strcmp(arg, "--force") || !strcmp(arg, "-f")) { + force = 1; + continue; + } + if (!strcmp(arg, "--thin")) { + thin = 1; + continue; + } + if (!strcmp(arg, "--no-thin")) { + thin = 0; + continue; + } + if (!strncmp(arg, "--receive-pack=", 15)) { + receivepack = arg; + continue; + } + if (!strncmp(arg, "--exec=", 7)) { + receivepack = arg; + continue; + } + usage(push_usage); + } + set_refspecs(argv + i, argc - i); + return do_push(repo); +} diff --git a/builtin-read-tree.c b/builtin-read-tree.c new file mode 100644 index 0000000000..8ba436dbac --- /dev/null +++ b/builtin-read-tree.c @@ -0,0 +1,274 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ + +#include "cache.h" +#include "object.h" +#include "tree.h" +#include "tree-walk.h" +#include "cache-tree.h" +#include "unpack-trees.h" +#include "dir.h" +#include "builtin.h" + +static struct object_list *trees; + +static int list_tree(unsigned char *sha1) +{ + struct tree *tree = parse_tree_indirect(sha1); + if (!tree) + return -1; + object_list_append(&tree->object, &trees); + return 0; +} + +static int read_cache_unmerged(void) +{ + int i; + struct cache_entry **dst; + struct cache_entry *last = NULL; + + read_cache(); + dst = active_cache; + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (ce_stage(ce)) { + if (last && !strcmp(ce->name, last->name)) + continue; + cache_tree_invalidate_path(active_cache_tree, ce->name); + last = ce; + ce->ce_mode = 0; + ce->ce_flags &= ~htons(CE_STAGEMASK); + } + *dst++ = ce; + } + active_nr = dst - active_cache; + return !!last; +} + +static void prime_cache_tree_rec(struct cache_tree *it, struct tree *tree) +{ + struct tree_desc desc; + struct name_entry entry; + int cnt; + + hashcpy(it->sha1, tree->object.sha1); + 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 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]] [--exclude-per-directory=<gitignore>] <sha1> [<sha2> [<sha3>]])"; + +static struct lock_file lock_file; + +int cmd_read_tree(int argc, const char **argv, const char *unused_prefix) +{ + int i, newfd, stage = 0; + unsigned char sha1[20]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + + setup_git_directory(); + git_config(git_default_config); + + newfd = hold_lock_file_for_update(&lock_file, get_index_file(), 1); + + git_config(git_default_config); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + /* "-u" means "update", meaning that a merge will update + * the working tree. + */ + if (!strcmp(arg, "-u")) { + opts.update = 1; + continue; + } + + if (!strcmp(arg, "-v")) { + opts.verbose_update = 1; + continue; + } + + /* "-i" means "index only", meaning that a merge will + * not even look at the working tree. + */ + if (!strcmp(arg, "-i")) { + opts.index_only = 1; + continue; + } + + /* "--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 || opts.merge || opts.prefix) + usage(read_tree_usage); + opts.prefix = arg + 9; + opts.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 || opts.merge || opts.prefix) + usage(read_tree_usage); + opts.reset = 1; + opts.merge = 1; + stage = 1; + read_cache_unmerged(); + continue; + } + + if (!strcmp(arg, "--trivial")) { + opts.trivial_merges_only = 1; + continue; + } + + if (!strcmp(arg, "--aggressive")) { + opts.aggressive = 1; + continue; + } + + /* "-m" stands for "merge", meaning we start in stage 1 */ + if (!strcmp(arg, "-m")) { + if (stage || opts.merge || opts.prefix) + usage(read_tree_usage); + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + stage = 1; + opts.merge = 1; + continue; + } + + if (!strncmp(arg, "--exclude-per-directory=", 24)) { + struct dir_struct *dir; + + if (opts.dir) + die("more than one --exclude-per-directory are given."); + + dir = calloc(1, sizeof(*opts.dir)); + dir->show_ignored = 1; + dir->exclude_per_dir = arg + 24; + opts.dir = dir; + /* We do not need to nor want to do read-directory + * here; we are merely interested in reusing the + * per directory ignore stack mechanism. + */ + continue; + } + + /* using -u and -i at the same time makes no sense */ + if (1 < opts.index_only + opts.update) + usage(read_tree_usage); + + if (get_sha1(arg, sha1)) + die("Not a valid object name %s", arg); + if (list_tree(sha1) < 0) + die("failed to unpack tree object %s", arg); + stage++; + } + if ((opts.update||opts.index_only) && !opts.merge) + usage(read_tree_usage); + if ((opts.dir && !opts.update)) + die("--exclude-per-directory is meaningless unless -u"); + + if (opts.prefix) { + int pfxlen = strlen(opts.prefix); + int pos; + if (opts.prefix[pfxlen-1] != '/') + die("prefix must end with /"); + if (stage != 2) + die("binding merge takes only one tree"); + pos = cache_name_pos(opts.prefix, pfxlen); + if (0 <= pos) + die("corrupt index file"); + pos = -pos-1; + if (pos < active_nr && + !strncmp(active_cache[pos]->name, opts.prefix, pfxlen)) + die("subdirectory '%s' already exists.", opts.prefix); + pos = cache_name_pos(opts.prefix, pfxlen-1); + if (0 <= pos) + die("file '%.*s' already exists.", + pfxlen-1, opts.prefix); + } + + if (opts.merge) { + if (stage < 2) + die("just how do you expect me to merge %d trees?", stage-1); + switch (stage - 1) { + case 1: + opts.fn = opts.prefix ? bind_merge : oneway_merge; + break; + case 2: + opts.fn = twoway_merge; + break; + case 3: + default: + opts.fn = threeway_merge; + cache_tree_free(&active_cache_tree); + break; + } + + if (stage - 1 >= 3) + opts.head_idx = stage - 2; + else + opts.head_idx = 1; + } + + unpack_trees(trees, &opts); + + /* + * 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 && !opts.prefix && (!opts.merge || (stage == 2))) { + cache_tree_free(&active_cache_tree); + prime_cache_tree(); + } + + if (write_cache(newfd, active_cache, active_nr) || + close(newfd) || commit_lock_file(&lock_file)) + die("unable to write new index file"); + return 0; +} diff --git a/builtin-reflog.c b/builtin-reflog.c new file mode 100644 index 0000000000..65b845b447 --- /dev/null +++ b/builtin-reflog.c @@ -0,0 +1,387 @@ +#include "cache.h" +#include "builtin.h" +#include "commit.h" +#include "refs.h" +#include "dir.h" +#include "tree-walk.h" +#include "diff.h" +#include "revision.h" +#include "reachable.h" + +/* + * reflog expire + */ + +static const char reflog_expire_usage[] = +"git-reflog (show|expire) [--verbose] [--dry-run] [--stale-fix] [--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>..."; + +static unsigned long default_reflog_expire; +static unsigned long default_reflog_expire_unreachable; + +struct cmd_reflog_expire_cb { + struct rev_info revs; + int dry_run; + int stalefix; + int verbose; + unsigned long expire_total; + unsigned long expire_unreachable; +}; + +struct expire_reflog_cb { + FILE *newlog; + const char *ref; + struct commit *ref_commit; + struct cmd_reflog_expire_cb *cmd; +}; + +#define INCOMPLETE (1u<<10) +#define STUDYING (1u<<11) + +static int tree_is_complete(const unsigned char *sha1) +{ + struct tree_desc desc; + struct name_entry entry; + int complete; + struct tree *tree; + + tree = lookup_tree(sha1); + if (!tree) + return 0; + if (tree->object.flags & SEEN) + return 1; + if (tree->object.flags & INCOMPLETE) + return 0; + + desc.buf = tree->buffer; + desc.size = tree->size; + if (!desc.buf) { + char type[20]; + void *data = read_sha1_file(sha1, type, &desc.size); + if (!data) { + tree->object.flags |= INCOMPLETE; + return 0; + } + desc.buf = data; + tree->buffer = data; + } + complete = 1; + while (tree_entry(&desc, &entry)) { + if (!has_sha1_file(entry.sha1) || + (S_ISDIR(entry.mode) && !tree_is_complete(entry.sha1))) { + tree->object.flags |= INCOMPLETE; + complete = 0; + } + } + free(tree->buffer); + tree->buffer = NULL; + + if (complete) + tree->object.flags |= SEEN; + return complete; +} + +static int commit_is_complete(struct commit *commit) +{ + struct object_array study; + struct object_array found; + int is_incomplete = 0; + int i; + + /* early return */ + if (commit->object.flags & SEEN) + return 1; + if (commit->object.flags & INCOMPLETE) + return 0; + /* + * Find all commits that are reachable and are not marked as + * SEEN. Then make sure the trees and blobs contained are + * complete. After that, mark these commits also as SEEN. + * If some of the objects that are needed to complete this + * commit are missing, mark this commit as INCOMPLETE. + */ + memset(&study, 0, sizeof(study)); + memset(&found, 0, sizeof(found)); + add_object_array(&commit->object, NULL, &study); + add_object_array(&commit->object, NULL, &found); + commit->object.flags |= STUDYING; + while (study.nr) { + struct commit *c; + struct commit_list *parent; + + c = (struct commit *)study.objects[--study.nr].item; + if (!c->object.parsed && !parse_object(c->object.sha1)) + c->object.flags |= INCOMPLETE; + + if (c->object.flags & INCOMPLETE) { + is_incomplete = 1; + break; + } + else if (c->object.flags & SEEN) + continue; + for (parent = c->parents; parent; parent = parent->next) { + struct commit *p = parent->item; + if (p->object.flags & STUDYING) + continue; + p->object.flags |= STUDYING; + add_object_array(&p->object, NULL, &study); + add_object_array(&p->object, NULL, &found); + } + } + if (!is_incomplete) { + /* + * make sure all commits in "found" array have all the + * necessary objects. + */ + for (i = 0; i < found.nr; i++) { + struct commit *c = + (struct commit *)found.objects[i].item; + if (!tree_is_complete(c->tree->object.sha1)) { + is_incomplete = 1; + c->object.flags |= INCOMPLETE; + } + } + if (!is_incomplete) { + /* mark all found commits as complete, iow SEEN */ + for (i = 0; i < found.nr; i++) + found.objects[i].item->flags |= SEEN; + } + } + /* clear flags from the objects we traversed */ + for (i = 0; i < found.nr; i++) + found.objects[i].item->flags &= ~STUDYING; + if (is_incomplete) + commit->object.flags |= INCOMPLETE; + else { + /* + * If we come here, we have (1) traversed the ancestry chain + * from the "commit" until we reach SEEN commits (which are + * known to be complete), and (2) made sure that the commits + * encountered during the above traversal refer to trees that + * are complete. Which means that we know *all* the commits + * we have seen during this process are complete. + */ + for (i = 0; i < found.nr; i++) + found.objects[i].item->flags |= SEEN; + } + /* free object arrays */ + free(study.objects); + free(found.objects); + return !is_incomplete; +} + +static int keep_entry(struct commit **it, unsigned char *sha1) +{ + struct commit *commit; + + if (is_null_sha1(sha1)) + return 1; + commit = lookup_commit_reference_gently(sha1, 1); + if (!commit) + return 0; + + /* + * Make sure everything in this commit exists. + * + * We have walked all the objects reachable from the refs + * and cache earlier. The commits reachable by this commit + * must meet SEEN commits -- and then we should mark them as + * SEEN as well. + */ + if (!commit_is_complete(commit)) + return 0; + *it = commit; + return 1; +} + +static int expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1, + const char *email, unsigned long timestamp, int tz, + const char *message, void *cb_data) +{ + struct expire_reflog_cb *cb = cb_data; + struct commit *old, *new; + + if (timestamp < cb->cmd->expire_total) + goto prune; + + old = new = NULL; + if (cb->cmd->stalefix && + (!keep_entry(&old, osha1) || !keep_entry(&new, nsha1))) + goto prune; + + if (timestamp < cb->cmd->expire_unreachable) { + if (!cb->ref_commit) + goto prune; + if (!old && !is_null_sha1(osha1)) + old = lookup_commit_reference_gently(osha1, 1); + if (!new && !is_null_sha1(nsha1)) + new = lookup_commit_reference_gently(nsha1, 1); + if ((old && !in_merge_bases(old, cb->ref_commit)) || + (new && !in_merge_bases(new, cb->ref_commit))) + goto prune; + } + + if (cb->newlog) { + char sign = (tz < 0) ? '-' : '+'; + int zone = (tz < 0) ? (-tz) : tz; + fprintf(cb->newlog, "%s %s %s %lu %c%04d\t%s", + sha1_to_hex(osha1), sha1_to_hex(nsha1), + email, timestamp, sign, zone, + message); + } + if (cb->cmd->verbose) + printf("keep %s", message); + return 0; + prune: + if (!cb->newlog || cb->cmd->verbose) + printf("%sprune %s", cb->newlog ? "" : "would ", message); + return 0; +} + +static int expire_reflog(const char *ref, const unsigned char *sha1, int unused, void *cb_data) +{ + struct cmd_reflog_expire_cb *cmd = cb_data; + struct expire_reflog_cb cb; + struct ref_lock *lock; + char *log_file, *newlog_path = NULL; + int status = 0; + + memset(&cb, 0, sizeof(cb)); + /* we take the lock for the ref itself to prevent it from + * getting updated. + */ + lock = lock_any_ref_for_update(ref, sha1); + if (!lock) + return error("cannot lock ref '%s'", ref); + log_file = xstrdup(git_path("logs/%s", ref)); + if (!file_exists(log_file)) + goto finish; + if (!cmd->dry_run) { + newlog_path = xstrdup(git_path("logs/%s.lock", ref)); + cb.newlog = fopen(newlog_path, "w"); + } + + cb.ref_commit = lookup_commit_reference_gently(sha1, 1); + cb.ref = ref; + cb.cmd = cmd; + for_each_reflog_ent(ref, expire_reflog_ent, &cb); + finish: + if (cb.newlog) { + if (fclose(cb.newlog)) + status |= error("%s: %s", strerror(errno), + newlog_path); + if (rename(newlog_path, log_file)) { + status |= error("cannot rename %s to %s", + newlog_path, log_file); + unlink(newlog_path); + } + } + free(newlog_path); + free(log_file); + unlock_ref(lock); + return status; +} + +static int reflog_expire_config(const char *var, const char *value) +{ + if (!strcmp(var, "gc.reflogexpire")) + default_reflog_expire = approxidate(value); + else if (!strcmp(var, "gc.reflogexpireunreachable")) + default_reflog_expire_unreachable = approxidate(value); + else + return git_default_config(var, value); + return 0; +} + +static int cmd_reflog_expire(int argc, const char **argv, const char *prefix) +{ + struct cmd_reflog_expire_cb cb; + unsigned long now = time(NULL); + int i, status, do_all; + + git_config(reflog_expire_config); + + save_commit_buffer = 0; + do_all = status = 0; + memset(&cb, 0, sizeof(cb)); + + if (!default_reflog_expire_unreachable) + default_reflog_expire_unreachable = now - 30 * 24 * 3600; + if (!default_reflog_expire) + default_reflog_expire = now - 90 * 24 * 3600; + cb.expire_total = default_reflog_expire; + cb.expire_unreachable = default_reflog_expire_unreachable; + + /* + * We can trust the commits and objects reachable from refs + * even in older repository. We cannot trust what's reachable + * from reflog if the repository was pruned with older git. + */ + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--dry-run") || !strcmp(arg, "-n")) + cb.dry_run = 1; + else if (!strncmp(arg, "--expire=", 9)) + cb.expire_total = approxidate(arg + 9); + else if (!strncmp(arg, "--expire-unreachable=", 21)) + cb.expire_unreachable = approxidate(arg + 21); + else if (!strcmp(arg, "--stale-fix")) + cb.stalefix = 1; + else if (!strcmp(arg, "--all")) + do_all = 1; + else if (!strcmp(arg, "--verbose")) + cb.verbose = 1; + else if (!strcmp(arg, "--")) { + i++; + break; + } + else if (arg[0] == '-') + usage(reflog_expire_usage); + else + break; + } + if (cb.stalefix) { + init_revisions(&cb.revs, prefix); + if (cb.verbose) + printf("Marking reachable objects..."); + mark_reachable_objects(&cb.revs, 0); + if (cb.verbose) + putchar('\n'); + } + + if (do_all) + status |= for_each_reflog(expire_reflog, &cb); + while (i < argc) { + const char *ref = argv[i++]; + unsigned char sha1[20]; + if (!resolve_ref(ref, sha1, 1, NULL)) { + status |= error("%s points nowhere!", ref); + continue; + } + status |= expire_reflog(ref, sha1, 0, &cb); + } + return status; +} + +/* + * main "reflog" + */ + +static const char reflog_usage[] = +"git-reflog (expire | ...)"; + +int cmd_reflog(int argc, const char **argv, const char *prefix) +{ + /* With no command, we default to showing it. */ + if (argc < 2 || *argv[1] == '-') + return cmd_log_reflog(argc, argv, prefix); + + if (!strcmp(argv[1], "show")) + return cmd_log_reflog(argc - 1, argv + 1, prefix); + + if (!strcmp(argv[1], "expire")) + return cmd_reflog_expire(argc - 1, argv + 1, prefix); + + /* Not a recognized reflog command..*/ + usage(reflog_usage); +} diff --git a/builtin-rerere.c b/builtin-rerere.c new file mode 100644 index 0000000000..318d959d89 --- /dev/null +++ b/builtin-rerere.c @@ -0,0 +1,421 @@ +#include "cache.h" +#include "path-list.h" +#include "xdiff/xdiff.h" +#include "xdiff-interface.h" + +#include <time.h> + +static const char git_rerere_usage[] = +"git-rerere [clear | status | diff | gc]"; + +/* these values are days */ +static int cutoff_noresolve = 15; +static int cutoff_resolve = 60; + +static char *merge_rr_path; + +static const char *rr_path(const char *name, const char *file) +{ + return git_path("rr-cache/%s/%s", name, file); +} + +static void read_rr(struct path_list *rr) +{ + unsigned char sha1[20]; + char buf[PATH_MAX]; + FILE *in = fopen(merge_rr_path, "r"); + if (!in) + return; + while (fread(buf, 40, 1, in) == 1) { + int i; + char *name; + if (get_sha1_hex(buf, sha1)) + die("corrupt MERGE_RR"); + buf[40] = '\0'; + name = xstrdup(buf); + if (fgetc(in) != '\t') + die("corrupt MERGE_RR"); + for (i = 0; i < sizeof(buf) && (buf[i] = fgetc(in)); i++) + ; /* do nothing */ + if (i == sizeof(buf)) + die("filename too long"); + path_list_insert(buf, rr)->util = xstrdup(name); + } + fclose(in); +} + +static struct lock_file write_lock; + +static int write_rr(struct path_list *rr, int out_fd) +{ + int i; + for (i = 0; i < rr->nr; i++) { + const char *path = rr->items[i].path; + int length = strlen(path) + 1; + if (write_in_full(out_fd, rr->items[i].util, 40) != 40 || + write_in_full(out_fd, "\t", 1) != 1 || + write_in_full(out_fd, path, length) != length) + die("unable to write rerere record"); + } + close(out_fd); + return commit_lock_file(&write_lock); +} + +struct buffer { + char *ptr; + int nr, alloc; +}; + +static void append_line(struct buffer *buffer, const char *line) +{ + int len = strlen(line); + + if (buffer->nr + len > buffer->alloc) { + buffer->alloc = alloc_nr(buffer->nr + len); + buffer->ptr = xrealloc(buffer->ptr, buffer->alloc); + } + memcpy(buffer->ptr + buffer->nr, line, len); + buffer->nr += len; +} + +static int handle_file(const char *path, + unsigned char *sha1, const char *output) +{ + SHA_CTX ctx; + char buf[1024]; + int hunk = 0, hunk_no = 0; + struct buffer minus = { NULL, 0, 0 }, plus = { NULL, 0, 0 }; + struct buffer *one = &minus, *two = + + FILE *f = fopen(path, "r"); + FILE *out; + + if (!f) + return error("Could not open %s", path); + + if (output) { + out = fopen(output, "w"); + if (!out) { + fclose(f); + return error("Could not write %s", output); + } + } else + out = NULL; + + if (sha1) + SHA1_Init(&ctx); + + while (fgets(buf, sizeof(buf), f)) { + if (!strncmp("<<<<<<< ", buf, 8)) + hunk = 1; + else if (!strncmp("=======", buf, 7)) + hunk = 2; + else if (!strncmp(">>>>>>> ", buf, 8)) { + hunk_no++; + hunk = 0; + if (memcmp(one->ptr, two->ptr, one->nr < two->nr ? + one->nr : two->nr) > 0) { + struct buffer *swap = one; + one = two; + two = swap; + } + if (out) { + fputs("<<<<<<<\n", out); + fwrite(one->ptr, one->nr, 1, out); + fputs("=======\n", out); + fwrite(two->ptr, two->nr, 1, out); + fputs(">>>>>>>\n", out); + } + if (sha1) { + SHA1_Update(&ctx, one->ptr, one->nr); + SHA1_Update(&ctx, "\0", 1); + SHA1_Update(&ctx, two->ptr, two->nr); + SHA1_Update(&ctx, "\0", 1); + } + } else if (hunk == 1) + append_line(one, buf); + else if (hunk == 2) + append_line(two, buf); + else if (out) + fputs(buf, out); + } + + fclose(f); + if (out) + fclose(out); + if (sha1) + SHA1_Final(sha1, &ctx); + return hunk_no; +} + +static int find_conflict(struct path_list *conflict) +{ + int i; + if (read_cache() < 0) + return error("Could not read index"); + for (i = 0; i + 2 < active_nr; i++) { + struct cache_entry *e1 = active_cache[i]; + struct cache_entry *e2 = active_cache[i + 1]; + struct cache_entry *e3 = active_cache[i + 2]; + if (ce_stage(e1) == 1 && ce_stage(e2) == 2 && + ce_stage(e3) == 3 && ce_same_name(e1, e2) && + ce_same_name(e1, e3)) { + path_list_insert((const char *)e1->name, conflict); + i += 3; + } + } + return 0; +} + +static int merge(const char *name, const char *path) +{ + int ret; + mmfile_t cur, base, other; + mmbuffer_t result = {NULL, 0}; + xpparam_t xpp = {XDF_NEED_MINIMAL}; + + if (handle_file(path, NULL, rr_path(name, "thisimage")) < 0) + return 1; + + if (read_mmfile(&cur, rr_path(name, "thisimage")) || + read_mmfile(&base, rr_path(name, "preimage")) || + read_mmfile(&other, rr_path(name, "postimage"))) + return 1; + ret = xdl_merge(&base, &cur, "", &other, "", + &xpp, XDL_MERGE_ZEALOUS, &result); + if (!ret) { + FILE *f = fopen(path, "w"); + if (!f) + return error("Could not write to %s", path); + fwrite(result.ptr, result.size, 1, f); + fclose(f); + } + + free(cur.ptr); + free(base.ptr); + free(other.ptr); + free(result.ptr); + + return ret; +} + +static void unlink_rr_item(const char *name) +{ + unlink(rr_path(name, "thisimage")); + unlink(rr_path(name, "preimage")); + unlink(rr_path(name, "postimage")); + rmdir(git_path("rr-cache/%s", name)); +} + +static void garbage_collect(struct path_list *rr) +{ + struct path_list to_remove = { NULL, 0, 0, 1 }; + char buf[1024]; + DIR *dir; + struct dirent *e; + int len, i, cutoff; + time_t now = time(NULL), then; + + strlcpy(buf, git_path("rr-cache"), sizeof(buf)); + len = strlen(buf); + dir = opendir(buf); + strcpy(buf + len++, "/"); + while ((e = readdir(dir))) { + const char *name = e->d_name; + struct stat st; + if (name[0] == '.' && (name[1] == '\0' || + (name[1] == '.' && name[2] == '\0'))) + continue; + i = snprintf(buf + len, sizeof(buf) - len, "%s", name); + strlcpy(buf + len + i, "/preimage", sizeof(buf) - len - i); + if (stat(buf, &st)) + continue; + then = st.st_mtime; + strlcpy(buf + len + i, "/postimage", sizeof(buf) - len - i); + cutoff = stat(buf, &st) ? cutoff_noresolve : cutoff_resolve; + if (then < now - cutoff * 86400) { + buf[len + i] = '\0'; + path_list_insert(xstrdup(name), &to_remove); + } + } + for (i = 0; i < to_remove.nr; i++) + unlink_rr_item(to_remove.items[i].path); + path_list_clear(&to_remove, 0); +} + +static int outf(void *dummy, mmbuffer_t *ptr, int nbuf) +{ + int i; + for (i = 0; i < nbuf; i++) + if (write_in_full(1, ptr[i].ptr, ptr[i].size) != ptr[i].size) + return -1; + return 0; +} + +static int diff_two(const char *file1, const char *label1, + const char *file2, const char *label2) +{ + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + mmfile_t minus, plus; + + if (read_mmfile(&minus, file1) || read_mmfile(&plus, file2)) + return 1; + + printf("--- a/%s\n+++ b/%s\n", label1, label2); + fflush(stdout); + xpp.flags = XDF_NEED_MINIMAL; + xecfg.ctxlen = 3; + xecfg.flags = 0; + ecb.outf = outf; + xdl_diff(&minus, &plus, &xpp, &xecfg, &ecb); + + free(minus.ptr); + free(plus.ptr); + return 0; +} + +static int copy_file(const char *src, const char *dest) +{ + FILE *in, *out; + char buffer[32768]; + int count; + + if (!(in = fopen(src, "r"))) + return error("Could not open %s", src); + if (!(out = fopen(dest, "w"))) + return error("Could not open %s", dest); + while ((count = fread(buffer, 1, sizeof(buffer), in))) + fwrite(buffer, 1, count, out); + fclose(in); + fclose(out); + return 0; +} + +static int do_plain_rerere(struct path_list *rr, int fd) +{ + struct path_list conflict = { NULL, 0, 0, 1 }; + int i; + + find_conflict(&conflict); + + /* + * MERGE_RR records paths with conflicts immediately after merge + * failed. Some of the conflicted paths might have been hand resolved + * in the working tree since then, but the initial run would catch all + * and register their preimages. + */ + + for (i = 0; i < conflict.nr; i++) { + const char *path = conflict.items[i].path; + if (!path_list_has_path(rr, path)) { + unsigned char sha1[20]; + char *hex; + int ret; + ret = handle_file(path, sha1, NULL); + if (ret < 1) + continue; + hex = xstrdup(sha1_to_hex(sha1)); + path_list_insert(path, rr)->util = hex; + if (mkdir(git_path("rr-cache/%s", hex), 0755)) + continue;; + handle_file(path, NULL, rr_path(hex, "preimage")); + fprintf(stderr, "Recorded preimage for '%s'\n", path); + } + } + + /* + * Now some of the paths that had conflicts earlier might have been + * hand resolved. Others may be similar to a conflict already that + * was resolved before. + */ + + for (i = 0; i < rr->nr; i++) { + struct stat st; + int ret; + const char *path = rr->items[i].path; + const char *name = (const char *)rr->items[i].util; + + if (!stat(rr_path(name, "preimage"), &st) && + !stat(rr_path(name, "postimage"), &st)) { + if (!merge(name, path)) { + fprintf(stderr, "Resolved '%s' using " + "previous resolution.\n", path); + goto tail_optimization; + } + } + + /* Let's see if we have resolved it. */ + ret = handle_file(path, NULL, NULL); + if (ret) + continue; + + fprintf(stderr, "Recorded resolution for '%s'.\n", path); + copy_file(path, rr_path(name, "postimage")); +tail_optimization: + if (i < rr->nr - 1) + memmove(rr->items + i, + rr->items + i + 1, + sizeof(rr->items[0]) * (rr->nr - i - 1)); + rr->nr--; + i--; + } + + return write_rr(rr, fd); +} + +static int git_rerere_config(const char *var, const char *value) +{ + if (!strcmp(var, "gc.rerereresolved")) + cutoff_resolve = git_config_int(var, value); + else if (!strcmp(var, "gc.rerereunresolved")) + cutoff_noresolve = git_config_int(var, value); + else + return git_default_config(var, value); + return 0; +} + +int cmd_rerere(int argc, const char **argv, const char *prefix) +{ + struct path_list merge_rr = { NULL, 0, 0, 1 }; + int i, fd = -1; + struct stat st; + + if (stat(git_path("rr-cache"), &st) || !S_ISDIR(st.st_mode)) + return 0; + + git_config(git_rerere_config); + + merge_rr_path = xstrdup(git_path("rr-cache/MERGE_RR")); + fd = hold_lock_file_for_update(&write_lock, merge_rr_path, 1); + read_rr(&merge_rr); + + if (argc < 2) + return do_plain_rerere(&merge_rr, fd); + else if (!strcmp(argv[1], "clear")) { + for (i = 0; i < merge_rr.nr; i++) { + const char *name = (const char *)merge_rr.items[i].util; + if (!stat(git_path("rr-cache/%s", name), &st) && + S_ISDIR(st.st_mode) && + stat(rr_path(name, "postimage"), &st)) + unlink_rr_item(name); + } + unlink(merge_rr_path); + } else if (!strcmp(argv[1], "gc")) + garbage_collect(&merge_rr); + else if (!strcmp(argv[1], "status")) + for (i = 0; i < merge_rr.nr; i++) + printf("%s\n", merge_rr.items[i].path); + else if (!strcmp(argv[1], "diff")) + for (i = 0; i < merge_rr.nr; i++) { + const char *path = merge_rr.items[i].path; + const char *name = (const char *)merge_rr.items[i].util; + diff_two(rr_path(name, "preimage"), path, path, path); + } + else + usage(git_rerere_usage); + + path_list_clear(&merge_rr, 1); + return 0; +} + diff --git a/builtin-rev-list.c b/builtin-rev-list.c new file mode 100644 index 0000000000..1bb3a06680 --- /dev/null +++ b/builtin-rev-list.c @@ -0,0 +1,293 @@ +#include "cache.h" +#include "refs.h" +#include "tag.h" +#include "commit.h" +#include "tree.h" +#include "blob.h" +#include "tree-walk.h" +#include "diff.h" +#include "revision.h" +#include "list-objects.h" +#include "builtin.h" + +/* bits #0-15 in revision.h */ + +#define COUNTED (1u<<16) + +static const char rev_list_usage[] = +"git-rev-list [OPTION] <commit-id>... [ -- paths... ]\n" +" limiting output:\n" +" --max-count=nr\n" +" --max-age=epoch\n" +" --min-age=epoch\n" +" --sparse\n" +" --no-merges\n" +" --remove-empty\n" +" --all\n" +" --stdin\n" +" ordering output:\n" +" --topo-order\n" +" --date-order\n" +" formatting output:\n" +" --parents\n" +" --objects | --objects-edge\n" +" --unpacked\n" +" --header | --pretty\n" +" --abbrev=nr | --no-abbrev\n" +" --abbrev-commit\n" +" special purpose:\n" +" --bisect" +; + +static struct rev_info revs; + +static int bisect_list; +static int show_timestamp; +static int hdr_termination; +static const char *header_prefix; + +static void show_commit(struct commit *commit) +{ + if (show_timestamp) + printf("%lu ", commit->date); + if (header_prefix) + fputs(header_prefix, stdout); + if (commit->object.flags & BOUNDARY) + putchar('-'); + else if (revs.left_right) { + if (commit->object.flags & SYMMETRIC_LEFT) + putchar('<'); + else + putchar('>'); + } + if (revs.abbrev_commit && revs.abbrev) + fputs(find_unique_abbrev(commit->object.sha1, revs.abbrev), + stdout); + else + fputs(sha1_to_hex(commit->object.sha1), stdout); + if (revs.parents) { + struct commit_list *parents = commit->parents; + while (parents) { + struct object *o = &(parents->item->object); + parents = parents->next; + if (o->flags & TMP_MARK) + continue; + printf(" %s", sha1_to_hex(o->sha1)); + o->flags |= TMP_MARK; + } + /* TMP_MARK is a general purpose flag that can + * be used locally, but the user should clean + * things up after it is done with them. + */ + for (parents = commit->parents; + parents; + parents = parents->next) + parents->item->object.flags &= ~TMP_MARK; + } + if (revs.commit_format == CMIT_FMT_ONELINE) + putchar(' '); + else + putchar('\n'); + + if (revs.verbose_header) { + static char pretty_header[16384]; + pretty_print_commit(revs.commit_format, commit, ~0, + pretty_header, sizeof(pretty_header), + revs.abbrev, NULL, NULL, revs.relative_date); + printf("%s%c", pretty_header, hdr_termination); + } + fflush(stdout); + if (commit->parents) { + free_commit_list(commit->parents); + commit->parents = NULL; + } + free(commit->buffer); + commit->buffer = NULL; +} + +static void show_object(struct object_array_entry *p) +{ + /* An object with name "foo\n0000000..." can be used to + * confuse downstream git-pack-objects very badly. + */ + const char *ep = strchr(p->name, '\n'); + if (ep) { + printf("%s %.*s\n", sha1_to_hex(p->item->sha1), + (int) (ep - p->name), + p->name); + } + else + printf("%s %s\n", sha1_to_hex(p->item->sha1), p->name); +} + +static void show_edge(struct commit *commit) +{ + printf("-%s\n", sha1_to_hex(commit->object.sha1)); +} + +/* + * This is a truly stupid algorithm, but it's only + * used for bisection, and we just don't care enough. + * + * We care just barely enough to avoid recursing for + * non-merge entries. + */ +static int count_distance(struct commit_list *entry) +{ + int nr = 0; + + while (entry) { + struct commit *commit = entry->item; + struct commit_list *p; + + if (commit->object.flags & (UNINTERESTING | COUNTED)) + break; + if (!revs.prune_fn || (commit->object.flags & TREECHANGE)) + nr++; + commit->object.flags |= COUNTED; + p = commit->parents; + entry = p; + if (p) { + p = p->next; + while (p) { + nr += count_distance(p); + p = p->next; + } + } + } + + return nr; +} + +static void clear_distance(struct commit_list *list) +{ + while (list) { + struct commit *commit = list->item; + commit->object.flags &= ~COUNTED; + list = list->next; + } +} + +static struct commit_list *find_bisection(struct commit_list *list) +{ + int nr, closest; + struct commit_list *p, *best; + + nr = 0; + p = list; + while (p) { + if (!revs.prune_fn || (p->item->object.flags & TREECHANGE)) + nr++; + p = p->next; + } + closest = 0; + best = list; + + for (p = list; p; p = p->next) { + int distance; + + if (revs.prune_fn && !(p->item->object.flags & TREECHANGE)) + continue; + + distance = count_distance(p); + clear_distance(list); + if (nr - distance < distance) + distance = nr - distance; + if (distance > closest) { + best = p; + closest = distance; + } + } + if (best) + best->next = NULL; + return best; +} + +static void read_revisions_from_stdin(struct rev_info *revs) +{ + char line[1000]; + + while (fgets(line, sizeof(line), stdin) != NULL) { + int len = strlen(line); + if (line[len - 1] == '\n') + line[--len] = 0; + if (!len) + break; + if (line[0] == '-') + die("options not supported in --stdin mode"); + if (handle_revision_arg(line, revs, 0, 1)) + die("bad revision '%s'", line); + } +} + +int cmd_rev_list(int argc, const char **argv, const char *prefix) +{ + struct commit_list *list; + int i; + int read_from_stdin = 0; + + init_revisions(&revs, prefix); + revs.abbrev = 0; + revs.commit_format = CMIT_FMT_UNSPECIFIED; + argc = setup_revisions(argc, argv, &revs, NULL); + + for (i = 1 ; i < argc; i++) { + const char *arg = argv[i]; + + if (!strcmp(arg, "--header")) { + revs.verbose_header = 1; + continue; + } + if (!strcmp(arg, "--timestamp")) { + show_timestamp = 1; + continue; + } + if (!strcmp(arg, "--bisect")) { + bisect_list = 1; + continue; + } + if (!strcmp(arg, "--stdin")) { + if (read_from_stdin++) + die("--stdin given twice?"); + read_revisions_from_stdin(&revs); + continue; + } + usage(rev_list_usage); + + } + if (revs.commit_format != CMIT_FMT_UNSPECIFIED) { + /* The command line has a --pretty */ + hdr_termination = '\n'; + if (revs.commit_format == CMIT_FMT_ONELINE) + header_prefix = ""; + else + header_prefix = "commit "; + } + else if (revs.verbose_header) + /* Only --header was specified */ + revs.commit_format = CMIT_FMT_RAW; + + list = revs.commits; + + if ((!list && + (!(revs.tag_objects||revs.tree_objects||revs.blob_objects) && + !revs.pending.nr)) || + revs.diff) + usage(rev_list_usage); + + save_commit_buffer = revs.verbose_header || revs.grep_filter; + track_object_refs = 0; + if (bisect_list) + revs.limited = 1; + + prepare_revision_walk(&revs); + if (revs.tree_objects) + mark_edges_uninteresting(revs.commits, &revs, show_edge); + + if (bisect_list) + revs.commits = find_bisection(revs.commits); + + traverse_commit_list(&revs, show_commit, show_object); + + return 0; +} diff --git a/builtin-rev-parse.c b/builtin-rev-parse.c new file mode 100644 index 0000000000..d53deaa369 --- /dev/null +++ b/builtin-rev-parse.c @@ -0,0 +1,398 @@ +/* + * 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; + +#define NORMAL 0 +#define REVERSED 1 +static int show_type = NORMAL; +static int symbolic; +static int abbrev; +static int output_sq; + +static int revs_count; + +/* + * 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, int flag, void *cb_data) +{ + 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; +} + +static int try_difference(const char *arg) +{ + char *dotdot; + unsigned char sha1[20]; + unsigned char end[20]; + const char *next; + const char *this; + int symmetric; + + if (!(dotdot = strstr(arg, ".."))) + return 0; + next = dotdot + 2; + this = arg; + symmetric = (*next == '.'); + + *dotdot = 0; + next += symmetric; + + 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(symmetric ? NORMAL : REVERSED, sha1, this); + if (symmetric) { + struct commit_list *exclude; + struct commit *a, *b; + a = lookup_commit_reference(sha1); + b = lookup_commit_reference(end); + exclude = get_merge_bases(a, b, 1); + while (exclude) { + struct commit_list *n = exclude->next; + show_rev(REVERSED, + exclude->item->object.sha1,NULL); + free(exclude); + exclude = n; + } + } + return 1; + } + *dotdot = '.'; + return 0; +} + +int cmd_rev_parse(int argc, const char **argv, const char *prefix) +{ + int i, as_is = 0, verify = 0; + unsigned char sha1[20]; + + git_config(git_default_config); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + 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, NULL); + continue; + } + if (!strcmp(arg, "--branches")) { + for_each_branch_ref(show_reference, NULL); + continue; + } + if (!strcmp(arg, "--tags")) { + for_each_tag_ref(show_reference, NULL); + continue; + } + if (!strcmp(arg, "--remotes")) { + for_each_remote_ref(show_reference, NULL); + 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 (!strcmp(arg, "--is-inside-git-dir")) { + printf("%s\n", is_inside_git_dir() ? "true" + : "false"); + 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 */ + if (try_difference(arg)) + continue; + 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; +} diff --git a/builtin-rm.c b/builtin-rm.c new file mode 100644 index 0000000000..00dbe39960 --- /dev/null +++ b/builtin-rm.c @@ -0,0 +1,238 @@ +/* + * "git rm" builtin command + * + * Copyright (C) Linus Torvalds 2006 + */ +#include "cache.h" +#include "builtin.h" +#include "dir.h" +#include "cache-tree.h" +#include "tree-walk.h" + +static const char builtin_rm_usage[] = +"git-rm [-f] [-n] [-r] [--cached] [--] <file>..."; + +static struct { + int nr, alloc; + const char **name; +} list; + +static void add_list(const char *name) +{ + if (list.nr >= list.alloc) { + list.alloc = alloc_nr(list.alloc); + list.name = xrealloc(list.name, list.alloc * sizeof(const char *)); + } + list.name[list.nr++] = name; +} + +static int remove_file(const char *name) +{ + int ret; + char *slash; + + ret = unlink(name); + if (ret && errno == ENOENT) + /* The user has removed it from the filesystem by hand */ + ret = errno = 0; + + if (!ret && (slash = strrchr(name, '/'))) { + char *n = xstrdup(name); + do { + n[slash - name] = 0; + name = n; + } while (!rmdir(name) && (slash = strrchr(name, '/'))); + } + return ret; +} + +static int check_local_mod(unsigned char *head) +{ + /* items in list are already sorted in the cache order, + * so we could do this a lot more efficiently by using + * tree_desc based traversal if we wanted to, but I am + * lazy, and who cares if removal of files is a tad + * slower than the theoretical maximum speed? + */ + int i, no_head; + int errs = 0; + + no_head = is_null_sha1(head); + for (i = 0; i < list.nr; i++) { + struct stat st; + int pos; + struct cache_entry *ce; + const char *name = list.name[i]; + unsigned char sha1[20]; + unsigned mode; + + pos = cache_name_pos(name, strlen(name)); + if (pos < 0) + continue; /* removing unmerged entry */ + ce = active_cache[pos]; + + if (lstat(ce->name, &st) < 0) { + if (errno != ENOENT) + fprintf(stderr, "warning: '%s': %s", + ce->name, strerror(errno)); + /* It already vanished from the working tree */ + continue; + } + else if (S_ISDIR(st.st_mode)) { + /* if a file was removed and it is now a + * directory, that is the same as ENOENT as + * far as git is concerned; we do not track + * directories. + */ + continue; + } + if (ce_match_stat(ce, &st, 0)) + errs = error("'%s' has local modifications " + "(hint: try -f)", ce->name); + if (no_head) + continue; + /* + * It is Ok to remove a newly added path, as long as + * it is cache-clean. + */ + if (get_tree_entry(head, name, sha1, &mode)) + continue; + /* + * Otherwise make sure the version from the HEAD + * matches the index. + */ + if (ce->ce_mode != create_ce_mode(mode) || + hashcmp(ce->sha1, sha1)) + errs = error("'%s' has changes staged in the index " + "(hint: try -f)", name); + } + return errs; +} + +static struct lock_file lock_file; + +int cmd_rm(int argc, const char **argv, const char *prefix) +{ + int i, newfd; + int show_only = 0, force = 0, index_only = 0, recursive = 0; + const char **pathspec; + char *seen; + + git_config(git_default_config); + + newfd = hold_lock_file_for_update(&lock_file, get_index_file(), 1); + + if (read_cache() < 0) + die("index file corrupt"); + + for (i = 1 ; i < argc ; i++) { + const char *arg = argv[i]; + + if (*arg != '-') + break; + else if (!strcmp(arg, "--")) { + i++; + break; + } + else if (!strcmp(arg, "-n")) + show_only = 1; + else if (!strcmp(arg, "--cached")) + index_only = 1; + else if (!strcmp(arg, "-f")) + force = 1; + else if (!strcmp(arg, "-r")) + recursive = 1; + else + usage(builtin_rm_usage); + } + if (argc <= i) + usage(builtin_rm_usage); + + pathspec = get_pathspec(prefix, argv + i); + seen = NULL; + for (i = 0; pathspec[i] ; i++) + /* nothing */; + seen = xcalloc(i, 1); + + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (!match_pathspec(pathspec, ce->name, ce_namelen(ce), 0, seen)) + continue; + add_list(ce->name); + } + + if (pathspec) { + const char *match; + for (i = 0; (match = pathspec[i]) != NULL ; i++) { + if (!seen[i]) + die("pathspec '%s' did not match any files", + match); + if (!recursive && seen[i] == MATCHED_RECURSIVELY) + die("not removing '%s' recursively without -r", + *match ? match : "."); + } + } + + /* + * If not forced, the file, the index and the HEAD (if exists) + * must match; but the file can already been removed, since + * this sequence is a natural "novice" way: + * + * rm F; git fm F + * + * Further, if HEAD commit exists, "diff-index --cached" must + * report no changes unless forced. + */ + if (!force) { + unsigned char sha1[20]; + if (get_sha1("HEAD", sha1)) + hashclr(sha1); + if (check_local_mod(sha1)) + exit(1); + } + + /* + * First remove the names from the index: we won't commit + * the index unless all of them succeed. + */ + for (i = 0; i < list.nr; i++) { + const char *path = list.name[i]; + printf("rm '%s'\n", path); + + 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, unless we used "--cached", remove the filenames from + * the workspace. If we fail to remove the first one, we + * abort the "git rm" (but once we've successfully removed + * any file at all, we'll go ahead and commit to it all: + * by then we've already committed ourselves and can't fail + * in the middle) + */ + if (!index_only) { + int removed = 0; + for (i = 0; i < list.nr; i++) { + const char *path = list.name[i]; + if (!remove_file(path)) { + removed = 1; + continue; + } + if (!removed) + die("git-rm: %s: %s", path, strerror(errno)); + } + } + + if (active_cache_changed) { + if (write_cache(newfd, active_cache, active_nr) || + close(newfd) || commit_lock_file(&lock_file)) + die("Unable to write new index file"); + } + + return 0; +} diff --git a/builtin-runstatus.c b/builtin-runstatus.c new file mode 100644 index 0000000000..4b489b1214 --- /dev/null +++ b/builtin-runstatus.c @@ -0,0 +1,36 @@ +#include "cache.h" +#include "wt-status.h" + +extern int wt_status_use_color; + +static const char runstatus_usage[] = +"git-runstatus [--color|--nocolor] [--amend] [--verbose] [--untracked]"; + +int cmd_runstatus(int argc, const char **argv, const char *prefix) +{ + struct wt_status s; + int i; + + git_config(git_status_config); + wt_status_prepare(&s); + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--color")) + wt_status_use_color = 1; + else if (!strcmp(argv[i], "--nocolor")) + wt_status_use_color = 0; + else if (!strcmp(argv[i], "--amend")) { + s.amend = 1; + s.reference = "HEAD^1"; + } + else if (!strcmp(argv[i], "--verbose")) + s.verbose = 1; + else if (!strcmp(argv[i], "--untracked")) + s.untracked = 1; + else + usage(runstatus_usage); + } + + wt_status_print(&s); + return s.commitable ? 0 : 1; +} diff --git a/builtin-shortlog.c b/builtin-shortlog.c new file mode 100644 index 0000000000..edb40429ec --- /dev/null +++ b/builtin-shortlog.c @@ -0,0 +1,341 @@ +#include "builtin.h" +#include "cache.h" +#include "commit.h" +#include "diff.h" +#include "path-list.h" +#include "revision.h" + +static const char shortlog_usage[] = +"git-shortlog [-n] [-s] [<commit-id>... ]"; + +static char *common_repo_prefix; + +static int compare_by_number(const void *a1, const void *a2) +{ + const struct path_list_item *i1 = a1, *i2 = a2; + const struct path_list *l1 = i1->util, *l2 = i2->util; + + if (l1->nr < l2->nr) + return 1; + else if (l1->nr == l2->nr) + return 0; + else + return -1; +} + +static struct path_list mailmap = {NULL, 0, 0, 0}; + +static int read_mailmap(const char *filename) +{ + char buffer[1024]; + FILE *f = fopen(filename, "r"); + + if (f == NULL) + return 1; + while (fgets(buffer, sizeof(buffer), f) != NULL) { + char *end_of_name, *left_bracket, *right_bracket; + char *name, *email; + int i; + if (buffer[0] == '#') { + static const char abbrev[] = "# repo-abbrev:"; + int abblen = sizeof(abbrev) - 1; + int len = strlen(buffer); + + if (len && buffer[len - 1] == '\n') + buffer[--len] = 0; + if (!strncmp(buffer, abbrev, abblen)) { + char *cp; + + if (common_repo_prefix) + free(common_repo_prefix); + common_repo_prefix = xmalloc(len); + + for (cp = buffer + abblen; isspace(*cp); cp++) + ; /* nothing */ + strcpy(common_repo_prefix, cp); + } + continue; + } + if ((left_bracket = strchr(buffer, '<')) == NULL) + continue; + if ((right_bracket = strchr(left_bracket + 1, '>')) == NULL) + continue; + if (right_bracket == left_bracket + 1) + continue; + for (end_of_name = left_bracket; end_of_name != buffer + && isspace(end_of_name[-1]); end_of_name--) + /* keep on looking */ + if (end_of_name == buffer) + continue; + name = xmalloc(end_of_name - buffer + 1); + strlcpy(name, buffer, end_of_name - buffer + 1); + email = xmalloc(right_bracket - left_bracket); + for (i = 0; i < right_bracket - left_bracket - 1; i++) + email[i] = tolower(left_bracket[i + 1]); + email[right_bracket - left_bracket - 1] = '\0'; + path_list_insert(email, &mailmap)->util = name; + } + fclose(f); + return 0; +} + +static int map_email(char *email, char *name, int maxlen) +{ + char *p; + struct path_list_item *item; + + /* autocomplete common developers */ + p = strchr(email, '>'); + if (!p) + return 0; + + *p = '\0'; + /* downcase the email address */ + for (p = email; *p; p++) + *p = tolower(*p); + item = path_list_lookup(email, &mailmap); + if (item != NULL) { + const char *realname = (const char *)item->util; + strncpy(name, realname, maxlen); + return 1; + } + return 0; +} + +static void insert_author_oneline(struct path_list *list, + const char *author, int authorlen, + const char *oneline, int onelinelen) +{ + const char *dot3 = common_repo_prefix; + char *buffer, *p; + struct path_list_item *item; + struct path_list *onelines; + + while (authorlen > 0 && isspace(author[authorlen - 1])) + authorlen--; + + buffer = xmalloc(authorlen + 1); + memcpy(buffer, author, authorlen); + buffer[authorlen] = '\0'; + + item = path_list_insert(buffer, list); + if (item->util == NULL) + item->util = xcalloc(1, sizeof(struct path_list)); + else + free(buffer); + + if (!strncmp(oneline, "[PATCH", 6)) { + char *eob = strchr(oneline, ']'); + + if (eob) { + while (isspace(eob[1]) && eob[1] != '\n') + eob++; + if (eob - oneline < onelinelen) { + onelinelen -= eob - oneline; + oneline = eob; + } + } + } + + while (onelinelen > 0 && isspace(oneline[0])) { + oneline++; + onelinelen--; + } + + while (onelinelen > 0 && isspace(oneline[onelinelen - 1])) + onelinelen--; + + buffer = xmalloc(onelinelen + 1); + memcpy(buffer, oneline, onelinelen); + buffer[onelinelen] = '\0'; + + if (dot3) { + int dot3len = strlen(dot3); + if (dot3len > 5) { + while ((p = strstr(buffer, dot3)) != NULL) { + int taillen = strlen(p) - dot3len; + memcpy(p, "/.../", 5); + memmove(p + 5, p + dot3len, taillen + 1); + } + } + } + + onelines = item->util; + if (onelines->nr >= onelines->alloc) { + onelines->alloc = alloc_nr(onelines->nr); + onelines->items = xrealloc(onelines->items, + onelines->alloc + * sizeof(struct path_list_item)); + } + + onelines->items[onelines->nr].util = NULL; + onelines->items[onelines->nr++].path = buffer; +} + +static void read_from_stdin(struct path_list *list) +{ + char buffer[1024]; + + while (fgets(buffer, sizeof(buffer), stdin) != NULL) { + char *bob; + if ((buffer[0] == 'A' || buffer[0] == 'a') && + !strncmp(buffer + 1, "uthor: ", 7) && + (bob = strchr(buffer + 7, '<')) != NULL) { + char buffer2[1024], offset = 0; + + if (map_email(bob + 1, buffer, sizeof(buffer))) + bob = buffer + strlen(buffer); + else { + offset = 8; + while (buffer + offset < bob && + isspace(bob[-1])) + bob--; + } + + while (fgets(buffer2, sizeof(buffer2), stdin) && + buffer2[0] != '\n') + ; /* chomp input */ + if (fgets(buffer2, sizeof(buffer2), stdin)) { + int l2 = strlen(buffer2); + int i; + for (i = 0; i < l2; i++) + if (!isspace(buffer2[i])) + break; + insert_author_oneline(list, + buffer + offset, + bob - buffer - offset, + buffer2 + i, l2 - i); + } + } + } +} + +static void get_from_rev(struct rev_info *rev, struct path_list *list) +{ + char scratch[1024]; + struct commit *commit; + + prepare_revision_walk(rev); + while ((commit = get_revision(rev)) != NULL) { + char *author = NULL, *oneline, *buffer; + int authorlen = authorlen, onelinelen; + + /* get author and oneline */ + for (buffer = commit->buffer; buffer && *buffer != '\0' && + *buffer != '\n'; ) { + char *eol = strchr(buffer, '\n'); + + if (eol == NULL) + eol = buffer + strlen(buffer); + else + eol++; + + if (!strncmp(buffer, "author ", 7)) { + char *bracket = strchr(buffer, '<'); + + if (bracket == NULL || bracket > eol) + die("Invalid commit buffer: %s", + sha1_to_hex(commit->object.sha1)); + + if (map_email(bracket + 1, scratch, + sizeof(scratch))) { + author = scratch; + authorlen = strlen(scratch); + } else { + if (bracket[-1] == ' ') + bracket--; + + author = buffer + 7; + authorlen = bracket - buffer - 7; + } + } + buffer = eol; + } + + if (author == NULL) + die ("Missing author: %s", + sha1_to_hex(commit->object.sha1)); + + if (buffer == NULL || *buffer == '\0') { + oneline = "<none>"; + onelinelen = sizeof(oneline) + 1; + } else { + char *eol; + + oneline = buffer + 1; + eol = strchr(oneline, '\n'); + if (eol == NULL) + onelinelen = strlen(oneline); + else + onelinelen = eol - oneline; + } + + insert_author_oneline(list, + author, authorlen, oneline, onelinelen); + } + +} + +int cmd_shortlog(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + struct path_list list = { NULL, 0, 0, 1 }; + int i, j, sort_by_number = 0, summary = 0; + + /* since -n is a shadowed rev argument, parse our args first */ + while (argc > 1) { + if (!strcmp(argv[1], "-n") || !strcmp(argv[1], "--numbered")) + sort_by_number = 1; + else if (!strcmp(argv[1], "-s") || + !strcmp(argv[1], "--summary")) + summary = 1; + else if (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) + usage(shortlog_usage); + else + break; + argv++; + argc--; + } + init_revisions(&rev, prefix); + argc = setup_revisions(argc, argv, &rev, NULL); + if (argc > 1) + die ("unrecognized argument: %s", argv[1]); + + if (!access(".mailmap", R_OK)) + read_mailmap(".mailmap"); + + if (rev.pending.nr == 0) + read_from_stdin(&list); + else + get_from_rev(&rev, &list); + + if (sort_by_number) + qsort(list.items, list.nr, sizeof(struct path_list_item), + compare_by_number); + + for (i = 0; i < list.nr; i++) { + struct path_list *onelines = list.items[i].util; + + if (summary) { + printf("%s: %d\n", list.items[i].path, onelines->nr); + } else { + printf("%s (%d):\n", list.items[i].path, onelines->nr); + for (j = onelines->nr - 1; j >= 0; j--) + printf(" %s\n", onelines->items[j].path); + printf("\n"); + } + + onelines->strdup_paths = 1; + path_list_clear(onelines, 1); + free(onelines); + list.items[i].util = NULL; + } + + list.strdup_paths = 1; + path_list_clear(&list, 1); + mailmap.strdup_paths = 1; + path_list_clear(&mailmap, 1); + + return 0; +} + diff --git a/builtin-show-branch.c b/builtin-show-branch.c new file mode 100644 index 0000000000..0d94e40df8 --- /dev/null +++ b/builtin-show-branch.c @@ -0,0 +1,913 @@ +#include "cache.h" +#include "commit.h" +#include "refs.h" +#include "builtin.h" + +static const char show_branch_usage[] = +"git-show-branch [--sparse] [--current] [--all] [--remotes] [--topo-order] [--more=count | --list | --independent | --merge-base ] [--topics] [<refs>...] | --reflog[=n[,b]] <branch>"; +static const char show_branch_usage_reflog[] = +"--reflog is incompatible with --all, --remotes, --independent or --merge-base"; + +static int default_num; +static int default_alloc; +static const char **default_arg; + +#define UNINTERESTING 01 + +#define REV_SHIFT 2 +#define MAX_REVS (FLAG_BITS - REV_SHIFT) /* should not exceed bits_per_int - REV_SHIFT */ + +#define DEFAULT_REFLOG 4 + +static struct commit *interesting(struct commit_list *list) +{ + while (list) { + struct commit *commit = list->item; + list = list->next; + if (commit->object.flags & UNINTERESTING) + continue; + return commit; + } + return NULL; +} + +static struct commit *pop_one_commit(struct commit_list **list_p) +{ + struct commit *commit; + struct commit_list *list; + list = *list_p; + commit = list->item; + *list_p = list->next; + free(list); + return commit; +} + +struct commit_name { + const char *head_name; /* which head's ancestor? */ + int generation; /* how many parents away from head_name */ +}; + +/* Name the commit as nth generation ancestor of head_name; + * we count only the first-parent relationship for naming purposes. + */ +static void name_commit(struct commit *commit, const char *head_name, int nth) +{ + struct commit_name *name; + if (!commit->util) + commit->util = xmalloc(sizeof(struct commit_name)); + name = commit->util; + name->head_name = head_name; + name->generation = nth; +} + +/* Parent is the first parent of the commit. We may name it + * as (n+1)th generation ancestor of the same head_name as + * commit is nth generation ancestor of, if that generation + * number is better than the name it already has. + */ +static void name_parent(struct commit *commit, struct commit *parent) +{ + struct commit_name *commit_name = commit->util; + struct commit_name *parent_name = parent->util; + if (!commit_name) + return; + if (!parent_name || + commit_name->generation + 1 < parent_name->generation) + name_commit(parent, commit_name->head_name, + commit_name->generation + 1); +} + +static int name_first_parent_chain(struct commit *c) +{ + int i = 0; + while (c) { + struct commit *p; + if (!c->util) + break; + if (!c->parents) + break; + p = c->parents->item; + if (!p->util) { + name_parent(c, p); + i++; + } + else + break; + c = p; + } + return i; +} + +static void name_commits(struct commit_list *list, + struct commit **rev, + char **ref_name, + int num_rev) +{ + struct commit_list *cl; + struct commit *c; + int i; + + /* First give names to the given heads */ + for (cl = list; cl; cl = cl->next) { + c = cl->item; + if (c->util) + continue; + for (i = 0; i < num_rev; i++) { + if (rev[i] == c) { + name_commit(c, ref_name[i], 0); + break; + } + } + } + + /* Then commits on the first parent ancestry chain */ + do { + i = 0; + for (cl = list; cl; cl = cl->next) { + i += name_first_parent_chain(cl->item); + } + } while (i); + + /* Finally, any unnamed commits */ + do { + i = 0; + for (cl = list; cl; cl = cl->next) { + struct commit_list *parents; + struct commit_name *n; + int nth; + c = cl->item; + if (!c->util) + continue; + n = c->util; + parents = c->parents; + nth = 0; + while (parents) { + struct commit *p = parents->item; + char newname[1000], *en; + parents = parents->next; + nth++; + if (p->util) + continue; + en = newname; + switch (n->generation) { + case 0: + en += sprintf(en, "%s", n->head_name); + break; + case 1: + en += sprintf(en, "%s^", n->head_name); + break; + default: + en += sprintf(en, "%s~%d", + n->head_name, n->generation); + break; + } + if (nth == 1) + en += sprintf(en, "^"); + else + en += sprintf(en, "^%d", nth); + name_commit(p, xstrdup(newname), 0); + i++; + name_first_parent_chain(p); + } + } + } while (i); +} + +static int mark_seen(struct commit *commit, struct commit_list **seen_p) +{ + if (!commit->object.flags) { + commit_list_insert(commit, seen_p); + return 1; + } + return 0; +} + +static void join_revs(struct commit_list **list_p, + struct commit_list **seen_p, + int num_rev, int extra) +{ + int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1); + int all_revs = all_mask & ~((1u << REV_SHIFT) - 1); + + while (*list_p) { + struct commit_list *parents; + int still_interesting = !!interesting(*list_p); + struct commit *commit = pop_one_commit(list_p); + int flags = commit->object.flags & all_mask; + + if (!still_interesting && extra <= 0) + break; + + mark_seen(commit, seen_p); + if ((flags & all_revs) == all_revs) + flags |= UNINTERESTING; + parents = commit->parents; + + while (parents) { + struct commit *p = parents->item; + int this_flag = p->object.flags; + parents = parents->next; + if ((this_flag & flags) == flags) + continue; + if (!p->object.parsed) + parse_commit(p); + if (mark_seen(p, seen_p) && !still_interesting) + extra--; + p->object.flags |= flags; + insert_by_date(p, list_p); + } + } + + /* + * Postprocess to complete well-poisoning. + * + * At this point we have all the commits we have seen in + * seen_p list. Mark anything that can be reached from + * uninteresting commits not interesting. + */ + for (;;) { + int changed = 0; + struct commit_list *s; + for (s = *seen_p; s; s = s->next) { + struct commit *c = s->item; + struct commit_list *parents; + + if (((c->object.flags & all_revs) != all_revs) && + !(c->object.flags & UNINTERESTING)) + continue; + + /* The current commit is either a merge base or + * already uninteresting one. Mark its parents + * as uninteresting commits _only_ if they are + * already parsed. No reason to find new ones + * here. + */ + parents = c->parents; + while (parents) { + struct commit *p = parents->item; + parents = parents->next; + if (!(p->object.flags & UNINTERESTING)) { + p->object.flags |= UNINTERESTING; + changed = 1; + } + } + } + if (!changed) + break; + } +} + +static void show_one_commit(struct commit *commit, int no_name) +{ + char pretty[256], *cp; + struct commit_name *name = commit->util; + if (commit->object.parsed) + pretty_print_commit(CMIT_FMT_ONELINE, commit, ~0, + pretty, sizeof(pretty), 0, NULL, NULL, 0); + else + strcpy(pretty, "(unavailable)"); + if (!strncmp(pretty, "[PATCH] ", 8)) + cp = pretty + 8; + else + cp = pretty; + + if (!no_name) { + if (name && name->head_name) { + printf("[%s", name->head_name); + if (name->generation) { + if (name->generation == 1) + printf("^"); + else + printf("~%d", name->generation); + } + printf("] "); + } + else + printf("[%s] ", + find_unique_abbrev(commit->object.sha1, 7)); + } + puts(cp); +} + +static char *ref_name[MAX_REVS + 1]; +static int ref_name_cnt; + +static const char *find_digit_prefix(const char *s, int *v) +{ + const char *p; + int ver; + char ch; + + for (p = s, ver = 0; + '0' <= (ch = *p) && ch <= '9'; + p++) + ver = ver * 10 + ch - '0'; + *v = ver; + return p; +} + + +static int version_cmp(const char *a, const char *b) +{ + while (1) { + int va, vb; + + a = find_digit_prefix(a, &va); + b = find_digit_prefix(b, &vb); + if (va != vb) + return va - vb; + + while (1) { + int ca = *a; + int cb = *b; + if ('0' <= ca && ca <= '9') + ca = 0; + if ('0' <= cb && cb <= '9') + cb = 0; + if (ca != cb) + return ca - cb; + if (!ca) + break; + a++; + b++; + } + if (!*a && !*b) + return 0; + } +} + +static int compare_ref_name(const void *a_, const void *b_) +{ + const char * const*a = a_, * const*b = b_; + return version_cmp(*a, *b); +} + +static void sort_ref_range(int bottom, int top) +{ + qsort(ref_name + bottom, top - bottom, sizeof(ref_name[0]), + compare_ref_name); +} + +static int append_ref(const char *refname, const unsigned char *sha1, + int allow_dups) +{ + struct commit *commit = lookup_commit_reference_gently(sha1, 1); + int i; + + if (!commit) + return 0; + + if (!allow_dups) { + /* Avoid adding the same thing twice */ + for (i = 0; i < ref_name_cnt; i++) + if (!strcmp(refname, ref_name[i])) + return 0; + } + if (MAX_REVS <= ref_name_cnt) { + fprintf(stderr, "warning: ignoring %s; " + "cannot handle more than %d refs\n", + refname, MAX_REVS); + return 0; + } + ref_name[ref_name_cnt++] = xstrdup(refname); + ref_name[ref_name_cnt] = NULL; + return 0; +} + +static int append_head_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + unsigned char tmp[20]; + int ofs = 11; + if (strncmp(refname, "refs/heads/", ofs)) + return 0; + /* If both heads/foo and tags/foo exists, get_sha1 would + * get confused. + */ + if (get_sha1(refname + ofs, tmp) || hashcmp(tmp, sha1)) + ofs = 5; + return append_ref(refname + ofs, sha1, 0); +} + +static int append_remote_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + unsigned char tmp[20]; + int ofs = 13; + if (strncmp(refname, "refs/remotes/", ofs)) + return 0; + /* If both heads/foo and tags/foo exists, get_sha1 would + * get confused. + */ + if (get_sha1(refname + ofs, tmp) || hashcmp(tmp, sha1)) + ofs = 5; + return append_ref(refname + ofs, sha1, 0); +} + +static int append_tag_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + if (strncmp(refname, "refs/tags/", 10)) + return 0; + return append_ref(refname + 5, sha1, 0); +} + +static const char *match_ref_pattern = NULL; +static int match_ref_slash = 0; +static int count_slash(const char *s) +{ + int cnt = 0; + while (*s) + if (*s++ == '/') + cnt++; + return cnt; +} + +static int append_matching_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + /* we want to allow pattern hold/<asterisk> to show all + * branches under refs/heads/hold/, and v0.99.9? to show + * refs/tags/v0.99.9a and friends. + */ + const char *tail; + int slash = count_slash(refname); + for (tail = refname; *tail && match_ref_slash < slash; ) + if (*tail++ == '/') + slash--; + if (!*tail) + return 0; + if (fnmatch(match_ref_pattern, tail, 0)) + return 0; + if (!strncmp("refs/heads/", refname, 11)) + return append_head_ref(refname, sha1, flag, cb_data); + if (!strncmp("refs/tags/", refname, 10)) + return append_tag_ref(refname, sha1, flag, cb_data); + return append_ref(refname, sha1, 0); +} + +static void snarf_refs(int head, int remotes) +{ + if (head) { + int orig_cnt = ref_name_cnt; + for_each_ref(append_head_ref, NULL); + sort_ref_range(orig_cnt, ref_name_cnt); + } + if (remotes) { + int orig_cnt = ref_name_cnt; + for_each_ref(append_remote_ref, NULL); + sort_ref_range(orig_cnt, ref_name_cnt); + } +} + +static int rev_is_head(char *head, int headlen, char *name, + unsigned char *head_sha1, unsigned char *sha1) +{ + if ((!head[0]) || + (head_sha1 && sha1 && hashcmp(head_sha1, sha1))) + return 0; + if (!strncmp(head, "refs/heads/", 11)) + head += 11; + if (!strncmp(name, "refs/heads/", 11)) + name += 11; + else if (!strncmp(name, "heads/", 6)) + name += 6; + return !strcmp(head, name); +} + +static int show_merge_base(struct commit_list *seen, int num_rev) +{ + int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1); + int all_revs = all_mask & ~((1u << REV_SHIFT) - 1); + int exit_status = 1; + + while (seen) { + struct commit *commit = pop_one_commit(&seen); + int flags = commit->object.flags & all_mask; + if (!(flags & UNINTERESTING) && + ((flags & all_revs) == all_revs)) { + puts(sha1_to_hex(commit->object.sha1)); + exit_status = 0; + commit->object.flags |= UNINTERESTING; + } + } + return exit_status; +} + +static int show_independent(struct commit **rev, + int num_rev, + char **ref_name, + unsigned int *rev_mask) +{ + int i; + + for (i = 0; i < num_rev; i++) { + struct commit *commit = rev[i]; + unsigned int flag = rev_mask[i]; + + if (commit->object.flags == flag) + puts(sha1_to_hex(commit->object.sha1)); + commit->object.flags |= UNINTERESTING; + } + return 0; +} + +static void append_one_rev(const char *av) +{ + unsigned char revkey[20]; + if (!get_sha1(av, revkey)) { + append_ref(av, revkey, 0); + return; + } + if (strchr(av, '*') || strchr(av, '?') || strchr(av, '[')) { + /* glob style match */ + int saved_matches = ref_name_cnt; + match_ref_pattern = av; + match_ref_slash = count_slash(av); + for_each_ref(append_matching_ref, NULL); + if (saved_matches == ref_name_cnt && + ref_name_cnt < MAX_REVS) + error("no matching refs with %s", av); + if (saved_matches + 1 < ref_name_cnt) + sort_ref_range(saved_matches, ref_name_cnt); + return; + } + die("bad sha1 reference %s", av); +} + +static int git_show_branch_config(const char *var, const char *value) +{ + if (!strcmp(var, "showbranch.default")) { + if (default_alloc <= default_num + 1) { + default_alloc = default_alloc * 3 / 2 + 20; + default_arg = xrealloc(default_arg, sizeof *default_arg * default_alloc); + } + default_arg[default_num++] = xstrdup(value); + default_arg[default_num] = NULL; + return 0; + } + + return git_default_config(var, value); +} + +static int omit_in_dense(struct commit *commit, struct commit **rev, int n) +{ + /* If the commit is tip of the named branches, do not + * omit it. + * Otherwise, if it is a merge that is reachable from only one + * tip, it is not that interesting. + */ + int i, flag, count; + for (i = 0; i < n; i++) + if (rev[i] == commit) + return 0; + flag = commit->object.flags; + for (i = count = 0; i < n; i++) { + if (flag & (1u << (i + REV_SHIFT))) + count++; + } + if (count == 1) + return 1; + return 0; +} + +static void parse_reflog_param(const char *arg, int *cnt, const char **base) +{ + char *ep; + *cnt = strtoul(arg, &ep, 10); + if (*ep == ',') + *base = ep + 1; + else if (*ep) + die("unrecognized reflog param '%s'", arg + 9); + else + *base = NULL; + if (*cnt <= 0) + *cnt = DEFAULT_REFLOG; +} + +int cmd_show_branch(int ac, const char **av, const char *prefix) +{ + struct commit *rev[MAX_REVS], *commit; + char *reflog_msg[MAX_REVS]; + struct commit_list *list = NULL, *seen = NULL; + unsigned int rev_mask[MAX_REVS]; + int num_rev, i, extra = 0; + int all_heads = 0, all_remotes = 0; + int all_mask, all_revs; + int lifo = 1; + char head[128]; + const char *head_p; + int head_len; + unsigned char head_sha1[20]; + int merge_base = 0; + int independent = 0; + int no_name = 0; + int sha1_name = 0; + int shown_merge_point = 0; + int with_current_branch = 0; + int head_at = -1; + int topics = 0; + int dense = 1; + int reflog = 0; + const char *reflog_base = NULL; + + git_config(git_show_branch_config); + + /* If nothing is specified, try the default first */ + if (ac == 1 && default_num) { + ac = default_num + 1; + av = default_arg - 1; /* ick; we would not address av[0] */ + } + + while (1 < ac && av[1][0] == '-') { + const char *arg = av[1]; + if (!strcmp(arg, "--")) { + ac--; av++; + break; + } + else if (!strcmp(arg, "--all") || !strcmp(arg, "-a")) + all_heads = all_remotes = 1; + else if (!strcmp(arg, "--remotes") || !strcmp(arg, "-r")) + all_remotes = 1; + else if (!strcmp(arg, "--more")) + extra = 1; + else if (!strcmp(arg, "--list")) + extra = -1; + else if (!strcmp(arg, "--no-name")) + no_name = 1; + else if (!strcmp(arg, "--current")) + with_current_branch = 1; + else if (!strcmp(arg, "--sha1-name")) + sha1_name = 1; + else if (!strncmp(arg, "--more=", 7)) + extra = atoi(arg + 7); + else if (!strcmp(arg, "--merge-base")) + merge_base = 1; + else if (!strcmp(arg, "--independent")) + independent = 1; + else if (!strcmp(arg, "--topo-order")) + lifo = 1; + else if (!strcmp(arg, "--topics")) + topics = 1; + else if (!strcmp(arg, "--sparse")) + dense = 0; + else if (!strcmp(arg, "--date-order")) + lifo = 0; + else if (!strcmp(arg, "--reflog") || !strcmp(arg, "-g")) { + reflog = DEFAULT_REFLOG; + } + else if (!strncmp(arg, "--reflog=", 9)) + parse_reflog_param(arg + 9, &reflog, &reflog_base); + else if (!strncmp(arg, "-g=", 3)) + parse_reflog_param(arg + 3, &reflog, &reflog_base); + else + usage(show_branch_usage); + ac--; av++; + } + ac--; av++; + + if (extra || reflog) { + /* "listing" mode is incompatible with + * independent nor merge-base modes. + */ + if (independent || merge_base) + usage(show_branch_usage); + if (reflog && ((0 < extra) || all_heads || all_remotes)) + /* + * Asking for --more in reflog mode does not + * make sense. --list is Ok. + * + * Also --all and --remotes do not make sense either. + */ + usage(show_branch_usage_reflog); + } + + /* If nothing is specified, show all branches by default */ + if (ac + all_heads + all_remotes == 0) + all_heads = 1; + + if (reflog) { + unsigned char sha1[20]; + char nth_desc[256]; + char *ref; + int base = 0; + + if (ac == 0) { + static const char *fake_av[2]; + const char *refname; + + refname = resolve_ref("HEAD", sha1, 1, NULL); + fake_av[0] = xstrdup(refname); + fake_av[1] = NULL; + av = fake_av; + ac = 1; + } + if (ac != 1) + die("--reflog option needs one branch name"); + + if (MAX_REVS < reflog) + die("Only %d entries can be shown at one time.", + MAX_REVS); + if (!dwim_ref(*av, strlen(*av), sha1, &ref)) + die("No such ref %s", *av); + + /* Has the base been specified? */ + if (reflog_base) { + char *ep; + base = strtoul(reflog_base, &ep, 10); + if (*ep) { + /* Ah, that is a date spec... */ + unsigned long at; + at = approxidate(reflog_base); + read_ref_at(ref, at, -1, sha1, NULL, + NULL, NULL, &base); + } + } + + for (i = 0; i < reflog; i++) { + char *logmsg, *msg, *m; + unsigned long timestamp; + int tz; + + if (read_ref_at(ref, 0, base+i, sha1, &logmsg, + ×tamp, &tz, NULL)) { + reflog = i; + break; + } + msg = strchr(logmsg, '\t'); + if (!msg) + msg = "(none)"; + else + msg++; + m = xmalloc(strlen(msg) + 200); + sprintf(m, "(%s) %s", + show_date(timestamp, tz, 1), + msg); + reflog_msg[i] = m; + free(logmsg); + sprintf(nth_desc, "%s@{%d}", *av, base+i); + append_ref(nth_desc, sha1, 1); + } + } + else if (all_heads + all_remotes) + snarf_refs(all_heads, all_remotes); + else { + while (0 < ac) { + append_one_rev(*av); + ac--; av++; + } + } + + head_p = resolve_ref("HEAD", head_sha1, 1, NULL); + if (head_p) { + head_len = strlen(head_p); + memcpy(head, head_p, head_len + 1); + } + else { + head_len = 0; + head[0] = 0; + } + + if (with_current_branch && head_p) { + int has_head = 0; + for (i = 0; !has_head && i < ref_name_cnt; i++) { + /* We are only interested in adding the branch + * HEAD points at. + */ + if (rev_is_head(head, + head_len, + ref_name[i], + head_sha1, NULL)) + has_head++; + } + if (!has_head) { + int pfxlen = strlen("refs/heads/"); + append_one_rev(head + pfxlen); + } + } + + if (!ref_name_cnt) { + fprintf(stderr, "No revs to be shown.\n"); + exit(0); + } + + for (num_rev = 0; ref_name[num_rev]; num_rev++) { + unsigned char revkey[20]; + unsigned int flag = 1u << (num_rev + REV_SHIFT); + + if (MAX_REVS <= num_rev) + die("cannot handle more than %d revs.", MAX_REVS); + if (get_sha1(ref_name[num_rev], revkey)) + die("'%s' is not a valid ref.", ref_name[num_rev]); + commit = lookup_commit_reference(revkey); + if (!commit) + die("cannot find commit %s (%s)", + ref_name[num_rev], revkey); + parse_commit(commit); + mark_seen(commit, &seen); + + /* rev#0 uses bit REV_SHIFT, rev#1 uses bit REV_SHIFT+1, + * and so on. REV_SHIFT bits from bit 0 are used for + * internal bookkeeping. + */ + commit->object.flags |= flag; + if (commit->object.flags == flag) + insert_by_date(commit, &list); + rev[num_rev] = commit; + } + for (i = 0; i < num_rev; i++) + rev_mask[i] = rev[i]->object.flags; + + if (0 <= extra) + join_revs(&list, &seen, num_rev, extra); + + sort_by_date(&seen); + + if (merge_base) + return show_merge_base(seen, num_rev); + + if (independent) + return show_independent(rev, num_rev, ref_name, rev_mask); + + /* Show list; --more=-1 means list-only */ + if (1 < num_rev || extra < 0) { + for (i = 0; i < num_rev; i++) { + int j; + int is_head = rev_is_head(head, + head_len, + ref_name[i], + head_sha1, + rev[i]->object.sha1); + if (extra < 0) + printf("%c [%s] ", + is_head ? '*' : ' ', ref_name[i]); + else { + for (j = 0; j < i; j++) + putchar(' '); + printf("%c [%s] ", + is_head ? '*' : '!', ref_name[i]); + } + + if (!reflog) { + /* header lines never need name */ + show_one_commit(rev[i], 1); + } + else + puts(reflog_msg[i]); + + if (is_head) + head_at = i; + } + if (0 <= extra) { + for (i = 0; i < num_rev; i++) + putchar('-'); + putchar('\n'); + } + } + if (extra < 0) + exit(0); + + /* Sort topologically */ + sort_in_topological_order(&seen, lifo); + + /* Give names to commits */ + if (!sha1_name && !no_name) + name_commits(seen, rev, ref_name, num_rev); + + all_mask = ((1u << (REV_SHIFT + num_rev)) - 1); + all_revs = all_mask & ~((1u << REV_SHIFT) - 1); + + while (seen) { + struct commit *commit = pop_one_commit(&seen); + int this_flag = commit->object.flags; + int is_merge_point = ((this_flag & all_revs) == all_revs); + + shown_merge_point |= is_merge_point; + + if (1 < num_rev) { + int is_merge = !!(commit->parents && + commit->parents->next); + if (topics && + !is_merge_point && + (this_flag & (1u << REV_SHIFT))) + continue; + if (dense && is_merge && + omit_in_dense(commit, rev, num_rev)) + continue; + for (i = 0; i < num_rev; i++) { + int mark; + if (!(this_flag & (1u << (i + REV_SHIFT)))) + mark = ' '; + else if (is_merge) + mark = '-'; + else if (i == head_at) + mark = '*'; + else + mark = '+'; + putchar(mark); + } + putchar(' '); + } + show_one_commit(commit, no_name); + + if (shown_merge_point && --extra < 0) + break; + } + return 0; +} diff --git a/builtin-show-ref.c b/builtin-show-ref.c new file mode 100644 index 0000000000..853f13f6ae --- /dev/null +++ b/builtin-show-ref.c @@ -0,0 +1,250 @@ +#include "cache.h" +#include "refs.h" +#include "object.h" +#include "tag.h" +#include "path-list.h" + +static const char show_ref_usage[] = "git show-ref [-q|--quiet] [--verify] [-h|--head] [-d|--dereference] [-s|--hash[=<length>]] [--abbrev[=<length>]] [--tags] [--heads] [--] [pattern*] < ref-list"; + +static int deref_tags = 0, show_head = 0, tags_only = 0, heads_only = 0, + found_match = 0, verify = 0, quiet = 0, hash_only = 0, abbrev = 0; +static const char **pattern; + +static void show_one(const char *refname, const unsigned char *sha1) +{ + const char *hex = find_unique_abbrev(sha1, abbrev); + if (hash_only) + printf("%s\n", hex); + else + printf("%s %s\n", hex, refname); +} + +static int show_ref(const char *refname, const unsigned char *sha1, int flag, void *cbdata) +{ + struct object *obj; + const char *hex; + unsigned char peeled[20]; + + if (tags_only || heads_only) { + int match; + + match = heads_only && !strncmp(refname, "refs/heads/", 11); + match |= tags_only && !strncmp(refname, "refs/tags/", 10); + if (!match) + return 0; + } + if (pattern) { + int reflen = strlen(refname); + const char **p = pattern, *m; + while ((m = *p++) != NULL) { + int len = strlen(m); + if (len > reflen) + continue; + if (memcmp(m, refname + reflen - len, len)) + continue; + if (len == reflen) + goto match; + /* "--verify" requires an exact match */ + if (verify) + continue; + if (refname[reflen - len - 1] == '/') + goto match; + } + return 0; + } + +match: + found_match++; + + /* This changes the semantics slightly that even under quiet we + * detect and return error if the repository is corrupt and + * ref points at a nonexistent object. + */ + if (!has_sha1_file(sha1)) + die("git-show-ref: bad ref %s (%s)", refname, + sha1_to_hex(sha1)); + + if (quiet) + return 0; + + show_one(refname, sha1); + + if (!deref_tags) + return 0; + + if ((flag & REF_ISPACKED) && !peel_ref(refname, peeled)) { + if (!is_null_sha1(peeled)) { + hex = find_unique_abbrev(peeled, abbrev); + printf("%s %s^{}\n", hex, refname); + } + } + else { + obj = parse_object(sha1); + if (!obj) + die("git-show-ref: bad ref %s (%s)", refname, + sha1_to_hex(sha1)); + if (obj->type == OBJ_TAG) { + obj = deref_tag(obj, refname, 0); + hex = find_unique_abbrev(obj->sha1, abbrev); + printf("%s %s^{}\n", hex, refname); + } + } + return 0; +} + +static int add_existing(const char *refname, const unsigned char *sha1, int flag, void *cbdata) +{ + struct path_list *list = (struct path_list *)cbdata; + path_list_insert(refname, list); + return 0; +} + +/* + * read "^(?:<anything>\s)?<refname>(?:\^\{\})?$" from the standard input, + * and + * (1) strip "^{}" at the end of line if any; + * (2) ignore if match is provided and does not head-match refname; + * (3) warn if refname is not a well-formed refname and skip; + * (4) ignore if refname is a ref that exists in the local repository; + * (5) otherwise output the line. + */ +static int exclude_existing(const char *match) +{ + static struct path_list existing_refs = { NULL, 0, 0, 0 }; + char buf[1024]; + int matchlen = match ? strlen(match) : 0; + + for_each_ref(add_existing, &existing_refs); + while (fgets(buf, sizeof(buf), stdin)) { + char *ref; + int len = strlen(buf); + + if (len > 0 && buf[len - 1] == '\n') + buf[--len] = '\0'; + if (3 <= len && !strcmp(buf + len - 3, "^{}")) { + len -= 3; + buf[len] = '\0'; + } + for (ref = buf + len; buf < ref; ref--) + if (isspace(ref[-1])) + break; + if (match) { + int reflen = buf + len - ref; + if (reflen < matchlen) + continue; + if (strncmp(ref, match, matchlen)) + continue; + } + if (check_ref_format(ref)) { + fprintf(stderr, "warning: ref '%s' ignored\n", ref); + continue; + } + if (!path_list_has_path(&existing_refs, ref)) { + printf("%s\n", buf); + } + } + return 0; +} + +int cmd_show_ref(int argc, const char **argv, const char *prefix) +{ + int i; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (*arg != '-') { + pattern = argv + i; + break; + } + if (!strcmp(arg, "--")) { + pattern = argv + i + 1; + if (!*pattern) + pattern = NULL; + break; + } + if (!strcmp(arg, "-q") || !strcmp(arg, "--quiet")) { + quiet = 1; + continue; + } + if (!strcmp(arg, "-h") || !strcmp(arg, "--head")) { + show_head = 1; + continue; + } + if (!strcmp(arg, "-d") || !strcmp(arg, "--dereference")) { + deref_tags = 1; + continue; + } + if (!strcmp(arg, "-s") || !strcmp(arg, "--hash")) { + hash_only = 1; + continue; + } + if (!strncmp(arg, "--hash=", 7) || + (!strncmp(arg, "--abbrev", 8) && + (arg[8] == '=' || arg[8] == '\0'))) { + if (arg[2] != 'h' && !arg[8]) + /* --abbrev only */ + abbrev = DEFAULT_ABBREV; + else { + /* --hash= or --abbrev= */ + char *end; + if (arg[2] == 'h') { + hash_only = 1; + arg += 7; + } + else + arg += 9; + abbrev = strtoul(arg, &end, 10); + if (*end || abbrev > 40) + usage(show_ref_usage); + if (abbrev < MINIMUM_ABBREV) + abbrev = MINIMUM_ABBREV; + } + continue; + } + if (!strcmp(arg, "--verify")) { + verify = 1; + continue; + } + if (!strcmp(arg, "--tags")) { + tags_only = 1; + continue; + } + if (!strcmp(arg, "--heads")) { + heads_only = 1; + continue; + } + if (!strcmp(arg, "--exclude-existing")) + return exclude_existing(NULL); + if (!strncmp(arg, "--exclude-existing=", 19)) + return exclude_existing(arg + 19); + usage(show_ref_usage); + } + + if (verify) { + unsigned char sha1[20]; + + while (*pattern) { + if (!strncmp(*pattern, "refs/", 5) && + resolve_ref(*pattern, sha1, 1, NULL)) { + if (!quiet) + show_one(*pattern, sha1); + } + else if (!quiet) + die("'%s' - not a valid ref", *pattern); + else + return 1; + pattern++; + } + return 0; + } + + if (show_head) + head_ref(show_ref, NULL); + for_each_ref(show_ref, NULL); + if (!found_match) { + if (verify && !quiet) + die("No match"); + return 1; + } + return 0; +} diff --git a/builtin-stripspace.c b/builtin-stripspace.c new file mode 100644 index 0000000000..f0d4d9e2d1 --- /dev/null +++ b/builtin-stripspace.c @@ -0,0 +1,58 @@ +#include "builtin.h" + +/* + * Remove empty lines from the beginning and end. + * + * Turn multiple consecutive empty lines into just one + * empty line. Return true if it is an incomplete line. + */ +static int cleanup(char *line) +{ + int len = strlen(line); + + if (len && line[len-1] == '\n') { + if (len == 1) + return 0; + do { + unsigned char c = line[len-2]; + if (!isspace(c)) + break; + line[len-2] = '\n'; + len--; + line[len] = 0; + } while (len > 1); + return 0; + } + return 1; +} + +void stripspace(FILE *in, FILE *out) +{ + int empties = -1; + int incomplete = 0; + char line[1024]; + + while (fgets(line, sizeof(line), in)) { + incomplete = cleanup(line); + + /* Not just an empty line? */ + if (line[0] != '\n') { + if (empties > 0) + fputc('\n', out); + empties = 0; + fputs(line, out); + continue; + } + if (empties < 0) + continue; + empties++; + } + if (incomplete) + fputc('\n', out); +} + +int cmd_stripspace(int argc, const char **argv, const char *prefix) +{ + stripspace(stdin, stdout); + return 0; +} diff --git a/builtin-symbolic-ref.c b/builtin-symbolic-ref.c new file mode 100644 index 0000000000..d41b40640b --- /dev/null +++ b/builtin-symbolic-ref.c @@ -0,0 +1,71 @@ +#include "builtin.h" +#include "cache.h" +#include "refs.h" + +static const char git_symbolic_ref_usage[] = +"git-symbolic-ref [-q] [-m <reason>] name [ref]"; + +static void check_symref(const char *HEAD, int quiet) +{ + unsigned char sha1[20]; + int flag; + const char *refs_heads_master = resolve_ref(HEAD, sha1, 0, &flag); + + if (!refs_heads_master) + die("No such ref: %s", HEAD); + else if (!(flag & REF_ISSYMREF)) { + if (!quiet) + die("ref %s is not a symbolic ref", HEAD); + else + exit(1); + } + puts(refs_heads_master); +} + +int cmd_symbolic_ref(int argc, const char **argv, const char *prefix) +{ + int quiet = 0; + const char *msg = NULL; + + git_config(git_default_config); + + while (1 < argc) { + const char *arg = argv[1]; + if (arg[0] != '-') + break; + else if (!strcmp("-q", arg)) + quiet = 1; + else if (!strcmp("-m", arg)) { + argc--; + argv++; + if (argc <= 1) + break; + msg = argv[1]; + if (!*msg) + die("Refusing to perform update with empty message"); + if (strchr(msg, '\n')) + die("Refusing to perform update with \\n in message"); + } + else if (!strcmp("--", arg)) { + argc--; + argv++; + break; + } + else + die("unknown option %s", arg); + argc--; + argv++; + } + + switch (argc) { + case 2: + check_symref(argv[1], quiet); + break; + case 3: + create_symref(argv[1], argv[2], msg); + break; + default: + usage(git_symbolic_ref_usage); + } + return 0; +} diff --git a/builtin-tar-tree.c b/builtin-tar-tree.c new file mode 100644 index 0000000000..8055ddab9b --- /dev/null +++ b/builtin-tar-tree.c @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2005, 2006 Rene Scharfe + */ +#include "cache.h" +#include "commit.h" +#include "tar.h" +#include "builtin.h" +#include "quote.h" + +static const char tar_tree_usage[] = +"git-tar-tree [--remote=<repo>] <tree-ish> [basedir]\n" +"*** Note that this command is now deprecated; use git-archive instead."; + +int cmd_tar_tree(int argc, const char **argv, const char *prefix) +{ + /* + * git-tar-tree is now a wrapper around git-archive --format=tar + * + * $0 --remote=<repo> arg... ==> + * git-archive --format=tar --remote=<repo> arg... + * $0 tree-ish ==> + * git-archive --format=tar tree-ish + * $0 tree-ish basedir ==> + * git-archive --format-tar --prefix=basedir tree-ish + */ + int i; + const char **nargv = xcalloc(sizeof(*nargv), argc + 2); + char *basedir_arg; + int nargc = 0; + + nargv[nargc++] = "git-archive"; + nargv[nargc++] = "--format=tar"; + + if (2 <= argc && !strncmp("--remote=", argv[1], 9)) { + nargv[nargc++] = argv[1]; + argv++; + argc--; + } + switch (argc) { + default: + usage(tar_tree_usage); + break; + case 3: + /* base-path */ + basedir_arg = xmalloc(strlen(argv[2]) + 11); + sprintf(basedir_arg, "--prefix=%s/", argv[2]); + nargv[nargc++] = basedir_arg; + /* fallthru */ + case 2: + /* tree-ish */ + nargv[nargc++] = argv[1]; + } + nargv[nargc] = NULL; + + fprintf(stderr, + "*** git-tar-tree is now deprecated.\n" + "*** Running git-archive instead.\n***"); + for (i = 0; i < nargc; i++) { + fputc(' ', stderr); + sq_quote_print(stderr, nargv[i]); + } + fputc('\n', stderr); + return cmd_archive(nargc, nargv, prefix); +} + +/* ustar header + extended global header content */ +#define RECORDSIZE (512) +#define HEADERSIZE (2 * RECORDSIZE) + +int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix) +{ + char buffer[HEADERSIZE]; + struct ustar_header *header = (struct ustar_header *)buffer; + char *content = buffer + RECORDSIZE; + ssize_t n; + + n = read_in_full(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 = write_in_full(1, content + 11, 41); + if (n < 41) + die("git-get-tar-commit-id: write error"); + + return 0; +} diff --git a/builtin-unpack-objects.c b/builtin-unpack-objects.c new file mode 100644 index 0000000000..d351e02649 --- /dev/null +++ b/builtin-unpack-objects.c @@ -0,0 +1,414 @@ +#include "builtin.h" +#include "cache.h" +#include "object.h" +#include "delta.h" +#include "pack.h" +#include "blob.h" +#include "commit.h" +#include "tag.h" +#include "tree.h" + +static int dry_run, quiet, recover, has_errors; +static const char unpack_usage[] = "git-unpack-objects [-n] [-q] [-r] < pack-file"; + +/* We always read in 4kB chunks. */ +static unsigned char buffer[4096]; +static unsigned long offset, len, consumed_bytes; +static SHA_CTX ctx; + +/* + * Make sure at least "min" bytes are available in the buffer, and + * return the pointer to the buffer. + */ +static void *fill(int min) +{ + if (min <= len) + return buffer + offset; + if (min > sizeof(buffer)) + die("cannot fill %d bytes", min); + if (offset) { + SHA1_Update(&ctx, buffer, offset); + memmove(buffer, buffer + offset, len); + offset = 0; + } + do { + int ret = xread(0, buffer + len, sizeof(buffer) - len); + if (ret <= 0) { + if (!ret) + die("early EOF"); + die("read error on input: %s", strerror(errno)); + } + len += ret; + } while (len < min); + return buffer; +} + +static void use(int bytes) +{ + if (bytes > len) + die("used more bytes than were available"); + len -= bytes; + offset += bytes; + consumed_bytes += bytes; +} + +static void *get_data(unsigned long size) +{ + z_stream stream; + void *buf = xmalloc(size); + + memset(&stream, 0, sizeof(stream)); + + stream.next_out = buf; + stream.avail_out = size; + stream.next_in = fill(1); + stream.avail_in = len; + inflateInit(&stream); + + for (;;) { + int ret = inflate(&stream, 0); + use(len - stream.avail_in); + if (stream.total_out == size && ret == Z_STREAM_END) + break; + if (ret != Z_OK) { + error("inflate returned %d\n", ret); + free(buf); + buf = NULL; + if (!recover) + exit(1); + has_errors = 1; + break; + } + stream.next_in = fill(1); + stream.avail_in = len; + } + inflateEnd(&stream); + return buf; +} + +struct delta_info { + unsigned char base_sha1[20]; + unsigned long base_offset; + unsigned long size; + void *delta; + unsigned nr; + struct delta_info *next; +}; + +static struct delta_info *delta_list; + +static void add_delta_to_list(unsigned nr, unsigned const char *base_sha1, + unsigned long base_offset, + void *delta, unsigned long size) +{ + struct delta_info *info = xmalloc(sizeof(*info)); + + hashcpy(info->base_sha1, base_sha1); + info->base_offset = base_offset; + info->size = size; + info->delta = delta; + info->nr = nr; + info->next = delta_list; + delta_list = info; +} + +struct obj_info { + unsigned long offset; + unsigned char sha1[20]; +}; + +static struct obj_info *obj_list; + +static void added_object(unsigned nr, const char *type, void *data, + unsigned long size); + +static void write_object(unsigned nr, void *buf, unsigned long size, + const char *type) +{ + if (write_sha1_file(buf, size, type, obj_list[nr].sha1) < 0) + die("failed to write object"); + added_object(nr, type, buf, size); +} + +static void resolve_delta(unsigned nr, const char *type, + void *base, unsigned long base_size, + void *delta, unsigned long delta_size) +{ + void *result; + unsigned long result_size; + + result = patch_delta(base, base_size, + delta, delta_size, + &result_size); + if (!result) + die("failed to apply delta"); + free(delta); + write_object(nr, result, result_size, type); + free(result); +} + +static void added_object(unsigned nr, const char *type, void *data, + unsigned long size) +{ + struct delta_info **p = &delta_list; + struct delta_info *info; + + while ((info = *p) != NULL) { + if (!hashcmp(info->base_sha1, obj_list[nr].sha1) || + info->base_offset == obj_list[nr].offset) { + *p = info->next; + p = &delta_list; + resolve_delta(info->nr, type, data, size, + info->delta, info->size); + free(info); + continue; + } + p = &info->next; + } +} + +static void unpack_non_delta_entry(enum object_type kind, unsigned long size, + unsigned nr) +{ + void *buf = get_data(size); + const char *type; + + switch (kind) { + case OBJ_COMMIT: type = commit_type; break; + case OBJ_TREE: type = tree_type; break; + case OBJ_BLOB: type = blob_type; break; + case OBJ_TAG: type = tag_type; break; + default: die("bad type %d", kind); + } + if (!dry_run && buf) + write_object(nr, buf, size, type); + free(buf); +} + +static void unpack_delta_entry(enum object_type kind, unsigned long delta_size, + unsigned nr) +{ + void *delta_data, *base; + unsigned long base_size; + char type[20]; + unsigned char base_sha1[20]; + + if (kind == OBJ_REF_DELTA) { + hashcpy(base_sha1, fill(20)); + use(20); + delta_data = get_data(delta_size); + if (dry_run || !delta_data) { + free(delta_data); + return; + } + if (!has_sha1_file(base_sha1)) { + hashcpy(obj_list[nr].sha1, null_sha1); + add_delta_to_list(nr, base_sha1, 0, delta_data, delta_size); + return; + } + } else { + unsigned base_found = 0; + unsigned char *pack, c; + unsigned long base_offset; + unsigned lo, mid, hi; + + pack = fill(1); + c = *pack; + use(1); + base_offset = c & 127; + while (c & 128) { + base_offset += 1; + if (!base_offset || base_offset & ~(~0UL >> 7)) + die("offset value overflow for delta base object"); + pack = fill(1); + c = *pack; + use(1); + base_offset = (base_offset << 7) + (c & 127); + } + base_offset = obj_list[nr].offset - base_offset; + + delta_data = get_data(delta_size); + if (dry_run || !delta_data) { + free(delta_data); + return; + } + lo = 0; + hi = nr; + while (lo < hi) { + mid = (lo + hi)/2; + if (base_offset < obj_list[mid].offset) { + hi = mid; + } else if (base_offset > obj_list[mid].offset) { + lo = mid + 1; + } else { + hashcpy(base_sha1, obj_list[mid].sha1); + base_found = !is_null_sha1(base_sha1); + break; + } + } + if (!base_found) { + /* The delta base object is itself a delta that + has not been resolved yet. */ + hashcpy(obj_list[nr].sha1, null_sha1); + add_delta_to_list(nr, null_sha1, base_offset, delta_data, delta_size); + return; + } + } + + base = read_sha1_file(base_sha1, type, &base_size); + if (!base) { + error("failed to read delta-pack base object %s", + sha1_to_hex(base_sha1)); + if (!recover) + exit(1); + has_errors = 1; + return; + } + resolve_delta(nr, type, base, base_size, delta_data, delta_size); + free(base); +} + +static void unpack_one(unsigned nr, unsigned total) +{ + unsigned shift; + unsigned char *pack, c; + unsigned long size; + enum object_type type; + + obj_list[nr].offset = consumed_bytes; + + pack = fill(1); + c = *pack; + use(1); + type = (c >> 4) & 7; + size = (c & 15); + shift = 4; + while (c & 0x80) { + pack = fill(1); + c = *pack; + use(1); + size += (c & 0x7f) << shift; + shift += 7; + } + if (!quiet) { + static unsigned long last_sec; + static unsigned last_percent; + struct timeval now; + unsigned percentage = ((nr+1) * 100) / total; + + gettimeofday(&now, NULL); + if (percentage != last_percent || now.tv_sec != last_sec) { + last_sec = now.tv_sec; + last_percent = percentage; + fprintf(stderr, "%4u%% (%u/%u) done\r", + percentage, (nr+1), total); + } + } + switch (type) { + case OBJ_COMMIT: + case OBJ_TREE: + case OBJ_BLOB: + case OBJ_TAG: + unpack_non_delta_entry(type, size, nr); + return; + case OBJ_REF_DELTA: + case OBJ_OFS_DELTA: + unpack_delta_entry(type, size, nr); + return; + default: + error("bad object type %d", type); + has_errors = 1; + if (recover) + return; + exit(1); + } +} + +static void unpack_all(void) +{ + int i; + struct pack_header *hdr = fill(sizeof(struct pack_header)); + unsigned nr_objects = ntohl(hdr->hdr_entries); + + if (ntohl(hdr->hdr_signature) != PACK_SIGNATURE) + die("bad pack file"); + if (!pack_version_ok(hdr->hdr_version)) + die("unknown pack file version %d", ntohl(hdr->hdr_version)); + fprintf(stderr, "Unpacking %d objects\n", nr_objects); + + obj_list = xmalloc(nr_objects * sizeof(*obj_list)); + use(sizeof(struct pack_header)); + for (i = 0; i < nr_objects; i++) + unpack_one(i, nr_objects); + if (delta_list) + die("unresolved deltas left after unpacking"); +} + +int cmd_unpack_objects(int argc, const char **argv, const char *prefix) +{ + int i; + unsigned char sha1[20]; + + git_config(git_default_config); + + quiet = !isatty(2); + + for (i = 1 ; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg == '-') { + if (!strcmp(arg, "-n")) { + dry_run = 1; + continue; + } + if (!strcmp(arg, "-q")) { + quiet = 1; + continue; + } + if (!strcmp(arg, "-r")) { + recover = 1; + continue; + } + if (!strncmp(arg, "--pack_header=", 14)) { + struct pack_header *hdr; + char *c; + + hdr = (struct pack_header *)buffer; + hdr->hdr_signature = htonl(PACK_SIGNATURE); + hdr->hdr_version = htonl(strtoul(arg + 14, &c, 10)); + if (*c != ',') + die("bad %s", arg); + hdr->hdr_entries = htonl(strtoul(c + 1, &c, 10)); + if (*c) + die("bad %s", arg); + len = sizeof(*hdr); + continue; + } + usage(unpack_usage); + } + + /* We don't take any non-flag arguments now.. Maybe some day */ + usage(unpack_usage); + } + SHA1_Init(&ctx); + unpack_all(); + SHA1_Update(&ctx, buffer, offset); + SHA1_Final(sha1, &ctx); + if (hashcmp(fill(20), sha1)) + die("final sha1 did not match"); + use(20); + + /* Write the last part of the buffer to stdout */ + while (len) { + int ret = xwrite(1, buffer + offset, len); + if (ret <= 0) + break; + len -= ret; + offset += ret; + } + + /* All done */ + if (!quiet) + fprintf(stderr, "\n"); + return has_errors; +} diff --git a/builtin-update-index.c b/builtin-update-index.c new file mode 100644 index 0000000000..1ac613a788 --- /dev/null +++ b/builtin-update-index.c @@ -0,0 +1,661 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "strbuf.h" +#include "quote.h" +#include "cache-tree.h" +#include "tree-walk.h" +#include "builtin.h" + +/* + * Default to not allowing changes to the list of files. The + * tool doesn't actually care, but this makes it harder to add + * files to the revision control by mistake by doing something + * like "git-update-index *" and suddenly having all the object + * files be revision controlled. + */ +static int allow_add; +static int allow_remove; +static int allow_replace; +static int info_only; +static int force_remove; +static int verbose; +static int mark_valid_only; +#define MARK_VALID 1 +#define UNMARK_VALID 2 + +static void report(const char *fmt, ...) +{ + va_list vp; + + if (!verbose) + return; + + va_start(vp, fmt); + vprintf(fmt, vp); + putchar('\n'); + va_end(vp); +} + +static int mark_valid(const char *path) +{ + int namelen = strlen(path); + int pos = cache_name_pos(path, namelen); + if (0 <= pos) { + switch (mark_valid_only) { + case MARK_VALID: + active_cache[pos]->ce_flags |= htons(CE_VALID); + break; + case UNMARK_VALID: + active_cache[pos]->ce_flags &= ~htons(CE_VALID); + break; + } + cache_tree_invalidate_path(active_cache_tree, path); + active_cache_changed = 1; + return 0; + } + return -1; +} + +static int add_file_to_cache(const char *path) +{ + int size, namelen, option, status; + struct cache_entry *ce; + 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 + * being able to add "path/file". However, + * "git-update-index --remove path" would not work. + * --force-remove can be used but this is more user + * friendly, especially since we can do the opposite + * case just fine without --force-remove. + */ + if (status == 0 || (errno == ENOENT || errno == ENOTDIR)) { + if (allow_remove) { + if (remove_file_from_cache(path)) + return error("%s: cannot remove from the index", + path); + else + return 0; + } else if (status < 0) { + return error("%s: does not exist and --remove not passed", + path); + } + } + if (0 == status) + return error("%s: is a directory - add files inside instead", + path); + else + return error("lstat(\"%s\"): %s", path, + strerror(errno)); + } + + namelen = strlen(path); + size = cache_entry_size(namelen); + ce = xcalloc(1, size); + memcpy(ce->name, path, namelen); + ce->ce_flags = htons(namelen); + fill_stat_cache_info(ce, &st); + + ce->ce_mode = create_ce_mode(st.st_mode); + if (!trust_executable_bit) { + /* If there is an existing entry, pick the mode bits + * from it, otherwise assume unexecutable. + */ + int pos = cache_name_pos(path, namelen); + if (0 <= pos) + ce->ce_mode = active_cache[pos]->ce_mode; + else if (S_ISREG(st.st_mode)) + ce->ce_mode = create_ce_mode(S_IFREG | 0666); + } + + if (index_path(ce->sha1, path, &st, !info_only)) + return -1; + option = allow_add ? ADD_CACHE_OK_TO_ADD : 0; + option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0; + if (add_cache_entry(ce, option)) + return error("%s: cannot add to the index - missing --add option?", + path); + return 0; +} + +static int add_cacheinfo(unsigned int mode, const unsigned char *sha1, + const char *path, int stage) +{ + int size, len, option; + struct cache_entry *ce; + + if (!verify_path(path)) + return -1; + + len = strlen(path); + size = cache_entry_size(len); + ce = xcalloc(1, size); + + hashcpy(ce->sha1, sha1); + memcpy(ce->name, path, len); + ce->ce_flags = create_ce_flags(len, stage); + ce->ce_mode = create_ce_mode(mode); + if (assume_unchanged) + ce->ce_flags |= htons(CE_VALID); + option = allow_add ? ADD_CACHE_OK_TO_ADD : 0; + option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0; + if (add_cache_entry(ce, option)) + 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; +} + +static void chmod_path(int flip, const char *path) +{ + int pos; + struct cache_entry *ce; + unsigned int mode; + + pos = cache_name_pos(path, strlen(path)); + if (pos < 0) + goto fail; + ce = active_cache[pos]; + mode = ntohl(ce->ce_mode); + if (!S_ISREG(mode)) + goto fail; + switch (flip) { + case '+': + ce->ce_mode |= htonl(0111); break; + case '-': + ce->ce_mode &= htonl(~0111); break; + default: + goto fail; + } + cache_tree_invalidate_path(active_cache_tree, path); + active_cache_changed = 1; + report("chmod %cx '%s'", flip, path); + return; + fail: + die("git-update-index: cannot chmod %cx '%s'", flip, path); +} + +static void update_one(const char *path, const char *prefix, int prefix_length) +{ + const char *p = prefix_path(prefix, prefix_length, path); + if (!verify_path(p)) { + fprintf(stderr, "Ignoring path %s\n", path); + goto free_return; + } + if (mark_valid_only) { + if (mark_valid(p)) + 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)) + die("git-update-index: unable to remove %s", path); + report("remove '%s'", path); + goto free_return; + } + if (add_file_to_cache(p)) + die("Unable to process file %s", path); + report("add '%s'", path); + free_return: + if (p < path || p > path + strlen(path)) + free((char*)p); +} + +static void read_index_info(int line_termination) +{ + struct strbuf buf; + strbuf_init(&buf); + while (1) { + char *ptr, *tab; + char *path_name; + unsigned char sha1[20]; + unsigned int mode; + int stage; + + /* This reads lines formatted in one of three formats: + * + * (1) mode SP sha1 TAB path + * The first format is what "git-apply --index-info" + * reports, and used to reconstruct a partial tree + * that is used for phony merge base tree when falling + * back on 3-way merge. + * + * (2) mode SP type SP sha1 TAB path + * The second format is to stuff git-ls-tree output + * into the index file. + * + * (3) mode SP sha1 SP stage TAB path + * This format is to put higher order stages into the + * index file and matches git-ls-files --stage output. + */ + read_line(&buf, stdin, line_termination); + if (buf.eof) + break; + + mode = strtoul(buf.buf, &ptr, 8); + if (ptr == buf.buf || *ptr != ' ') + goto bad_line; + + tab = strchr(ptr, '\t'); + if (!tab || tab - ptr < 41) + goto bad_line; + + if (tab[-2] == ' ' && '0' <= tab[-1] && tab[-1] <= '3') { + stage = tab[-1] - '0'; + ptr = tab + 1; /* point at the head of path */ + tab = tab - 2; /* point at tail of sha1 */ + } + else { + stage = 0; + ptr = tab + 1; /* point at the head of path */ + } + + if (get_sha1_hex(tab - 40, sha1) || tab[-41] != ' ') + goto bad_line; + + if (line_termination && ptr[0] == '"') + path_name = unquote_c_style(ptr, NULL); + else + path_name = ptr; + + if (!verify_path(path_name)) { + fprintf(stderr, "Ignoring path %s\n", path_name); + if (path_name != ptr) + free(path_name); + continue; + } + cache_tree_invalidate_path(active_cache_tree, path_name); + + if (!mode) { + /* mode == 0 means there is no such path -- remove */ + if (remove_file_from_cache(path_name)) + die("git-update-index: unable to remove %s", + ptr); + } + else { + /* mode ' ' sha1 '\t' name + * ptr[-1] points at tab, + * ptr[-41] is at the beginning of sha1 + */ + ptr[-42] = ptr[-1] = 0; + if (add_cacheinfo(mode, sha1, path_name, stage)) + die("git-update-index: unable to update %s", + path_name); + } + if (path_name != ptr) + free(path_name); + continue; + + bad_line: + die("malformed index info %s", buf.buf); + } +} + +static const char update_index_usage[] = +"git-update-index [-q] [--add] [--replace] [--remove] [--unmerged] [--refresh] [--really-refresh] [--cacheinfo] [--chmod=(+|-)x] [--assume-unchanged] [--info-only] [--force-remove] [--stdin] [--index-info] [--unresolve] [--again | -g] [--ignore-missing] [-z] [--verbose] [--] <file>..."; + +static unsigned char head_sha1[20]; +static unsigned char merge_head_sha1[20]; + +static struct cache_entry *read_one_ent(const char *which, + unsigned char *ent, const char *path, + int namelen, int stage) +{ + unsigned mode; + unsigned char sha1[20]; + int size; + struct cache_entry *ce; + + if (get_tree_entry(ent, path, sha1, &mode)) { + if (which) + error("%s: not in %s branch.", path, which); + return NULL; + } + if (mode == S_IFDIR) { + if (which) + error("%s: not a blob in %s branch.", path, which); + return NULL; + } + size = cache_entry_size(namelen); + ce = xcalloc(1, size); + + hashcpy(ce->sha1, sha1); + memcpy(ce->name, path, namelen); + ce->ce_flags = create_ce_flags(namelen, stage); + ce->ce_mode = create_ce_mode(mode); + return ce; +} + +static int unresolve_one(const char *path) +{ + int namelen = strlen(path); + int pos; + int ret = 0; + struct cache_entry *ce_2 = NULL, *ce_3 = NULL; + + /* See if there is such entry in the index. */ + pos = cache_name_pos(path, namelen); + if (pos < 0) { + /* If there isn't, either it is unmerged, or + * resolved as "removed" by mistake. We do not + * want to do anything in the former case. + */ + pos = -pos-1; + if (pos < active_nr) { + struct cache_entry *ce = active_cache[pos]; + if (ce_namelen(ce) == namelen && + !memcmp(ce->name, path, namelen)) { + fprintf(stderr, + "%s: skipping still unmerged path.\n", + path); + goto free_return; + } + } + } + + /* Grab blobs from given path from HEAD and MERGE_HEAD, + * stuff HEAD version in stage #2, + * stuff MERGE_HEAD version in stage #3. + */ + ce_2 = read_one_ent("our", head_sha1, path, namelen, 2); + ce_3 = read_one_ent("their", merge_head_sha1, path, namelen, 3); + + if (!ce_2 || !ce_3) { + ret = -1; + goto free_return; + } + if (!hashcmp(ce_2->sha1, ce_3->sha1) && + ce_2->ce_mode == ce_3->ce_mode) { + fprintf(stderr, "%s: identical in both, skipping.\n", + path); + 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); + ret = -1; + goto free_return; + } + if (!add_cache_entry(ce_3, ADD_CACHE_OK_TO_ADD)) + return 0; + error("%s: cannot add their version to the index.", path); + ret = -1; + free_return: + free(ce_2); + free(ce_3); + return ret; +} + +static void read_head_pointers(void) +{ + if (read_ref("HEAD", head_sha1)) + die("No HEAD -- no initial commit yet?\n"); + if (read_ref("MERGE_HEAD", merge_head_sha1)) { + fprintf(stderr, "Not in the middle of a merge.\n"); + exit(0); + } +} + +static int do_unresolve(int ac, const char **av, + const char *prefix, int prefix_length) +{ + int i; + int err = 0; + + /* Read HEAD and MERGE_HEAD; if MERGE_HEAD does not exist, we + * are not doing a merge, so exit with success status. + */ + read_head_pointers(); + + for (i = 1; i < ac; i++) { + const char *arg = av[i]; + const char *p = prefix_path(prefix, prefix_length, arg); + err |= unresolve_one(p); + if (p < arg || p > arg + strlen(arg)) + free((char*)p); + } + return err; +} + +static int do_reupdate(int ac, const char **av, + const char *prefix, int prefix_length) +{ + /* Read HEAD and run update-index on paths that are + * merged and already different between index and HEAD. + */ + int pos; + int has_head = 1; + const char **pathspec = get_pathspec(prefix, av + 1); + + if (read_ref("HEAD", head_sha1)) + /* If there is no HEAD, that means it is an initial + * commit. Update everything in the index. + */ + has_head = 0; + redo: + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + struct cache_entry *old = NULL; + int save_nr; + + if (ce_stage(ce) || !ce_path_match(ce, pathspec)) + continue; + if (has_head) + old = read_one_ent(NULL, head_sha1, + ce->name, ce_namelen(ce), 0); + if (old && ce->ce_mode == old->ce_mode && + !hashcmp(ce->sha1, old->sha1)) { + free(old); + continue; /* unchanged */ + } + /* Be careful. The working tree may not have the + * path anymore, in which case, under 'allow_remove', + * or worse yet 'allow_replace', active_nr may decrease. + */ + save_nr = active_nr; + update_one(ce->name + prefix_length, prefix, prefix_length); + if (save_nr != active_nr) + goto redo; + } + return 0; +} + +int cmd_update_index(int argc, const char **argv, const char *prefix) +{ + int i, newfd, entries, has_errors = 0, line_termination = '\n'; + int allow_options = 1; + int read_from_stdin = 0; + int prefix_length = prefix ? strlen(prefix) : 0; + char set_executable_bit = 0; + unsigned int refresh_flags = 0; + struct lock_file *lock_file; + + git_config(git_default_config); + + /* We can't free this memory, it becomes part of a linked list parsed atexit() */ + lock_file = xcalloc(1, sizeof(struct lock_file)); + + newfd = hold_lock_file_for_update(lock_file, get_index_file(), 1); + + entries = read_cache(); + if (entries < 0) + die("cache corrupted"); + + for (i = 1 ; i < argc; i++) { + const char *path = argv[i]; + const char *p; + + if (allow_options && *path == '-') { + if (!strcmp(path, "--")) { + allow_options = 0; + continue; + } + if (!strcmp(path, "-q")) { + refresh_flags |= REFRESH_QUIET; + continue; + } + if (!strcmp(path, "--add")) { + allow_add = 1; + continue; + } + if (!strcmp(path, "--replace")) { + allow_replace = 1; + continue; + } + if (!strcmp(path, "--remove")) { + allow_remove = 1; + continue; + } + if (!strcmp(path, "--unmerged")) { + refresh_flags |= REFRESH_UNMERGED; + continue; + } + if (!strcmp(path, "--refresh")) { + has_errors |= refresh_cache(refresh_flags); + continue; + } + if (!strcmp(path, "--really-refresh")) { + has_errors |= refresh_cache(REFRESH_REALLY | refresh_flags); + continue; + } + if (!strcmp(path, "--cacheinfo")) { + unsigned char sha1[20]; + unsigned int mode; + + if (i+3 >= argc) + die("git-update-index: --cacheinfo <mode> <sha1> <path>"); + + if ((sscanf(argv[i+1], "%o", &mode) != 1) || + get_sha1_hex(argv[i+2], sha1) || + add_cacheinfo(mode, sha1, argv[i+3], 0)) + die("git-update-index: --cacheinfo" + " cannot add %s", argv[i+3]); + i += 3; + continue; + } + if (!strcmp(path, "--chmod=-x") || + !strcmp(path, "--chmod=+x")) { + if (argc <= i+1) + die("git-update-index: %s <path>", path); + set_executable_bit = path[8]; + continue; + } + if (!strcmp(path, "--assume-unchanged")) { + mark_valid_only = MARK_VALID; + continue; + } + if (!strcmp(path, "--no-assume-unchanged")) { + mark_valid_only = UNMARK_VALID; + continue; + } + if (!strcmp(path, "--info-only")) { + info_only = 1; + continue; + } + if (!strcmp(path, "--force-remove")) { + force_remove = 1; + continue; + } + if (!strcmp(path, "-z")) { + line_termination = 0; + continue; + } + if (!strcmp(path, "--stdin")) { + if (i != argc - 1) + die("--stdin must be at the end"); + read_from_stdin = 1; + break; + } + if (!strcmp(path, "--index-info")) { + if (i != argc - 1) + die("--index-info must be at the end"); + allow_add = allow_replace = allow_remove = 1; + read_index_info(line_termination); + break; + } + if (!strcmp(path, "--unresolve")) { + has_errors = do_unresolve(argc - i, argv + i, + prefix, prefix_length); + if (has_errors) + active_cache_changed = 0; + goto finish; + } + if (!strcmp(path, "--again") || !strcmp(path, "-g")) { + has_errors = do_reupdate(argc - i, argv + i, + prefix, prefix_length); + if (has_errors) + active_cache_changed = 0; + goto finish; + } + if (!strcmp(path, "--ignore-missing")) { + refresh_flags |= REFRESH_IGNORE_MISSING; + continue; + } + if (!strcmp(path, "--verbose")) { + verbose = 1; + continue; + } + if (!strcmp(path, "-h") || !strcmp(path, "--help")) + usage(update_index_usage); + die("unknown option %s", path); + } + p = prefix_path(prefix, prefix_length, path); + update_one(p, NULL, 0); + if (set_executable_bit) + chmod_path(set_executable_bit, p); + if (p < path || p > path + strlen(path)) + free((char*)p); + } + if (read_from_stdin) { + struct strbuf buf; + strbuf_init(&buf); + while (1) { + char *path_name; + const char *p; + read_line(&buf, stdin, line_termination); + if (buf.eof) + break; + if (line_termination && buf.buf[0] == '"') + path_name = unquote_c_style(buf.buf, NULL); + else + path_name = buf.buf; + p = prefix_path(prefix, prefix_length, path_name); + update_one(p, NULL, 0); + if (set_executable_bit) + chmod_path(set_executable_bit, p); + if (p < path_name || p > path_name + strlen(path_name)) + free((char*) p); + if (path_name != buf.buf) + free(path_name); + } + } + + finish: + if (active_cache_changed) { + if (write_cache(newfd, active_cache, active_nr) || + close(newfd) || commit_lock_file(lock_file)) + die("Unable to write new index file"); + } + + rollback_lock_file(lock_file); + + return has_errors ? 1 : 0; +} diff --git a/builtin-update-ref.c b/builtin-update-ref.c new file mode 100644 index 0000000000..5ee960bf41 --- /dev/null +++ b/builtin-update-ref.c @@ -0,0 +1,68 @@ +#include "cache.h" +#include "refs.h" +#include "builtin.h" + +static const char git_update_ref_usage[] = +"git-update-ref [-m <reason>] (-d <refname> <value> | <refname> <value> [<oldval>])"; + +int cmd_update_ref(int argc, const char **argv, const char *prefix) +{ + const char *refname=NULL, *value=NULL, *oldval=NULL, *msg=NULL; + struct ref_lock *lock; + unsigned char sha1[20], oldsha1[20]; + int i, delete; + + delete = 0; + git_config(git_default_config); + + 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 (!strcmp("-d", argv[i])) { + delete = 1; + 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); + + if (get_sha1(value, sha1)) + die("%s: not a valid SHA1", value); + + if (delete) { + if (oldval) + usage(git_update_ref_usage); + return delete_ref(refname, sha1); + } + + hashclr(oldsha1); + if (oldval && *oldval && get_sha1(oldval, oldsha1)) + die("%s: not a valid old SHA1", oldval); + + lock = lock_any_ref_for_update(refname, oldval ? oldsha1 : NULL); + if (!lock) + die("%s: cannot lock the ref", refname); + if (write_ref_sha1(lock, sha1, msg) < 0) + die("%s: cannot update the ref", refname); + return 0; +} diff --git a/builtin-upload-archive.c b/builtin-upload-archive.c new file mode 100644 index 0000000000..48ae09e9b5 --- /dev/null +++ b/builtin-upload-archive.c @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2006 Franck Bui-Huu + */ +#include "cache.h" +#include "builtin.h" +#include "archive.h" +#include "pkt-line.h" +#include "sideband.h" + +static const char upload_archive_usage[] = + "git-upload-archive <repo>"; + +static const char deadchild[] = +"git-upload-archive: archiver died with error"; + +static const char lostchild[] = +"git-upload-archive: archiver process was lost"; + + +static int run_upload_archive(int argc, const char **argv, const char *prefix) +{ + struct archiver ar; + const char *sent_argv[MAX_ARGS]; + const char *arg_cmd = "argument "; + char *p, buf[4096]; + int treeish_idx; + int sent_argc; + int len; + + if (argc != 2) + usage(upload_archive_usage); + + if (strlen(argv[1]) > sizeof(buf)) + die("insanely long repository name"); + + strcpy(buf, argv[1]); /* enter-repo smudges its argument */ + + if (!enter_repo(buf, 0)) + die("not a git archive"); + + /* put received options in sent_argv[] */ + sent_argc = 1; + sent_argv[0] = "git-upload-archive"; + for (p = buf;;) { + /* This will die if not enough free space in buf */ + len = packet_read_line(0, p, (buf + sizeof buf) - p); + if (len == 0) + break; /* got a flush */ + if (sent_argc > MAX_ARGS - 2) + die("Too many options (>29)"); + + if (p[len-1] == '\n') { + p[--len] = 0; + } + if (len < strlen(arg_cmd) || + strncmp(arg_cmd, p, strlen(arg_cmd))) + die("'argument' token or flush expected"); + + len -= strlen(arg_cmd); + memmove(p, p + strlen(arg_cmd), len); + sent_argv[sent_argc++] = p; + p += len; + *p++ = 0; + } + sent_argv[sent_argc] = NULL; + + /* parse all options sent by the client */ + treeish_idx = parse_archive_args(sent_argc, sent_argv, &ar); + + parse_treeish_arg(sent_argv + treeish_idx, &ar.args, prefix); + parse_pathspec_arg(sent_argv + treeish_idx + 1, &ar.args); + + return ar.write_archive(&ar.args); +} + +static void error_clnt(const char *fmt, ...) +{ + char buf[1024]; + va_list params; + int len; + + va_start(params, fmt); + len = vsprintf(buf, fmt, params); + va_end(params); + send_sideband(1, 3, buf, len, LARGE_PACKET_MAX); + die("sent error to the client: %s", buf); +} + +static void process_input(int child_fd, int band) +{ + char buf[16384]; + ssize_t sz = read(child_fd, buf, sizeof(buf)); + if (sz < 0) { + if (errno != EAGAIN && errno != EINTR) + error_clnt("read error: %s\n", strerror(errno)); + return; + } + send_sideband(1, band, buf, sz, LARGE_PACKET_MAX); +} + +int cmd_upload_archive(int argc, const char **argv, const char *prefix) +{ + pid_t writer; + int fd1[2], fd2[2]; + /* + * Set up sideband subprocess. + * + * We (parent) monitor and read from child, sending its fd#1 and fd#2 + * multiplexed out to our fd#1. If the child dies, we tell the other + * end over channel #3. + */ + if (pipe(fd1) < 0 || pipe(fd2) < 0) { + int err = errno; + packet_write(1, "NACK pipe failed on the remote side\n"); + die("upload-archive: %s", strerror(err)); + } + writer = fork(); + if (writer < 0) { + int err = errno; + packet_write(1, "NACK fork failed on the remote side\n"); + die("upload-archive: %s", strerror(err)); + } + if (!writer) { + /* child - connect fd#1 and fd#2 to the pipe */ + dup2(fd1[1], 1); + dup2(fd2[1], 2); + close(fd1[1]); close(fd2[1]); + close(fd1[0]); close(fd2[0]); /* we do not read from pipe */ + + exit(run_upload_archive(argc, argv, prefix)); + } + + /* parent - read from child, multiplex and send out to fd#1 */ + close(fd1[1]); close(fd2[1]); /* we do not write to pipe */ + packet_write(1, "ACK\n"); + packet_flush(1); + + while (1) { + struct pollfd pfd[2]; + int status; + + pfd[0].fd = fd1[0]; + pfd[0].events = POLLIN; + pfd[1].fd = fd2[0]; + pfd[1].events = POLLIN; + if (poll(pfd, 2, -1) < 0) { + if (errno != EINTR) { + error("poll failed resuming: %s", + strerror(errno)); + sleep(1); + } + continue; + } + if (pfd[0].revents & POLLIN) + /* Data stream ready */ + process_input(pfd[0].fd, 1); + if (pfd[1].revents & POLLIN) + /* Status stream ready */ + process_input(pfd[1].fd, 2); + /* Always finish to read data when available */ + if ((pfd[0].revents | pfd[1].revents) & POLLIN) + continue; + + if (waitpid(writer, &status, 0) < 0) + error_clnt("%s", lostchild); + else if (!WIFEXITED(status) || WEXITSTATUS(status) > 0) + error_clnt("%s", deadchild); + packet_flush(1); + break; + } + return 0; +} diff --git a/builtin-verify-pack.c b/builtin-verify-pack.c new file mode 100644 index 0000000000..4e31c273f4 --- /dev/null +++ b/builtin-verify-pack.c @@ -0,0 +1,80 @@ +#include "builtin.h" +#include "cache.h" +#include "pack.h" + +static int verify_one_pack(const char *path, int verbose) +{ + char arg[PATH_MAX]; + int len; + struct packed_git *pack; + int err; + + len = strlcpy(arg, path, PATH_MAX); + if (len >= PATH_MAX) + return error("name too long: %s", path); + + /* + * In addition to "foo.idx" we accept "foo.pack" and "foo"; + * normalize these forms to "foo.idx" for add_packed_git(). + */ + if (has_extension(arg, ".pack")) { + strcpy(arg + len - 5, ".idx"); + len--; + } else if (!has_extension(arg, ".idx")) { + if (len + 4 >= PATH_MAX) + return error("name too long: %s.idx", arg); + strcpy(arg + len, ".idx"); + len += 4; + } + + /* + * add_packed_git() uses our buffer (containing "foo.idx") to + * build the pack filename ("foo.pack"). Make sure it fits. + */ + if (len + 1 >= PATH_MAX) { + arg[len - 4] = '\0'; + return error("name too long: %s.pack", arg); + } + + pack = add_packed_git(arg, len, 1); + if (!pack) + return error("packfile %s not found.", arg); + + err = verify_pack(pack, verbose); + free(pack); + + return err; +} + +static const char verify_pack_usage[] = "git-verify-pack [-v] <pack>..."; + +int cmd_verify_pack(int argc, const char **argv, const char *prefix) +{ + int err = 0; + int verbose = 0; + int no_more_options = 0; + int nothing_done = 1; + + git_config(git_default_config); + while (1 < argc) { + if (!no_more_options && argv[1][0] == '-') { + if (!strcmp("-v", argv[1])) + verbose = 1; + else if (!strcmp("--", argv[1])) + no_more_options = 1; + else + usage(verify_pack_usage); + } + else { + if (verify_one_pack(argv[1], verbose)) + err = 1; + nothing_done = 0; + } + argc--; argv++; + } + + if (nothing_done) + usage(verify_pack_usage); + + return err; +} diff --git a/builtin-write-tree.c b/builtin-write-tree.c new file mode 100644 index 0000000000..50670dc7bf --- /dev/null +++ b/builtin-write-tree.c @@ -0,0 +1,87 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "builtin.h" +#include "cache.h" +#include "tree.h" +#include "cache-tree.h" + +static const char write_tree_usage[] = +"git-write-tree [--missing-ok] [--prefix=<prefix>/]"; + +int write_tree(unsigned char *sha1, int missing_ok, const char *prefix) +{ + int entries, was_valid, newfd; + + /* We can't free this memory, it becomes part of a linked list parsed atexit() */ + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + newfd = hold_lock_file_for_update(lock_file, get_index_file(), 0); + + entries = read_cache(); + if (entries < 0) + die("git-write-tree: error reading cache"); + + 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) + && !close(newfd)) + commit_lock_file(lock_file); + } + /* 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. + */ + } + + if (prefix) { + struct cache_tree *subtree = + cache_tree_find(active_cache_tree, prefix); + hashcpy(sha1, subtree->sha1); + } + else + hashcpy(sha1, active_cache_tree->sha1); + + rollback_lock_file(lock_file); + + return 0; +} + +int cmd_write_tree(int argc, const char **argv, const char *unused_prefix) +{ + int missing_ok = 0, ret; + const char *prefix = NULL; + unsigned char sha1[20]; + + while (1 < argc) { + const char *arg = argv[1]; + if (!strcmp(arg, "--missing-ok")) + missing_ok = 1; + else if (!strncmp(arg, "--prefix=", 9)) + prefix = arg + 9; + else + usage(write_tree_usage); + argc--; argv++; + } + + if (argc > 2) + die("too many options"); + + ret = write_tree(sha1, missing_ok, prefix); + printf("%s\n", sha1_to_hex(sha1)); + + return ret; +} diff --git a/builtin.h b/builtin.h new file mode 100644 index 0000000000..5108fd2d74 --- /dev/null +++ b/builtin.h @@ -0,0 +1,82 @@ +#ifndef BUILTIN_H +#define BUILTIN_H + +#include "git-compat-util.h" + +extern const char git_version_string[]; +extern const char git_usage_string[]; + +extern void help_unknown_cmd(const char *cmd); +extern int mailinfo(FILE *in, FILE *out, int ks, const char *encoding, const char *msg, const char *patch); +extern int split_mbox(const char **mbox, const char *dir, int allow_bare, int nr_prec, int skip); +extern void stripspace(FILE *in, FILE *out); +extern int write_tree(unsigned char *sha1, int missing_ok, const char *prefix); +extern void prune_packed_objects(int); + +extern int cmd_add(int argc, const char **argv, const char *prefix); +extern int cmd_annotate(int argc, const char **argv, const char *prefix); +extern int cmd_apply(int argc, const char **argv, const char *prefix); +extern int cmd_archive(int argc, const char **argv, const char *prefix); +extern int cmd_blame(int argc, const char **argv, const char *prefix); +extern int cmd_branch(int argc, const char **argv, const char *prefix); +extern int cmd_cat_file(int argc, const char **argv, const char *prefix); +extern int cmd_checkout_index(int argc, const char **argv, const char *prefix); +extern int cmd_check_ref_format(int argc, const char **argv, const char *prefix); +extern int cmd_cherry(int argc, const char **argv, const char *prefix); +extern int cmd_commit_tree(int argc, const char **argv, const char *prefix); +extern int cmd_count_objects(int argc, const char **argv, const char *prefix); +extern int cmd_describe(int argc, const char **argv, const char *prefix); +extern int cmd_diff_files(int argc, const char **argv, const char *prefix); +extern int cmd_diff_index(int argc, const char **argv, const char *prefix); +extern int cmd_diff(int argc, const char **argv, const char *prefix); +extern int cmd_diff_stages(int argc, const char **argv, const char *prefix); +extern int cmd_diff_tree(int argc, const char **argv, const char *prefix); +extern int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix); +extern int cmd_for_each_ref(int argc, const char **argv, const char *prefix); +extern int cmd_format_patch(int argc, const char **argv, const char *prefix); +extern int cmd_fsck(int argc, const char **argv, const char *prefix); +extern int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix); +extern int cmd_grep(int argc, const char **argv, const char *prefix); +extern int cmd_help(int argc, const char **argv, const char *prefix); +extern int cmd_init_db(int argc, const char **argv, const char *prefix); +extern int cmd_log(int argc, const char **argv, const char *prefix); +extern int cmd_log_reflog(int argc, const char **argv, const char *prefix); +extern int cmd_ls_files(int argc, const char **argv, const char *prefix); +extern int cmd_ls_tree(int argc, const char **argv, const char *prefix); +extern int cmd_mailinfo(int argc, const char **argv, const char *prefix); +extern int cmd_mailsplit(int argc, const char **argv, const char *prefix); +extern int cmd_merge_file(int argc, const char **argv, const char *prefix); +extern int cmd_mv(int argc, const char **argv, const char *prefix); +extern int cmd_name_rev(int argc, const char **argv, const char *prefix); +extern int cmd_pack_objects(int argc, const char **argv, const char *prefix); +extern int cmd_pickaxe(int argc, const char **argv, const char *prefix); +extern int cmd_prune(int argc, const char **argv, const char *prefix); +extern int cmd_prune_packed(int argc, const char **argv, const char *prefix); +extern int cmd_push(int argc, const char **argv, const char *prefix); +extern int cmd_read_tree(int argc, const char **argv, const char *prefix); +extern int cmd_reflog(int argc, const char **argv, const char *prefix); +extern int cmd_config(int argc, const char **argv, const char *prefix); +extern int cmd_rerere(int argc, const char **argv, const char *prefix); +extern int cmd_rev_list(int argc, const char **argv, const char *prefix); +extern int cmd_rev_parse(int argc, const char **argv, const char *prefix); +extern int cmd_rm(int argc, const char **argv, const char *prefix); +extern int cmd_runstatus(int argc, const char **argv, const char *prefix); +extern int cmd_shortlog(int argc, const char **argv, const char *prefix); +extern int cmd_show(int argc, const char **argv, const char *prefix); +extern int cmd_show_branch(int argc, const char **argv, const char *prefix); +extern int cmd_stripspace(int argc, const char **argv, const char *prefix); +extern int cmd_symbolic_ref(int argc, const char **argv, const char *prefix); +extern int cmd_tar_tree(int argc, const char **argv, const char *prefix); +extern int cmd_unpack_objects(int argc, const char **argv, const char *prefix); +extern int cmd_update_index(int argc, const char **argv, const char *prefix); +extern int cmd_update_ref(int argc, const char **argv, const char *prefix); +extern int cmd_upload_archive(int argc, const char **argv, const char *prefix); +extern int cmd_upload_tar(int argc, const char **argv, const char *prefix); +extern int cmd_version(int argc, const char **argv, const char *prefix); +extern int cmd_whatchanged(int argc, const char **argv, const char *prefix); +extern int cmd_write_tree(int argc, const char **argv, const char *prefix); +extern int cmd_verify_pack(int argc, const char **argv, const char *prefix); +extern int cmd_show_ref(int argc, const char **argv, const char *prefix); +extern int cmd_pack_refs(int argc, const char **argv, const char *prefix); + +#endif diff --git a/cache-tree.c b/cache-tree.c new file mode 100644 index 0000000000..9b73c8669a --- /dev/null +++ b/cache-tree.c @@ -0,0 +1,557 @@ +#include "cache.h" +#include "tree.h" +#include "cache-tree.h" + +#ifndef DEBUG +#define DEBUG 0 +#endif + +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); + if (subcnt < 0) + return subcnt; + 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; + hashcpy((unsigned char*)buffer + offset, sha1); + offset += 20; + +#if DEBUG + fprintf(stderr, "cache-tree update-one %o %.*s\n", + mode, entlen, path + baselen); +#endif + } + + if (dryrun) + hash_sha1_file(buffer, offset, tree_type, it->sha1); + 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) { + hashcpy((unsigned char*)buffer + *offset, it->sha1); + *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; + hashcpy(it->sha1, (unsigned char*)buf); + 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; +} diff --git a/cache-tree.h b/cache-tree.h new file mode 100644 index 0000000000..119407e3a1 --- /dev/null +++ b/cache-tree.h @@ -0,0 +1,33 @@ +#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 diff --git a/cache.h b/cache.h new file mode 100644 index 0000000000..c62b0b090d --- /dev/null +++ b/cache.h @@ -0,0 +1,471 @@ +#ifndef CACHE_H +#define CACHE_H + +#include "git-compat-util.h" + +#include SHA1_HEADER +#include <zlib.h> + +#if ZLIB_VERNUM < 0x1200 +#define deflateBound(c,s) ((s) + (((s) + 7) >> 3) + (((s) + 63) >> 6) + 11) +#endif + +#if defined(DT_UNKNOWN) && !defined(NO_D_TYPE_IN_DIRENT) +#define DTYPE(de) ((de)->d_type) +#else +#undef DT_UNKNOWN +#undef DT_DIR +#undef DT_REG +#undef DT_LNK +#define DT_UNKNOWN 0 +#define DT_DIR 1 +#define DT_REG 2 +#define DT_LNK 3 +#define DTYPE(de) DT_UNKNOWN +#endif + +/* + * Intensive research over the course of many years has shown that + * port 9418 is totally unused by anything else. Or + * + * Your search - "port 9418" - did not match any documents. + * + * as www.google.com puts it. + * + * This port has been properly assigned for git use by IANA: + * git (Assigned-9418) [I06-050728-0001]. + * + * git 9418/tcp git pack transfer service + * git 9418/udp git pack transfer service + * + * with Linus Torvalds <torvalds@osdl.org> as the point of + * contact. September 2005. + * + * See http://www.iana.org/assignments/port-numbers + */ +#define DEFAULT_GIT_PORT 9418 + +/* + * Basic data structures for the directory cache + */ + +#define CACHE_SIGNATURE 0x44495243 /* "DIRC" */ +struct cache_header { + unsigned int hdr_signature; + unsigned int hdr_version; + unsigned int hdr_entries; +}; + +/* + * The "cache_time" is just the low 32 bits of the + * time. It doesn't matter if it overflows - we only + * check it for equality in the 32 bits we save. + */ +struct cache_time { + unsigned int sec; + unsigned int nsec; +}; + +/* + * dev/ino/uid/gid/size are also just tracked to the low 32 bits + * Again - this is just a (very strong in practice) heuristic that + * the inode hasn't changed. + * + * We save the fields in big-endian order to allow using the + * index file over NFS transparently. + */ +struct cache_entry { + struct cache_time ce_ctime; + struct cache_time ce_mtime; + unsigned int ce_dev; + unsigned int ce_ino; + unsigned int ce_mode; + unsigned int ce_uid; + unsigned int ce_gid; + unsigned int ce_size; + unsigned char sha1[20]; + unsigned short ce_flags; + char name[FLEX_ARRAY]; /* more */ +}; + +#define CE_NAMEMASK (0x0fff) +#define CE_STAGEMASK (0x3000) +#define CE_UPDATE (0x4000) +#define CE_VALID (0x8000) +#define CE_STAGESHIFT 12 + +#define create_ce_flags(len, stage) htons((len) | ((stage) << CE_STAGESHIFT)) +#define ce_namelen(ce) (CE_NAMEMASK & ntohs((ce)->ce_flags)) +#define ce_size(ce) cache_entry_size(ce_namelen(ce)) +#define ce_stage(ce) ((CE_STAGEMASK & ntohs((ce)->ce_flags)) >> CE_STAGESHIFT) + +#define ce_permissions(mode) (((mode) & 0100) ? 0755 : 0644) +static inline unsigned int create_ce_mode(unsigned int mode) +{ + if (S_ISLNK(mode)) + return htonl(S_IFLNK); + return htonl(S_IFREG | ce_permissions(mode)); +} +#define canon_mode(mode) \ + (S_ISREG(mode) ? (S_IFREG | ce_permissions(mode)) : \ + S_ISLNK(mode) ? S_IFLNK : S_IFDIR) + +#define cache_entry_size(len) ((offsetof(struct cache_entry,name) + (len) + 8) & ~7) + +extern struct cache_entry **active_cache; +extern unsigned int active_nr, active_alloc, active_cache_changed; +extern struct cache_tree *active_cache_tree; +extern int cache_errno; + +#define GIT_DIR_ENVIRONMENT "GIT_DIR" +#define DEFAULT_GIT_DIR_ENVIRONMENT ".git" +#define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" +#define INDEX_ENVIRONMENT "GIT_INDEX_FILE" +#define GRAFT_ENVIRONMENT "GIT_GRAFT_FILE" +#define TEMPLATE_DIR_ENVIRONMENT "GIT_TEMPLATE_DIR" +#define CONFIG_ENVIRONMENT "GIT_CONFIG" +#define CONFIG_LOCAL_ENVIRONMENT "GIT_CONFIG_LOCAL" +#define EXEC_PATH_ENVIRONMENT "GIT_EXEC_PATH" + +extern int is_bare_repository_cfg; +extern int is_bare_repository(void); +extern int is_inside_git_dir(void); +extern const char *get_git_dir(void); +extern char *get_object_directory(void); +extern char *get_refs_directory(void); +extern char *get_index_file(void); +extern char *get_graft_file(void); + +#define ALTERNATE_DB_ENVIRONMENT "GIT_ALTERNATE_OBJECT_DIRECTORIES" + +extern const char **get_pathspec(const char *prefix, const char **pathspec); +extern const char *setup_git_directory_gently(int *); +extern const char *setup_git_directory(void); +extern const char *prefix_path(const char *prefix, int len, const char *path); +extern const char *prefix_filename(const char *prefix, int len, const char *path); +extern void verify_filename(const char *prefix, const char *name); +extern void verify_non_filename(const char *prefix, const char *name); + +#define alloc_nr(x) (((x)+16)*3/2) + +/* Initialize and use the cache information */ +extern int read_cache(void); +extern int read_cache_from(const char *path); +extern int write_cache(int newfd, struct cache_entry **cache, int entries); +extern int discard_cache(void); +extern int verify_path(const char *path); +extern int cache_name_pos(const char *name, int namelen); +#define ADD_CACHE_OK_TO_ADD 1 /* Ok to add */ +#define ADD_CACHE_OK_TO_REPLACE 2 /* Ok to replace file/directory */ +#define ADD_CACHE_SKIP_DFCHECK 4 /* Ok to skip DF conflict checks */ +extern int add_cache_entry(struct cache_entry *ce, int option); +extern struct cache_entry *refresh_cache_entry(struct cache_entry *ce, int really); +extern int remove_cache_entry_at(int pos); +extern int remove_file_from_cache(const char *path); +extern int add_file_to_index(const char *path, int verbose); +extern int ce_same_name(struct cache_entry *a, struct cache_entry *b); +extern int ce_match_stat(struct cache_entry *ce, struct stat *st, int); +extern int ce_modified(struct cache_entry *ce, struct stat *st, int); +extern int ce_path_match(const struct cache_entry *ce, const char **pathspec); +extern int index_fd(unsigned char *sha1, int fd, struct stat *st, int write_object, const char *type); +extern int read_pipe(int fd, char** return_buf, unsigned long* return_size); +extern int index_pipe(unsigned char *sha1, int fd, const char *type, int write_object); +extern int index_path(unsigned char *sha1, const char *path, struct stat *st, int write_object); +extern void fill_stat_cache_info(struct cache_entry *ce, struct stat *st); + +#define REFRESH_REALLY 0x0001 /* ignore_valid */ +#define REFRESH_UNMERGED 0x0002 /* allow unmerged */ +#define REFRESH_QUIET 0x0004 /* be quiet about it */ +#define REFRESH_IGNORE_MISSING 0x0008 /* ignore non-existent */ +extern int refresh_cache(unsigned int flags); + +struct lock_file { + struct lock_file *next; + char on_list; + char filename[PATH_MAX]; +}; +extern int hold_lock_file_for_update(struct lock_file *, const char *path, int); +extern int commit_lock_file(struct lock_file *); +extern void rollback_lock_file(struct lock_file *); +extern int delete_ref(const char *, unsigned char *sha1); + +/* Environment bits from configuration mechanism */ +extern int use_legacy_headers; +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 shared_repository; +extern const char *apply_default_whitespace; +extern int zlib_compression_level; +extern size_t packed_git_window_size; +extern size_t packed_git_limit; + +#define GIT_REPO_VERSION 0 +extern int repository_format_version; +extern int check_repository_format(void); + +#define MTIME_CHANGED 0x0001 +#define CTIME_CHANGED 0x0002 +#define OWNER_CHANGED 0x0004 +#define MODE_CHANGED 0x0008 +#define INODE_CHANGED 0x0010 +#define DATA_CHANGED 0x0020 +#define TYPE_CHANGED 0x0040 + +/* Return a statically allocated filename matching the sha1 signature */ +extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2))); +extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2))); +extern char *sha1_file_name(const unsigned char *sha1); +extern char *sha1_pack_name(const unsigned char *sha1); +extern char *sha1_pack_index_name(const unsigned char *sha1); +extern const char *find_unique_abbrev(const unsigned char *sha1, int); +extern const unsigned char null_sha1[20]; +static inline int is_null_sha1(const unsigned char *sha1) +{ + return !memcmp(sha1, null_sha1, 20); +} +static inline int hashcmp(const unsigned char *sha1, const unsigned char *sha2) +{ + return memcmp(sha1, sha2, 20); +} +static inline void hashcpy(unsigned char *sha_dst, const unsigned char *sha_src) +{ + memcpy(sha_dst, sha_src, 20); +} +static inline void hashclr(unsigned char *hash) +{ + memset(hash, 0, 20); +} + +int git_mkstemp(char *path, size_t n, const char *template); + +enum sharedrepo { + PERM_UMASK = 0, + PERM_GROUP, + PERM_EVERYBODY +}; +int git_config_perm(const char *var, const char *value); +int adjust_shared_perm(const char *path); +int safe_create_leading_directories(char *path); +char *enter_repo(char *path, int strict); + +/* Read and unpack a sha1 file into memory, write memory to a sha1 file */ +extern int sha1_object_info(const unsigned char *, char *, unsigned long *); +extern void * unpack_sha1_file(void *map, unsigned long mapsize, char *type, unsigned long *size); +extern void * read_sha1_file(const unsigned char *sha1, char *type, unsigned long *size); +extern int hash_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *sha1); +extern int write_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *return_sha1); +extern int pretend_sha1_file(void *, unsigned long, const char *, unsigned char *); + +extern int check_sha1_signature(const unsigned char *sha1, void *buf, unsigned long size, const char *type); + +extern int write_sha1_from_fd(const unsigned char *sha1, int fd, char *buffer, + size_t bufsize, size_t *bufposn); +extern int write_sha1_to_fd(int fd, const unsigned char *sha1); +extern int move_temp_to_file(const char *tmpfile, const char *filename); + +extern int has_sha1_pack(const unsigned char *sha1, const char **ignore); +extern int has_sha1_file(const unsigned char *sha1); +extern void *map_sha1_file(const unsigned char *sha1, unsigned long *); +extern int legacy_loose_object(unsigned char *); + +extern int has_pack_file(const unsigned char *sha1); +extern int has_pack_index(const unsigned char *sha1); + +enum object_type { + OBJ_NONE = 0, + OBJ_COMMIT = 1, + OBJ_TREE = 2, + OBJ_BLOB = 3, + OBJ_TAG = 4, + /* 5 for future expansion */ + OBJ_OFS_DELTA = 6, + OBJ_REF_DELTA = 7, + OBJ_BAD, +}; + +extern signed char hexval_table[256]; +static inline unsigned int hexval(unsigned int c) +{ + return hexval_table[c]; +} + +/* Convert to/from hex/sha1 representation */ +#define MINIMUM_ABBREV 4 +#define DEFAULT_ABBREV 7 + +extern int get_sha1(const char *str, unsigned char *sha1); +extern int get_sha1_hex(const char *hex, unsigned char *sha1); +extern char *sha1_to_hex(const unsigned char *sha1); /* static buffer result! */ +extern int read_ref(const char *filename, unsigned char *sha1); +extern const char *resolve_ref(const char *path, unsigned char *sha1, int, int *); +extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref); +extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref); + +extern int create_symref(const char *ref, const char *refs_heads_master, const char *logmsg); +extern int validate_headref(const char *ref); + +extern int base_name_compare(const char *name1, int len1, int mode1, const char *name2, int len2, int mode2); +extern int cache_name_compare(const char *name1, int len1, const char *name2, int len2); + +extern void *read_object_with_reference(const unsigned char *sha1, + const char *required_type, + unsigned long *size, + unsigned char *sha1_ret); + +const char *show_date(unsigned long time, int timezone, int relative); +const char *show_rfc2822_date(unsigned long time, int timezone); +int parse_date(const char *date, char *buf, int bufsize); +void datestamp(char *buf, int bufsize); +unsigned long approxidate(const char *); + +extern const char *git_author_info(int); +extern const char *git_committer_info(int); +extern const char *fmt_ident(const char *name, const char *email, const char *date_str, int); + +struct checkout { + const char *base_dir; + int base_dir_len; + unsigned force:1, + quiet:1, + not_new:1, + refresh_cache:1; +}; + +extern int checkout_entry(struct cache_entry *ce, struct checkout *state, char *topath); + +extern struct alternate_object_database { + struct alternate_object_database *next; + char *name; + char base[FLEX_ARRAY]; /* more */ +} *alt_odb_list; +extern void prepare_alt_odb(void); + +struct pack_window { + struct pack_window *next; + unsigned char *base; + off_t offset; + size_t len; + unsigned int last_used; + unsigned int inuse_cnt; +}; + +extern struct packed_git { + struct packed_git *next; + struct pack_window *windows; + uint32_t *index_base; + off_t index_size; + off_t pack_size; + int pack_fd; + int pack_local; + unsigned char sha1[20]; + /* something like ".git/objects/pack/xxxxx.pack" */ + char pack_name[FLEX_ARRAY]; /* more */ +} *packed_git; + +struct pack_entry { + unsigned int offset; + unsigned char sha1[20]; + struct packed_git *p; +}; + +struct ref { + struct ref *next; + unsigned char old_sha1[20]; + unsigned char new_sha1[20]; + unsigned char force; + struct ref *peer_ref; /* when renaming */ + char name[FLEX_ARRAY]; /* more */ +}; + +#define REF_NORMAL (1u << 0) +#define REF_HEADS (1u << 1) +#define REF_TAGS (1u << 2) + +extern pid_t git_connect(int fd[2], char *url, const char *prog); +extern int finish_connect(pid_t pid); +extern int path_match(const char *path, int nr, char **match); +extern int match_refs(struct ref *src, struct ref *dst, struct ref ***dst_tail, + int nr_refspec, char **refspec, int all); +extern int get_ack(int fd, unsigned char *result_sha1); +extern struct ref **get_remote_heads(int in, struct ref **list, int nr_match, char **match, unsigned int flags); +extern int server_supports(const char *feature); + +extern struct packed_git *parse_pack_index(unsigned char *sha1); +extern struct packed_git *parse_pack_index_file(const unsigned char *sha1, + char *idx_path); + +extern void prepare_packed_git(void); +extern void reprepare_packed_git(void); +extern void install_packed_git(struct packed_git *pack); + +extern struct packed_git *find_sha1_pack(const unsigned char *sha1, + struct packed_git *packs); + +extern void pack_report(void); +extern unsigned char* use_pack(struct packed_git *, struct pack_window **, unsigned long, unsigned int *); +extern void unuse_pack(struct pack_window **); +extern struct packed_git *add_packed_git(char *, int, int); +extern int num_packed_objects(const struct packed_git *p); +extern int nth_packed_object_sha1(const struct packed_git *, int, unsigned char*); +extern unsigned long find_pack_entry_one(const unsigned char *, struct packed_git *); +extern void *unpack_entry(struct packed_git *, unsigned long, char *, unsigned long *); +extern unsigned long unpack_object_header_gently(const unsigned char *buf, unsigned long len, enum object_type *type, unsigned long *sizep); +extern void packed_object_info_detail(struct packed_git *, unsigned long, char *, unsigned long *, unsigned long *, unsigned int *, unsigned char *); + +/* Dumb servers support */ +extern int update_server_info(int); + +typedef int (*config_fn_t)(const char *, const char *); +extern int git_default_config(const char *, const char *); +extern int git_config_from_file(config_fn_t fn, const char *); +extern int git_config(config_fn_t fn); +extern int git_config_int(const char *, const char *); +extern int git_config_bool(const char *, const char *); +extern int git_config_set(const char *, const char *); +extern int git_config_set_multivar(const char *, const char *, const char *, int); +extern int git_config_rename_section(const char *, const char *); +extern int check_repository_format_version(const char *var, const char *value); + +#define MAX_GITNAME (1000) +extern char git_default_email[MAX_GITNAME]; +extern char git_default_name[MAX_GITNAME]; + +extern char *git_commit_encoding; +extern char *git_log_output_encoding; + +extern int copy_fd(int ifd, int ofd); +extern int read_in_full(int fd, void *buf, size_t count); +extern int write_in_full(int fd, const void *buf, size_t count); +extern void write_or_die(int fd, const void *buf, size_t count); +extern int write_or_whine(int fd, const void *buf, size_t count, const char *msg); +extern int write_or_whine_pipe(int fd, const void *buf, size_t count, const char *msg); + +/* pager.c */ +extern void setup_pager(void); +extern int pager_in_use; +extern int pager_use_color; + +/* base85 */ +int decode_85(char *dst, char *line, int linelen); +void encode_85(char *buf, unsigned char *data, int bytes); + +/* alloc.c */ +struct blob; +struct tree; +struct commit; +struct tag; +extern struct blob *alloc_blob_node(void); +extern struct tree *alloc_tree_node(void); +extern struct commit *alloc_commit_node(void); +extern struct tag *alloc_tag_node(void); +extern void alloc_report(void); + +/* trace.c */ +extern int nfvasprintf(char **str, const char *fmt, va_list va); +extern void trace_printf(const char *format, ...); +extern void trace_argv_printf(const char **argv, int count, const char *format, ...); + +#endif /* CACHE_H */ diff --git a/check-builtins.sh b/check-builtins.sh new file mode 100755 index 0000000000..d6fe6cf174 --- /dev/null +++ b/check-builtins.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +{ + cat <<\EOF +sayIt: + $(foreach b,$(BUILT_INS),echo XXX $b YYY;) +EOF + cat Makefile +} | +make -f - sayIt 2>/dev/null | +sed -n -e 's/.*XXX \(.*\) YYY.*/\1/p' | +sort | +{ + bad=0 + while read builtin + do + base=`expr "$builtin" : 'git-\(.*\)'` + x=`sed -ne 's/.*{ "'$base'", \(cmd_[^, ]*\).*/'$base' \1/p' git.c` + if test -z "$x" + then + echo "$base is builtin but not listed in git.c command list" + bad=1 + fi + for sfx in sh perl py + do + if test -f "$builtin.$sfx" + then + echo "$base is builtin but $builtin.$sfx still exists" + bad=1 + fi + done + done + exit $bad +} diff --git a/check-racy.c b/check-racy.c new file mode 100644 index 0000000000..d6a08b4a55 --- /dev/null +++ b/check-racy.c @@ -0,0 +1,28 @@ +#include "cache.h" + +int main(int ac, char **av) +{ + int i; + int dirty, clean, racy; + + dirty = clean = racy = 0; + read_cache(); + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + struct stat st; + + if (lstat(ce->name, &st)) { + error("lstat(%s): %s", ce->name, strerror(errno)); + continue; + } + + if (ce_match_stat(ce, &st, 0)) + dirty++; + else if (ce_match_stat(ce, &st, 2)) + racy++; + else + clean++; + } + printf("dirty %d, clean %d, racy %d\n", dirty, clean, racy); + return 0; +} diff --git a/color.c b/color.c new file mode 100644 index 0000000000..09d82eec3d --- /dev/null +++ b/color.c @@ -0,0 +1,173 @@ +#include "cache.h" +#include "color.h" + +#define COLOR_RESET "\033[m" + +static int parse_color(const char *name, int len) +{ + static const char * const color_names[] = { + "normal", "black", "red", "green", "yellow", + "blue", "magenta", "cyan", "white" + }; + char *end; + int i; + for (i = 0; i < ARRAY_SIZE(color_names); i++) { + const char *str = color_names[i]; + if (!strncasecmp(name, str, len) && !str[len]) + return i - 1; + } + i = strtol(name, &end, 10); + if (*name && !*end && i >= -1 && i <= 255) + return i; + return -2; +} + +static int parse_attr(const char *name, int len) +{ + static const int attr_values[] = { 1, 2, 4, 5, 7 }; + static const char * const attr_names[] = { + "bold", "dim", "ul", "blink", "reverse" + }; + int i; + for (i = 0; i < ARRAY_SIZE(attr_names); i++) { + const char *str = attr_names[i]; + if (!strncasecmp(name, str, len) && !str[len]) + return attr_values[i]; + } + return -1; +} + +void color_parse(const char *value, const char *var, char *dst) +{ + const char *ptr = value; + int attr = -1; + int fg = -2; + int bg = -2; + + if (!strcasecmp(value, "reset")) { + strcpy(dst, "\033[m"); + return; + } + + /* [fg [bg]] [attr] */ + while (*ptr) { + const char *word = ptr; + int val, len = 0; + + while (word[len] && !isspace(word[len])) + len++; + + ptr = word + len; + while (*ptr && isspace(*ptr)) + ptr++; + + val = parse_color(word, len); + if (val >= -1) { + if (fg == -2) { + fg = val; + continue; + } + if (bg == -2) { + bg = val; + continue; + } + goto bad; + } + val = parse_attr(word, len); + if (val < 0 || attr != -1) + goto bad; + attr = val; + } + + if (attr >= 0 || fg >= 0 || bg >= 0) { + int sep = 0; + + *dst++ = '\033'; + *dst++ = '['; + if (attr >= 0) { + *dst++ = '0' + attr; + sep++; + } + if (fg >= 0) { + if (sep++) + *dst++ = ';'; + if (fg < 8) { + *dst++ = '3'; + *dst++ = '0' + fg; + } else { + dst += sprintf(dst, "38;5;%d", fg); + } + } + if (bg >= 0) { + if (sep++) + *dst++ = ';'; + if (bg < 8) { + *dst++ = '4'; + *dst++ = '0' + bg; + } else { + dst += sprintf(dst, "48;5;%d", bg); + } + } + *dst++ = 'm'; + } + *dst = 0; + return; +bad: + die("bad config value '%s' for variable '%s'", value, var); +} + +int git_config_colorbool(const char *var, const char *value) +{ + if (!value) + return 1; + if (!strcasecmp(value, "auto")) { + if (isatty(1) || (pager_in_use && pager_use_color)) { + char *term = getenv("TERM"); + if (term && strcmp(term, "dumb")) + return 1; + } + return 0; + } + if (!strcasecmp(value, "never")) + return 0; + if (!strcasecmp(value, "always")) + return 1; + return git_config_bool(var, value); +} + +static int color_vprintf(const char *color, const char *fmt, + va_list args, const char *trail) +{ + int r = 0; + + if (*color) + r += printf("%s", color); + r += vprintf(fmt, args); + if (*color) + r += printf("%s", COLOR_RESET); + if (trail) + r += printf("%s", trail); + return r; +} + + + +int color_printf(const char *color, const char *fmt, ...) +{ + va_list args; + int r; + va_start(args, fmt); + r = color_vprintf(color, fmt, args, NULL); + va_end(args); + return r; +} + +int color_printf_ln(const char *color, const char *fmt, ...) +{ + va_list args; + int r; + va_start(args, fmt); + r = color_vprintf(color, fmt, args, "\n"); + va_end(args); + return r; +} diff --git a/color.h b/color.h new file mode 100644 index 0000000000..88bb8ff1bd --- /dev/null +++ b/color.h @@ -0,0 +1,12 @@ +#ifndef COLOR_H +#define COLOR_H + +/* "\033[1;38;5;2xx;48;5;2xxm\0" is 23 bytes */ +#define COLOR_MAXLEN 24 + +int git_config_colorbool(const char *var, const char *value); +void color_parse(const char *var, const char *value, char *dst); +int color_printf(const char *color, const char *fmt, ...); +int color_printf_ln(const char *color, const char *fmt, ...); + +#endif /* COLOR_H */ diff --git a/combine-diff.c b/combine-diff.c new file mode 100644 index 0000000000..a5f2c8dd4a --- /dev/null +++ b/combine-diff.c @@ -0,0 +1,1003 @@ +#include "cache.h" +#include "commit.h" +#include "blob.h" +#include "diff.h" +#include "diffcore.h" +#include "quote.h" +#include "xdiff-interface.h" +#include "log-tree.h" + +static struct combine_diff_path *intersect_paths(struct combine_diff_path *curr, int n, int num_parent) +{ + struct diff_queue_struct *q = &diff_queued_diff; + struct combine_diff_path *p; + int i; + + if (!n) { + struct combine_diff_path *list = NULL, **tail = &list; + for (i = 0; i < q->nr; i++) { + int len; + const char *path; + if (diff_unmodified_pair(q->queue[i])) + continue; + path = q->queue[i]->two->path; + len = strlen(path); + p = xmalloc(combine_diff_path_size(num_parent, len)); + p->path = (char*) &(p->parent[num_parent]); + memcpy(p->path, path, len); + p->path[len] = 0; + p->len = len; + p->next = NULL; + memset(p->parent, 0, + sizeof(p->parent[0]) * num_parent); + + hashcpy(p->sha1, q->queue[i]->two->sha1); + p->mode = q->queue[i]->two->mode; + hashcpy(p->parent[n].sha1, q->queue[i]->one->sha1); + p->parent[n].mode = q->queue[i]->one->mode; + p->parent[n].status = q->queue[i]->status; + *tail = p; + tail = &p->next; + } + return list; + } + + for (p = curr; p; p = p->next) { + int found = 0; + if (!p->len) + continue; + for (i = 0; i < q->nr; i++) { + const char *path; + int len; + + if (diff_unmodified_pair(q->queue[i])) + continue; + path = q->queue[i]->two->path; + len = strlen(path); + if (len == p->len && !memcmp(path, p->path, len)) { + found = 1; + hashcpy(p->parent[n].sha1, q->queue[i]->one->sha1); + p->parent[n].mode = q->queue[i]->one->mode; + p->parent[n].status = q->queue[i]->status; + break; + } + } + if (!found) + p->len = 0; + } + return curr; +} + +/* Lines lost from parent */ +struct lline { + struct lline *next; + int len; + unsigned long parent_map; + char line[FLEX_ARRAY]; +}; + +/* Lines surviving in the merge result */ +struct sline { + struct lline *lost_head, **lost_tail; + char *bol; + int len; + /* bit 0 up to (N-1) are on if the parent has this line (i.e. + * we did not change it). + * bit N is used for "interesting" lines, including context. + */ + unsigned long flag; + unsigned long *p_lno; +}; + +static char *grab_blob(const unsigned char *sha1, unsigned long *size) +{ + char *blob; + char type[20]; + if (is_null_sha1(sha1)) { + /* deleted blob */ + *size = 0; + return xcalloc(1, 1); + } + blob = read_sha1_file(sha1, type, size); + if (strcmp(type, blob_type)) + die("object '%s' is not a blob!", sha1_to_hex(sha1)); + return blob; +} + +static void append_lost(struct sline *sline, int n, const char *line, int len) +{ + struct lline *lline; + unsigned long this_mask = (1UL<<n); + if (line[len-1] == '\n') + len--; + + /* Check to see if we can squash things */ + if (sline->lost_head) { + struct lline *last_one = NULL; + /* We cannot squash it with earlier one */ + for (lline = sline->lost_head; + lline; + lline = lline->next) + if (lline->parent_map & this_mask) + last_one = lline; + lline = last_one ? last_one->next : sline->lost_head; + while (lline) { + if (lline->len == len && + !memcmp(lline->line, line, len)) { + lline->parent_map |= this_mask; + return; + } + lline = lline->next; + } + } + + lline = xmalloc(sizeof(*lline) + len + 1); + lline->len = len; + lline->next = NULL; + lline->parent_map = this_mask; + memcpy(lline->line, line, len); + lline->line[len] = 0; + *sline->lost_tail = lline; + sline->lost_tail = &lline->next; +} + +struct combine_diff_state { + struct xdiff_emit_state xm; + + unsigned int lno; + int ob, on, nb, nn; + unsigned long nmask; + int num_parent; + int n; + struct sline *sline; + struct sline *lost_bucket; +}; + +static void consume_line(void *state_, char *line, unsigned long len) +{ + struct combine_diff_state *state = state_; + if (5 < len && !memcmp("@@ -", line, 4)) { + if (parse_hunk_header(line, len, + &state->ob, &state->on, + &state->nb, &state->nn)) + return; + state->lno = state->nb; + if (!state->nb) + /* @@ -1,2 +0,0 @@ to remove the + * first two lines... + */ + state->nb = 1; + if (state->nn == 0) + /* @@ -X,Y +N,0 @@ removed Y lines + * that would have come *after* line N + * in the result. Our lost buckets hang + * to the line after the removed lines, + */ + state->lost_bucket = &state->sline[state->nb]; + else + state->lost_bucket = &state->sline[state->nb-1]; + if (!state->sline[state->nb-1].p_lno) + state->sline[state->nb-1].p_lno = + xcalloc(state->num_parent, + sizeof(unsigned long)); + state->sline[state->nb-1].p_lno[state->n] = state->ob; + return; + } + if (!state->lost_bucket) + return; /* not in any hunk yet */ + switch (line[0]) { + case '-': + append_lost(state->lost_bucket, state->n, line+1, len-1); + break; + case '+': + state->sline[state->lno-1].flag |= state->nmask; + state->lno++; + break; + } +} + +static void combine_diff(const unsigned char *parent, mmfile_t *result_file, + struct sline *sline, unsigned int cnt, int n, + int num_parent) +{ + unsigned int p_lno, lno; + unsigned long nmask = (1UL << n); + xpparam_t xpp; + xdemitconf_t xecfg; + mmfile_t parent_file; + xdemitcb_t ecb; + struct combine_diff_state state; + unsigned long sz; + + if (!cnt) + return; /* result deleted */ + + parent_file.ptr = grab_blob(parent, &sz); + parent_file.size = sz; + xpp.flags = XDF_NEED_MINIMAL; + xecfg.ctxlen = 0; + xecfg.flags = 0; + ecb.outf = xdiff_outf; + ecb.priv = &state; + memset(&state, 0, sizeof(state)); + state.xm.consume = consume_line; + state.nmask = nmask; + state.sline = sline; + state.lno = 1; + state.num_parent = num_parent; + state.n = n; + + xdl_diff(&parent_file, result_file, &xpp, &xecfg, &ecb); + free(parent_file.ptr); + + /* Assign line numbers for this parent. + * + * sline[lno].p_lno[n] records the first line number + * (counting from 1) for parent N if the final hunk display + * started by showing sline[lno] (possibly showing the lost + * lines attached to it first). + */ + for (lno = 0, p_lno = 1; lno <= cnt; lno++) { + struct lline *ll; + sline[lno].p_lno[n] = p_lno; + + /* How many lines would this sline advance the p_lno? */ + ll = sline[lno].lost_head; + while (ll) { + if (ll->parent_map & nmask) + p_lno++; /* '-' means parent had it */ + ll = ll->next; + } + if (lno < cnt && !(sline[lno].flag & nmask)) + p_lno++; /* no '+' means parent had it */ + } + sline[lno].p_lno[n] = p_lno; /* trailer */ +} + +static unsigned long context = 3; +static char combine_marker = '@'; + +static int interesting(struct sline *sline, unsigned long all_mask) +{ + /* If some parents lost lines here, or if we have added to + * some parent, it is interesting. + */ + return ((sline->flag & all_mask) || sline->lost_head); +} + +static unsigned long adjust_hunk_tail(struct sline *sline, + unsigned long all_mask, + unsigned long hunk_begin, + unsigned long i) +{ + /* i points at the first uninteresting line. If the last line + * of the hunk was interesting only because it has some + * deletion, then it is not all that interesting for the + * purpose of giving trailing context lines. This is because + * we output '-' line and then unmodified sline[i-1] itself in + * that case which gives us one extra context line. + */ + if ((hunk_begin + 1 <= i) && !(sline[i-1].flag & all_mask)) + i--; + return i; +} + +static unsigned long find_next(struct sline *sline, + unsigned long mark, + unsigned long i, + unsigned long cnt, + int look_for_uninteresting) +{ + /* We have examined up to i-1 and are about to look at i. + * Find next interesting or uninteresting line. Here, + * "interesting" does not mean interesting(), but marked by + * the give_context() function below (i.e. it includes context + * lines that are not interesting to interesting() function + * that are surrounded by interesting() ones. + */ + while (i <= cnt) + if (look_for_uninteresting + ? !(sline[i].flag & mark) + : (sline[i].flag & mark)) + return i; + else + i++; + return i; +} + +static int give_context(struct sline *sline, unsigned long cnt, int num_parent) +{ + unsigned long all_mask = (1UL<<num_parent) - 1; + unsigned long mark = (1UL<<num_parent); + unsigned long i; + + /* Two groups of interesting lines may have a short gap of + * uninteresting lines. Connect such groups to give them a + * bit of context. + * + * We first start from what the interesting() function says, + * and mark them with "mark", and paint context lines with the + * mark. So interesting() would still say false for such context + * lines but they are treated as "interesting" in the end. + */ + i = find_next(sline, mark, 0, cnt, 0); + if (cnt < i) + return 0; + + while (i <= cnt) { + unsigned long j = (context < i) ? (i - context) : 0; + unsigned long k; + + /* Paint a few lines before the first interesting line. */ + while (j < i) + sline[j++].flag |= mark; + + again: + /* we know up to i is to be included. where does the + * next uninteresting one start? + */ + j = find_next(sline, mark, i, cnt, 1); + if (cnt < j) + break; /* the rest are all interesting */ + + /* lookahead context lines */ + k = find_next(sline, mark, j, cnt, 0); + j = adjust_hunk_tail(sline, all_mask, i, j); + + if (k < j + context) { + /* k is interesting and [j,k) are not, but + * paint them interesting because the gap is small. + */ + while (j < k) + sline[j++].flag |= mark; + i = k; + goto again; + } + + /* j is the first uninteresting line and there is + * no overlap beyond it within context lines. Paint + * the trailing edge a bit. + */ + i = k; + k = (j + context < cnt+1) ? j + context : cnt+1; + while (j < k) + sline[j++].flag |= mark; + } + return 1; +} + +static int make_hunks(struct sline *sline, unsigned long cnt, + int num_parent, int dense) +{ + unsigned long all_mask = (1UL<<num_parent) - 1; + unsigned long mark = (1UL<<num_parent); + unsigned long i; + int has_interesting = 0; + + for (i = 0; i <= cnt; i++) { + if (interesting(&sline[i], all_mask)) + sline[i].flag |= mark; + else + sline[i].flag &= ~mark; + } + if (!dense) + return give_context(sline, cnt, num_parent); + + /* Look at each hunk, and if we have changes from only one + * parent, or the changes are the same from all but one + * parent, mark that uninteresting. + */ + i = 0; + while (i <= cnt) { + unsigned long j, hunk_begin, hunk_end; + unsigned long same_diff; + while (i <= cnt && !(sline[i].flag & mark)) + i++; + if (cnt < i) + break; /* No more interesting hunks */ + hunk_begin = i; + for (j = i + 1; j <= cnt; j++) { + if (!(sline[j].flag & mark)) { + /* Look beyond the end to see if there + * is an interesting line after this + * hunk within context span. + */ + unsigned long la; /* lookahead */ + int contin = 0; + la = adjust_hunk_tail(sline, all_mask, + hunk_begin, j); + la = (la + context < cnt + 1) ? + (la + context) : cnt + 1; + while (j <= --la) { + if (sline[la].flag & mark) { + contin = 1; + break; + } + } + if (!contin) + break; + j = la; + } + } + hunk_end = j; + + /* [i..hunk_end) are interesting. Now is it really + * interesting? We check if there are only two versions + * and the result matches one of them. That is, we look + * at: + * (+) line, which records lines added to which parents; + * this line appears in the result. + * (-) line, which records from what parents the line + * was removed; this line does not appear in the result. + * then check the set of parents the result has difference + * from, from all lines. If there are lines that has + * different set of parents that the result has differences + * from, that means we have more than two versions. + * + * Even when we have only two versions, if the result does + * not match any of the parents, the it should be considered + * interesting. In such a case, we would have all '+' line. + * After passing the above "two versions" test, that would + * appear as "the same set of parents" to be "all parents". + */ + same_diff = 0; + has_interesting = 0; + for (j = i; j < hunk_end && !has_interesting; j++) { + unsigned long this_diff = sline[j].flag & all_mask; + struct lline *ll = sline[j].lost_head; + if (this_diff) { + /* This has some changes. Is it the + * same as others? + */ + if (!same_diff) + same_diff = this_diff; + else if (same_diff != this_diff) { + has_interesting = 1; + break; + } + } + while (ll && !has_interesting) { + /* Lost this line from these parents; + * who are they? Are they the same? + */ + this_diff = ll->parent_map; + if (!same_diff) + same_diff = this_diff; + else if (same_diff != this_diff) { + has_interesting = 1; + } + ll = ll->next; + } + } + + if (!has_interesting && same_diff != all_mask) { + /* This hunk is not that interesting after all */ + for (j = hunk_begin; j < hunk_end; j++) + sline[j].flag &= ~mark; + } + i = hunk_end; + } + + has_interesting = give_context(sline, cnt, num_parent); + return has_interesting; +} + +static void show_parent_lno(struct sline *sline, unsigned long l0, unsigned long l1, int n, unsigned long null_context) +{ + l0 = sline[l0].p_lno[n]; + l1 = sline[l1].p_lno[n]; + printf(" -%lu,%lu", l0, l1-l0-null_context); +} + +static int hunk_comment_line(const char *bol) +{ + int ch; + + if (!bol) + return 0; + ch = *bol & 0xff; + return (isalpha(ch) || ch == '_' || ch == '$'); +} + +static void dump_sline(struct sline *sline, unsigned long cnt, int num_parent, + int use_color) +{ + unsigned long mark = (1UL<<num_parent); + int i; + unsigned long lno = 0; + const char *c_frag = diff_get_color(use_color, DIFF_FRAGINFO); + const char *c_new = diff_get_color(use_color, DIFF_FILE_NEW); + const char *c_old = diff_get_color(use_color, DIFF_FILE_OLD); + const char *c_plain = diff_get_color(use_color, DIFF_PLAIN); + const char *c_reset = diff_get_color(use_color, DIFF_RESET); + + if (!cnt) + return; /* result deleted */ + + while (1) { + struct sline *sl = &sline[lno]; + unsigned long hunk_end; + unsigned long rlines; + const char *hunk_comment = NULL; + unsigned long null_context = 0; + + while (lno <= cnt && !(sline[lno].flag & mark)) { + if (hunk_comment_line(sline[lno].bol)) + hunk_comment = sline[lno].bol; + lno++; + } + if (cnt < lno) + break; + else { + for (hunk_end = lno + 1; hunk_end <= cnt; hunk_end++) + if (!(sline[hunk_end].flag & mark)) + break; + } + rlines = hunk_end - lno; + if (cnt < hunk_end) + rlines--; /* pointing at the last delete hunk */ + + if (!context) { + /* + * Even when running with --unified=0, all + * lines in the hunk needs to be processed in + * the loop below in order to show the + * deletion recorded in lost_head. However, + * we do not want to show the resulting line + * with all blank context markers in such a + * case. Compensate. + */ + unsigned long j; + for (j = lno; j < hunk_end; j++) + if (!(sline[j].flag & (mark-1))) + null_context++; + rlines -= null_context; + } + + fputs(c_frag, stdout); + for (i = 0; i <= num_parent; i++) putchar(combine_marker); + for (i = 0; i < num_parent; i++) + show_parent_lno(sline, lno, hunk_end, i, null_context); + printf(" +%lu,%lu ", lno+1, rlines); + for (i = 0; i <= num_parent; i++) putchar(combine_marker); + + if (hunk_comment) { + int comment_end = 0; + for (i = 0; i < 40; i++) { + int ch = hunk_comment[i] & 0xff; + if (!ch || ch == '\n') + break; + if (!isspace(ch)) + comment_end = i; + } + if (comment_end) + putchar(' '); + for (i = 0; i < comment_end; i++) + putchar(hunk_comment[i]); + } + + printf("%s\n", c_reset); + while (lno < hunk_end) { + struct lline *ll; + int j; + unsigned long p_mask; + sl = &sline[lno++]; + ll = sl->lost_head; + while (ll) { + fputs(c_old, stdout); + for (j = 0; j < num_parent; j++) { + if (ll->parent_map & (1UL<<j)) + putchar('-'); + else + putchar(' '); + } + printf("%s%s\n", ll->line, c_reset); + ll = ll->next; + } + if (cnt < lno) + break; + p_mask = 1; + if (!(sl->flag & (mark-1))) { + /* + * This sline was here to hang the + * lost lines in front of it. + */ + if (!context) + continue; + fputs(c_plain, stdout); + } + else + fputs(c_new, stdout); + for (j = 0; j < num_parent; j++) { + if (p_mask & sl->flag) + putchar('+'); + else + putchar(' '); + p_mask <<= 1; + } + printf("%.*s%s\n", sl->len, sl->bol, c_reset); + } + } +} + +static void reuse_combine_diff(struct sline *sline, unsigned long cnt, + int i, int j) +{ + /* We have already examined parent j and we know parent i + * and parent j are the same, so reuse the combined result + * of parent j for parent i. + */ + unsigned long lno, imask, jmask; + imask = (1UL<<i); + jmask = (1UL<<j); + + for (lno = 0; lno <= cnt; lno++) { + struct lline *ll = sline->lost_head; + sline->p_lno[i] = sline->p_lno[j]; + while (ll) { + if (ll->parent_map & jmask) + ll->parent_map |= imask; + ll = ll->next; + } + if (sline->flag & jmask) + sline->flag |= imask; + sline++; + } + /* the overall size of the file (sline[cnt]) */ + sline->p_lno[i] = sline->p_lno[j]; +} + +static void dump_quoted_path(const char *prefix, const char *path, + const char *c_meta, const char *c_reset) +{ + printf("%s%s", c_meta, prefix); + if (quote_c_style(path, NULL, NULL, 0)) + quote_c_style(path, NULL, stdout, 0); + else + printf("%s", path); + printf("%s\n", c_reset); +} + +static void show_patch_diff(struct combine_diff_path *elem, int num_parent, + int dense, struct rev_info *rev) +{ + struct diff_options *opt = &rev->diffopt; + unsigned long result_size, cnt, lno; + char *result, *cp; + struct sline *sline; /* survived lines */ + int mode_differs = 0; + int i, show_hunks; + int working_tree_file = is_null_sha1(elem->sha1); + int abbrev = opt->full_index ? 40 : DEFAULT_ABBREV; + mmfile_t result_file; + + context = opt->context; + /* Read the result of merge first */ + if (!working_tree_file) + result = grab_blob(elem->sha1, &result_size); + else { + /* Used by diff-tree to read from the working tree */ + struct stat st; + int fd; + if (0 <= (fd = open(elem->path, O_RDONLY)) && + !fstat(fd, &st)) { + int len = st.st_size; + int sz = 0; + + elem->mode = canon_mode(st.st_mode); + result_size = len; + result = xmalloc(len + 1); + while (sz < len) { + int done = xread(fd, result+sz, len-sz); + if (done == 0) + break; + if (done < 0) + die("read error '%s'", elem->path); + sz += done; + } + result[len] = 0; + } + else { + /* deleted file */ + result_size = 0; + elem->mode = 0; + result = xcalloc(1, 1); + } + if (0 <= fd) + close(fd); + } + + for (cnt = 0, cp = result; cp < result + result_size; cp++) { + if (*cp == '\n') + cnt++; + } + if (result_size && result[result_size-1] != '\n') + cnt++; /* incomplete line */ + + sline = xcalloc(cnt+2, sizeof(*sline)); + sline[0].bol = result; + for (lno = 0; lno <= cnt + 1; lno++) { + sline[lno].lost_tail = &sline[lno].lost_head; + sline[lno].flag = 0; + } + for (lno = 0, cp = result; cp < result + result_size; cp++) { + if (*cp == '\n') { + sline[lno].len = cp - sline[lno].bol; + lno++; + if (lno < cnt) + sline[lno].bol = cp + 1; + } + } + if (result_size && result[result_size-1] != '\n') + sline[cnt-1].len = result_size - (sline[cnt-1].bol - result); + + result_file.ptr = result; + result_file.size = result_size; + + /* Even p_lno[cnt+1] is valid -- that is for the end line number + * for deletion hunk at the end. + */ + sline[0].p_lno = xcalloc((cnt+2) * num_parent, sizeof(unsigned long)); + for (lno = 0; lno <= cnt; lno++) + sline[lno+1].p_lno = sline[lno].p_lno + num_parent; + + for (i = 0; i < num_parent; i++) { + int j; + for (j = 0; j < i; j++) { + if (!hashcmp(elem->parent[i].sha1, + elem->parent[j].sha1)) { + reuse_combine_diff(sline, cnt, i, j); + break; + } + } + if (i <= j) + combine_diff(elem->parent[i].sha1, &result_file, sline, + cnt, i, num_parent); + if (elem->parent[i].mode != elem->mode) + mode_differs = 1; + } + + show_hunks = make_hunks(sline, cnt, num_parent, dense); + + if (show_hunks || mode_differs || working_tree_file) { + const char *abb; + int use_color = opt->color_diff; + const char *c_meta = diff_get_color(use_color, DIFF_METAINFO); + const char *c_reset = diff_get_color(use_color, DIFF_RESET); + int added = 0; + int deleted = 0; + + if (rev->loginfo && !rev->no_commit_id) + show_log(rev, opt->msg_sep); + dump_quoted_path(dense ? "diff --cc " : "diff --combined ", + elem->path, c_meta, c_reset); + printf("%sindex ", c_meta); + for (i = 0; i < num_parent; i++) { + abb = find_unique_abbrev(elem->parent[i].sha1, + abbrev); + printf("%s%s", i ? "," : "", abb); + } + abb = find_unique_abbrev(elem->sha1, abbrev); + printf("..%s%s\n", abb, c_reset); + + if (mode_differs) { + deleted = !elem->mode; + + /* We say it was added if nobody had it */ + added = !deleted; + for (i = 0; added && i < num_parent; i++) + if (elem->parent[i].status != + DIFF_STATUS_ADDED) + added = 0; + if (added) + printf("%snew file mode %06o", + c_meta, elem->mode); + else { + if (deleted) + printf("%sdeleted file ", c_meta); + printf("mode "); + for (i = 0; i < num_parent; i++) { + printf("%s%06o", i ? "," : "", + elem->parent[i].mode); + } + if (elem->mode) + printf("..%06o", elem->mode); + } + printf("%s\n", c_reset); + } + if (added) + dump_quoted_path("--- /dev/", "null", c_meta, c_reset); + else + dump_quoted_path("--- a/", elem->path, c_meta, c_reset); + if (deleted) + dump_quoted_path("+++ /dev/", "null", c_meta, c_reset); + else + dump_quoted_path("+++ b/", elem->path, c_meta, c_reset); + dump_sline(sline, cnt, num_parent, opt->color_diff); + } + free(result); + + for (lno = 0; lno < cnt; lno++) { + if (sline[lno].lost_head) { + struct lline *ll = sline[lno].lost_head; + while (ll) { + struct lline *tmp = ll; + ll = ll->next; + free(tmp); + } + } + } + free(sline[0].p_lno); + free(sline); +} + +#define COLONS "::::::::::::::::::::::::::::::::" + +static void show_raw_diff(struct combine_diff_path *p, int num_parent, struct rev_info *rev) +{ + struct diff_options *opt = &rev->diffopt; + int i, offset; + const char *prefix; + int line_termination, inter_name_termination; + + line_termination = opt->line_termination; + inter_name_termination = '\t'; + if (!line_termination) + inter_name_termination = 0; + + if (rev->loginfo && !rev->no_commit_id) + show_log(rev, opt->msg_sep); + + if (opt->output_format & DIFF_FORMAT_RAW) { + offset = strlen(COLONS) - num_parent; + if (offset < 0) + offset = 0; + prefix = COLONS + offset; + + /* Show the modes */ + for (i = 0; i < num_parent; i++) { + printf("%s%06o", prefix, p->parent[i].mode); + prefix = " "; + } + printf("%s%06o", prefix, p->mode); + + /* Show sha1's */ + for (i = 0; i < num_parent; i++) + printf(" %s", diff_unique_abbrev(p->parent[i].sha1, + opt->abbrev)); + printf(" %s ", diff_unique_abbrev(p->sha1, opt->abbrev)); + } + + if (opt->output_format & (DIFF_FORMAT_RAW | DIFF_FORMAT_NAME_STATUS)) { + for (i = 0; i < num_parent; i++) + putchar(p->parent[i].status); + putchar(inter_name_termination); + } + + if (line_termination) { + if (quote_c_style(p->path, NULL, NULL, 0)) + quote_c_style(p->path, NULL, stdout, 0); + else + printf("%s", p->path); + putchar(line_termination); + } + else { + printf("%s%c", p->path, line_termination); + } +} + +void show_combined_diff(struct combine_diff_path *p, + int num_parent, + int dense, + struct rev_info *rev) +{ + struct diff_options *opt = &rev->diffopt; + if (!p->len) + return; + if (opt->output_format & (DIFF_FORMAT_RAW | + DIFF_FORMAT_NAME | + DIFF_FORMAT_NAME_STATUS)) + show_raw_diff(p, num_parent, rev); + else if (opt->output_format & DIFF_FORMAT_PATCH) + show_patch_diff(p, num_parent, dense, rev); +} + +void diff_tree_combined(const unsigned char *sha1, + const unsigned char parent[][20], + int num_parent, + int dense, + struct rev_info *rev) +{ + struct diff_options *opt = &rev->diffopt; + struct diff_options diffopts; + struct combine_diff_path *p, *paths = NULL; + int i, num_paths, needsep, show_log_first; + + diffopts = *opt; + diffopts.output_format = DIFF_FORMAT_NO_OUTPUT; + diffopts.recursive = 1; + + show_log_first = !!rev->loginfo && !rev->no_commit_id; + needsep = 0; + /* find set of paths that everybody touches */ + for (i = 0; i < num_parent; i++) { + /* show stat against the first parent even + * when doing combined diff. + */ + int stat_opt = (opt->output_format & + (DIFF_FORMAT_NUMSTAT|DIFF_FORMAT_DIFFSTAT)); + if (i == 0 && stat_opt) + diffopts.output_format = stat_opt; + else + diffopts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_tree_sha1(parent[i], sha1, "", &diffopts); + diffcore_std(&diffopts); + paths = intersect_paths(paths, i, num_parent); + + if (show_log_first && i == 0) { + show_log(rev, opt->msg_sep); + if (rev->verbose_header && opt->output_format) + putchar(opt->line_termination); + } + diff_flush(&diffopts); + } + + /* find out surviving paths */ + for (num_paths = 0, p = paths; p; p = p->next) { + if (p->len) + num_paths++; + } + if (num_paths) { + if (opt->output_format & (DIFF_FORMAT_RAW | + DIFF_FORMAT_NAME | + DIFF_FORMAT_NAME_STATUS)) { + for (p = paths; p; p = p->next) { + if (p->len) + show_raw_diff(p, num_parent, rev); + } + needsep = 1; + } + else if (opt->output_format & + (DIFF_FORMAT_NUMSTAT|DIFF_FORMAT_DIFFSTAT)) + needsep = 1; + if (opt->output_format & DIFF_FORMAT_PATCH) { + if (needsep) + putchar(opt->line_termination); + for (p = paths; p; p = p->next) { + if (p->len) + show_patch_diff(p, num_parent, dense, + rev); + } + } + } + + /* Clean things up */ + while (paths) { + struct combine_diff_path *tmp = paths; + paths = paths->next; + free(tmp); + } +} + +void diff_tree_combined_merge(const unsigned char *sha1, + int dense, struct rev_info *rev) +{ + int num_parent; + const unsigned char (*parent)[20]; + struct commit *commit = lookup_commit(sha1); + struct commit_list *parents; + + /* count parents */ + for (parents = commit->parents, num_parent = 0; + parents; + parents = parents->next, num_parent++) + ; /* nothing */ + + parent = xmalloc(num_parent * sizeof(*parent)); + for (parents = commit->parents, num_parent = 0; + parents; + parents = parents->next, num_parent++) + hashcpy((unsigned char*)(parent + num_parent), + parents->item->object.sha1); + diff_tree_combined(sha1, parent, num_parent, dense, rev); +} diff --git a/commit.c b/commit.c new file mode 100644 index 0000000000..3e8c87294b --- /dev/null +++ b/commit.c @@ -0,0 +1,1205 @@ +#include "cache.h" +#include "tag.h" +#include "commit.h" +#include "pkt-line.h" +#include "utf8.h" + +int save_commit_buffer = 1; + +struct sort_node +{ + /* + * the number of children of the associated commit + * that also occur in the list being sorted. + */ + unsigned int indegree; + + /* + * reference to original list item that we will re-use + * on output. + */ + struct commit_list * list_item; + +}; + +const char *commit_type = "commit"; + +struct cmt_fmt_map { + const char *n; + size_t cmp_len; + enum cmit_fmt v; +} cmt_fmts[] = { + { "raw", 1, CMIT_FMT_RAW }, + { "medium", 1, CMIT_FMT_MEDIUM }, + { "short", 1, CMIT_FMT_SHORT }, + { "email", 1, CMIT_FMT_EMAIL }, + { "full", 5, CMIT_FMT_FULL }, + { "fuller", 5, CMIT_FMT_FULLER }, + { "oneline", 1, CMIT_FMT_ONELINE }, +}; + +enum cmit_fmt get_commit_format(const char *arg) +{ + int i; + + if (!arg || !*arg) + return CMIT_FMT_DEFAULT; + if (*arg == '=') + arg++; + for (i = 0; i < ARRAY_SIZE(cmt_fmts); i++) { + if (!strncmp(arg, cmt_fmts[i].n, cmt_fmts[i].cmp_len) && + !strncmp(arg, cmt_fmts[i].n, strlen(arg))) + return cmt_fmts[i].v; + } + + die("invalid --pretty format: %s", arg); +} + +static struct commit *check_commit(struct object *obj, + const unsigned char *sha1, + int quiet) +{ + if (obj->type != OBJ_COMMIT) { + if (!quiet) + error("Object %s is a %s, not a commit", + sha1_to_hex(sha1), typename(obj->type)); + return NULL; + } + return (struct commit *) obj; +} + +struct commit *lookup_commit_reference_gently(const unsigned char *sha1, + int quiet) +{ + struct object *obj = deref_tag(parse_object(sha1), NULL, 0); + + if (!obj) + return NULL; + return check_commit(obj, sha1, quiet); +} + +struct commit *lookup_commit_reference(const unsigned char *sha1) +{ + return lookup_commit_reference_gently(sha1, 0); +} + +struct commit *lookup_commit(const unsigned char *sha1) +{ + struct object *obj = lookup_object(sha1); + if (!obj) { + struct commit *ret = alloc_commit_node(); + created_object(sha1, &ret->object); + ret->object.type = OBJ_COMMIT; + return ret; + } + if (!obj->type) + obj->type = OBJ_COMMIT; + return check_commit(obj, sha1, 0); +} + +static unsigned long parse_commit_date(const char *buf) +{ + unsigned long date; + + if (memcmp(buf, "author", 6)) + return 0; + while (*buf++ != '\n') + /* nada */; + if (memcmp(buf, "committer", 9)) + return 0; + while (*buf++ != '>') + /* nada */; + date = strtoul(buf, NULL, 10); + if (date == ULONG_MAX) + date = 0; + return date; +} + +static struct commit_graft **commit_graft; +static int commit_graft_alloc, commit_graft_nr; + +static int commit_graft_pos(const unsigned char *sha1) +{ + int lo, hi; + lo = 0; + hi = commit_graft_nr; + while (lo < hi) { + int mi = (lo + hi) / 2; + struct commit_graft *graft = commit_graft[mi]; + int cmp = hashcmp(sha1, graft->sha1); + if (!cmp) + return mi; + if (cmp < 0) + hi = mi; + else + lo = mi + 1; + } + return -lo - 1; +} + +int register_commit_graft(struct commit_graft *graft, int ignore_dups) +{ + int pos = commit_graft_pos(graft->sha1); + + if (0 <= pos) { + if (ignore_dups) + free(graft); + else { + free(commit_graft[pos]); + commit_graft[pos] = graft; + } + return 1; + } + pos = -pos - 1; + if (commit_graft_alloc <= ++commit_graft_nr) { + commit_graft_alloc = alloc_nr(commit_graft_alloc); + commit_graft = xrealloc(commit_graft, + sizeof(*commit_graft) * + commit_graft_alloc); + } + if (pos < commit_graft_nr) + memmove(commit_graft + pos + 1, + commit_graft + pos, + (commit_graft_nr - pos - 1) * + sizeof(*commit_graft)); + commit_graft[pos] = graft; + return 0; +} + +struct commit_graft *read_graft_line(char *buf, int len) +{ + /* The format is just "Commit Parent1 Parent2 ...\n" */ + int i; + struct commit_graft *graft = NULL; + + if (buf[len-1] == '\n') + buf[--len] = 0; + if (buf[0] == '#' || buf[0] == '\0') + return NULL; + if ((len + 1) % 41) { + bad_graft_data: + error("bad graft data: %s", buf); + free(graft); + return NULL; + } + i = (len + 1) / 41 - 1; + graft = xmalloc(sizeof(*graft) + 20 * i); + graft->nr_parent = i; + if (get_sha1_hex(buf, graft->sha1)) + goto bad_graft_data; + for (i = 40; i < len; i += 41) { + if (buf[i] != ' ') + goto bad_graft_data; + if (get_sha1_hex(buf + i + 1, graft->parent[i/41])) + goto bad_graft_data; + } + return graft; +} + +int read_graft_file(const char *graft_file) +{ + FILE *fp = fopen(graft_file, "r"); + char buf[1024]; + if (!fp) + return -1; + while (fgets(buf, sizeof(buf), fp)) { + /* The format is just "Commit Parent1 Parent2 ...\n" */ + int len = strlen(buf); + struct commit_graft *graft = read_graft_line(buf, len); + if (!graft) + continue; + if (register_commit_graft(graft, 1)) + error("duplicate graft data: %s", buf); + } + fclose(fp); + return 0; +} + +static void prepare_commit_graft(void) +{ + static int commit_graft_prepared; + char *graft_file; + + if (commit_graft_prepared) + return; + graft_file = get_graft_file(); + read_graft_file(graft_file); + /* make sure shallows are read */ + is_repository_shallow(); + commit_graft_prepared = 1; +} + +static struct commit_graft *lookup_commit_graft(const unsigned char *sha1) +{ + int pos; + prepare_commit_graft(); + pos = commit_graft_pos(sha1); + if (pos < 0) + return NULL; + return commit_graft[pos]; +} + +int write_shallow_commits(int fd, int use_pack_protocol) +{ + int i, count = 0; + for (i = 0; i < commit_graft_nr; i++) + if (commit_graft[i]->nr_parent < 0) { + const char *hex = + sha1_to_hex(commit_graft[i]->sha1); + count++; + if (use_pack_protocol) + packet_write(fd, "shallow %s", hex); + else { + if (write_in_full(fd, hex, 40) != 40) + break; + if (write_in_full(fd, "\n", 1) != 1) + break; + } + } + return count; +} + +int unregister_shallow(const unsigned char *sha1) +{ + int pos = commit_graft_pos(sha1); + if (pos < 0) + return -1; + if (pos + 1 < commit_graft_nr) + memcpy(commit_graft + pos, commit_graft + pos + 1, + sizeof(struct commit_graft *) + * (commit_graft_nr - pos - 1)); + commit_graft_nr--; + return 0; +} + +int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size) +{ + char *tail = buffer; + char *bufptr = buffer; + unsigned char parent[20]; + struct commit_list **pptr; + struct commit_graft *graft; + unsigned n_refs = 0; + + if (item->object.parsed) + return 0; + item->object.parsed = 1; + tail += size; + if (tail <= bufptr + 5 || memcmp(bufptr, "tree ", 5)) + return error("bogus commit object %s", sha1_to_hex(item->object.sha1)); + if (tail <= bufptr + 45 || get_sha1_hex(bufptr + 5, parent) < 0) + return error("bad tree pointer in commit %s", + sha1_to_hex(item->object.sha1)); + item->tree = lookup_tree(parent); + if (item->tree) + n_refs++; + bufptr += 46; /* "tree " + "hex sha1" + "\n" */ + pptr = &item->parents; + + graft = lookup_commit_graft(item->object.sha1); + while (bufptr + 48 < tail && !memcmp(bufptr, "parent ", 7)) { + struct commit *new_parent; + + if (tail <= bufptr + 48 || + get_sha1_hex(bufptr + 7, parent) || + bufptr[47] != '\n') + return error("bad parents in commit %s", sha1_to_hex(item->object.sha1)); + bufptr += 48; + if (graft) + continue; + new_parent = lookup_commit(parent); + if (new_parent) { + pptr = &commit_list_insert(new_parent, pptr)->next; + n_refs++; + } + } + if (graft) { + int i; + struct commit *new_parent; + for (i = 0; i < graft->nr_parent; i++) { + new_parent = lookup_commit(graft->parent[i]); + if (!new_parent) + continue; + pptr = &commit_list_insert(new_parent, pptr)->next; + n_refs++; + } + } + item->date = parse_commit_date(bufptr); + + if (track_object_refs) { + unsigned i = 0; + struct commit_list *p; + struct object_refs *refs = alloc_object_refs(n_refs); + if (item->tree) + refs->ref[i++] = &item->tree->object; + for (p = item->parents; p; p = p->next) + refs->ref[i++] = &p->item->object; + set_object_refs(&item->object, refs); + } + + return 0; +} + +int parse_commit(struct commit *item) +{ + char type[20]; + void *buffer; + unsigned long size; + int ret; + + if (item->object.parsed) + return 0; + buffer = read_sha1_file(item->object.sha1, type, &size); + if (!buffer) + return error("Could not read %s", + sha1_to_hex(item->object.sha1)); + if (strcmp(type, commit_type)) { + free(buffer); + return error("Object %s not a commit", + sha1_to_hex(item->object.sha1)); + } + ret = parse_commit_buffer(item, buffer, size); + if (save_commit_buffer && !ret) { + item->buffer = buffer; + return 0; + } + free(buffer); + return ret; +} + +struct commit_list *commit_list_insert(struct commit *item, struct commit_list **list_p) +{ + struct commit_list *new_list = xmalloc(sizeof(struct commit_list)); + new_list->item = item; + new_list->next = *list_p; + *list_p = new_list; + return new_list; +} + +void free_commit_list(struct commit_list *list) +{ + while (list) { + struct commit_list *temp = list; + list = temp->next; + free(temp); + } +} + +struct commit_list * insert_by_date(struct commit *item, struct commit_list **list) +{ + struct commit_list **pp = list; + struct commit_list *p; + while ((p = *pp) != NULL) { + if (p->item->date < item->date) { + break; + } + pp = &p->next; + } + return commit_list_insert(item, pp); +} + + +void sort_by_date(struct commit_list **list) +{ + struct commit_list *ret = NULL; + while (*list) { + insert_by_date((*list)->item, &ret); + *list = (*list)->next; + } + *list = ret; +} + +struct commit *pop_most_recent_commit(struct commit_list **list, + unsigned int mark) +{ + struct commit *ret = (*list)->item; + struct commit_list *parents = ret->parents; + struct commit_list *old = *list; + + *list = (*list)->next; + free(old); + + while (parents) { + struct commit *commit = parents->item; + parse_commit(commit); + if (!(commit->object.flags & mark)) { + commit->object.flags |= mark; + insert_by_date(commit, list); + } + parents = parents->next; + } + return ret; +} + +void clear_commit_marks(struct commit *commit, unsigned int mark) +{ + struct commit_list *parents; + + commit->object.flags &= ~mark; + parents = commit->parents; + while (parents) { + struct commit *parent = parents->item; + + /* Have we already cleared this? */ + if (mark & parent->object.flags) + clear_commit_marks(parent, mark); + parents = parents->next; + } +} + +/* + * Generic support for pretty-printing the header + */ +static int get_one_line(const char *msg, unsigned long len) +{ + int ret = 0; + + while (len--) { + char c = *msg++; + if (!c) + break; + ret++; + if (c == '\n') + break; + } + return ret; +} + +/* High bit set, or ISO-2022-INT */ +static int non_ascii(int ch) +{ + ch = (ch & 0xff); + return ((ch & 0x80) || (ch == 0x1b)); +} + +static int is_rfc2047_special(char ch) +{ + return (non_ascii(ch) || (ch == '=') || (ch == '?') || (ch == '_')); +} + +static int add_rfc2047(char *buf, const char *line, int len, + const char *encoding) +{ + char *bp = buf; + int i, needquote; + char q_encoding[128]; + const char *q_encoding_fmt = "=?%s?q?"; + + for (i = needquote = 0; !needquote && i < len; i++) { + int ch = line[i]; + if (non_ascii(ch)) + needquote++; + if ((i + 1 < len) && + (ch == '=' && line[i+1] == '?')) + needquote++; + } + if (!needquote) + return sprintf(buf, "%.*s", len, line); + + i = snprintf(q_encoding, sizeof(q_encoding), q_encoding_fmt, encoding); + if (sizeof(q_encoding) < i) + die("Insanely long encoding name %s", encoding); + memcpy(bp, q_encoding, i); + bp += i; + for (i = 0; i < len; i++) { + unsigned ch = line[i] & 0xFF; + if (is_rfc2047_special(ch)) { + sprintf(bp, "=%02X", ch); + bp += 3; + } + else if (ch == ' ') + *bp++ = '_'; + else + *bp++ = ch; + } + memcpy(bp, "?=", 2); + bp += 2; + return bp - buf; +} + +static int add_user_info(const char *what, enum cmit_fmt fmt, char *buf, + const char *line, int relative_date, + const char *encoding) +{ + char *date; + int namelen; + unsigned long time; + int tz, ret; + const char *filler = " "; + + if (fmt == CMIT_FMT_ONELINE) + return 0; + date = strchr(line, '>'); + if (!date) + return 0; + namelen = ++date - line; + time = strtoul(date, &date, 10); + tz = strtol(date, NULL, 10); + + if (fmt == CMIT_FMT_EMAIL) { + char *name_tail = strchr(line, '<'); + int display_name_length; + if (!name_tail) + return 0; + while (line < name_tail && isspace(name_tail[-1])) + name_tail--; + display_name_length = name_tail - line; + filler = ""; + strcpy(buf, "From: "); + ret = strlen(buf); + ret += add_rfc2047(buf + ret, line, display_name_length, + encoding); + memcpy(buf + ret, name_tail, namelen - display_name_length); + ret += namelen - display_name_length; + buf[ret++] = '\n'; + } + else { + ret = sprintf(buf, "%s: %.*s%.*s\n", what, + (fmt == CMIT_FMT_FULLER) ? 4 : 0, + filler, namelen, line); + } + switch (fmt) { + case CMIT_FMT_MEDIUM: + ret += sprintf(buf + ret, "Date: %s\n", + show_date(time, tz, relative_date)); + break; + case CMIT_FMT_EMAIL: + ret += sprintf(buf + ret, "Date: %s\n", + show_rfc2822_date(time, tz)); + break; + case CMIT_FMT_FULLER: + ret += sprintf(buf + ret, "%sDate: %s\n", what, + show_date(time, tz, relative_date)); + break; + default: + /* notin' */ + break; + } + return ret; +} + +static int is_empty_line(const char *line, int *len_p) +{ + int len = *len_p; + while (len && isspace(line[len-1])) + len--; + *len_p = len; + return !len; +} + +static int add_merge_info(enum cmit_fmt fmt, char *buf, const struct commit *commit, int abbrev) +{ + struct commit_list *parent = commit->parents; + int offset; + + if ((fmt == CMIT_FMT_ONELINE) || (fmt == CMIT_FMT_EMAIL) || + !parent || !parent->next) + return 0; + + offset = sprintf(buf, "Merge:"); + + while (parent) { + struct commit *p = parent->item; + const char *hex = NULL; + const char *dots; + if (abbrev) + hex = find_unique_abbrev(p->object.sha1, abbrev); + if (!hex) + hex = sha1_to_hex(p->object.sha1); + dots = (abbrev && strlen(hex) != 40) ? "..." : ""; + parent = parent->next; + + offset += sprintf(buf + offset, " %s%s", hex, dots); + } + buf[offset++] = '\n'; + return offset; +} + +static char *get_header(const struct commit *commit, const char *key) +{ + int key_len = strlen(key); + const char *line = commit->buffer; + + for (;;) { + const char *eol = strchr(line, '\n'), *next; + + if (line == eol) + return NULL; + if (!eol) { + eol = line + strlen(line); + next = NULL; + } else + next = eol + 1; + if (!strncmp(line, key, key_len) && line[key_len] == ' ') { + int len = eol - line - key_len; + char *ret = xmalloc(len); + memcpy(ret, line + key_len + 1, len - 1); + ret[len - 1] = '\0'; + return ret; + } + line = next; + } +} + +static char *replace_encoding_header(char *buf, char *encoding) +{ + char *encoding_header = strstr(buf, "\nencoding "); + char *end_of_encoding_header; + int encoding_header_pos; + int encoding_header_len; + int new_len; + int need_len; + int buflen = strlen(buf) + 1; + + if (!encoding_header) + return buf; /* should not happen but be defensive */ + encoding_header++; + end_of_encoding_header = strchr(encoding_header, '\n'); + if (!end_of_encoding_header) + return buf; /* should not happen but be defensive */ + end_of_encoding_header++; + + encoding_header_len = end_of_encoding_header - encoding_header; + encoding_header_pos = encoding_header - buf; + + if (is_encoding_utf8(encoding)) { + /* we have re-coded to UTF-8; drop the header */ + memmove(encoding_header, end_of_encoding_header, + buflen - (encoding_header_pos + encoding_header_len)); + return buf; + } + new_len = strlen(encoding); + need_len = new_len + strlen("encoding \n"); + if (encoding_header_len < need_len) { + buf = xrealloc(buf, buflen + (need_len - encoding_header_len)); + encoding_header = buf + encoding_header_pos; + end_of_encoding_header = encoding_header + encoding_header_len; + } + memmove(end_of_encoding_header + (need_len - encoding_header_len), + end_of_encoding_header, + buflen - (encoding_header_pos + encoding_header_len)); + memcpy(encoding_header + 9, encoding, strlen(encoding)); + encoding_header[9 + new_len] = '\n'; + return buf; +} + +static char *logmsg_reencode(const struct commit *commit, + char *output_encoding) +{ + char *encoding; + char *out; + char *utf8 = "utf-8"; + + if (!*output_encoding) + return NULL; + encoding = get_header(commit, "encoding"); + if (!encoding) + encoding = utf8; + if (!strcmp(encoding, output_encoding)) + out = strdup(commit->buffer); + else + out = reencode_string(commit->buffer, + output_encoding, encoding); + if (out) + out = replace_encoding_header(out, output_encoding); + + if (encoding != utf8) + free(encoding); + if (!out) + return NULL; + return out; +} + +unsigned long pretty_print_commit(enum cmit_fmt fmt, + const struct commit *commit, + unsigned long len, + char *buf, unsigned long space, + int abbrev, const char *subject, + const char *after_subject, + int relative_date) +{ + int hdr = 1, body = 0, seen_title = 0; + unsigned long offset = 0; + int indent = 4; + int parents_shown = 0; + const char *msg = commit->buffer; + int plain_non_ascii = 0; + char *reencoded; + char *encoding; + + encoding = (git_log_output_encoding + ? git_log_output_encoding + : git_commit_encoding); + if (!encoding) + encoding = "utf-8"; + reencoded = logmsg_reencode(commit, encoding); + if (reencoded) + msg = reencoded; + + if (fmt == CMIT_FMT_ONELINE || fmt == CMIT_FMT_EMAIL) + indent = 0; + + /* After-subject is used to pass in Content-Type: multipart + * MIME header; in that case we do not have to do the + * plaintext content type even if the commit message has + * non 7-bit ASCII character. Otherwise, check if we need + * to say this is not a 7-bit ASCII. + */ + if (fmt == CMIT_FMT_EMAIL && !after_subject) { + int i, ch, in_body; + + for (in_body = i = 0; (ch = msg[i]) && i < len; i++) { + if (!in_body) { + /* author could be non 7-bit ASCII but + * the log may be so; skip over the + * header part first. + */ + if (ch == '\n' && + i + 1 < len && msg[i+1] == '\n') + in_body = 1; + } + else if (non_ascii(ch)) { + plain_non_ascii = 1; + break; + } + } + } + + for (;;) { + const char *line = msg; + int linelen = get_one_line(msg, len); + + if (!linelen) + break; + + /* + * We want some slop for indentation and a possible + * final "...". Thus the "+ 20". + */ + if (offset + linelen + 20 > space) { + memcpy(buf + offset, " ...\n", 8); + offset += 8; + break; + } + + msg += linelen; + len -= linelen; + if (hdr) { + if (linelen == 1) { + hdr = 0; + if ((fmt != CMIT_FMT_ONELINE) && !subject) + buf[offset++] = '\n'; + continue; + } + if (fmt == CMIT_FMT_RAW) { + memcpy(buf + offset, line, linelen); + offset += linelen; + continue; + } + if (!memcmp(line, "parent ", 7)) { + if (linelen != 48) + die("bad parent line in commit"); + continue; + } + + if (!parents_shown) { + offset += add_merge_info(fmt, buf + offset, + commit, abbrev); + parents_shown = 1; + continue; + } + /* + * MEDIUM == DEFAULT shows only author with dates. + * FULL shows both authors but not dates. + * FULLER shows both authors and dates. + */ + if (!memcmp(line, "author ", 7)) + offset += add_user_info("Author", fmt, + buf + offset, + line + 7, + relative_date, + encoding); + if (!memcmp(line, "committer ", 10) && + (fmt == CMIT_FMT_FULL || fmt == CMIT_FMT_FULLER)) + offset += add_user_info("Commit", fmt, + buf + offset, + line + 10, + relative_date, + encoding); + continue; + } + + if (!subject) + body = 1; + + if (is_empty_line(line, &linelen)) { + if (!seen_title) + continue; + if (!body) + continue; + if (subject) + continue; + if (fmt == CMIT_FMT_SHORT) + break; + } + + seen_title = 1; + if (subject) { + int slen = strlen(subject); + memcpy(buf + offset, subject, slen); + offset += slen; + offset += add_rfc2047(buf + offset, line, linelen, + encoding); + } + else { + memset(buf + offset, ' ', indent); + memcpy(buf + offset + indent, line, linelen); + offset += linelen + indent; + } + buf[offset++] = '\n'; + if (fmt == CMIT_FMT_ONELINE) + break; + if (subject && plain_non_ascii) { + int sz; + char header[512]; + const char *header_fmt = + "Content-Type: text/plain; charset=%s\n" + "Content-Transfer-Encoding: 8bit\n"; + sz = snprintf(header, sizeof(header), header_fmt, + encoding); + if (sizeof(header) < sz) + die("Encoding name %s too long", encoding); + memcpy(buf + offset, header, sz); + offset += sz; + } + if (after_subject) { + int slen = strlen(after_subject); + if (slen > space - offset - 1) + slen = space - offset - 1; + memcpy(buf + offset, after_subject, slen); + offset += slen; + after_subject = NULL; + } + subject = NULL; + } + while (offset && isspace(buf[offset-1])) + offset--; + /* Make sure there is an EOLN for the non-oneline case */ + if (fmt != CMIT_FMT_ONELINE) + buf[offset++] = '\n'; + /* + * make sure there is another EOLN to separate the headers from whatever + * body the caller appends if we haven't already written a body + */ + if (fmt == CMIT_FMT_EMAIL && !body) + buf[offset++] = '\n'; + buf[offset] = '\0'; + + free(reencoded); + return offset; +} + +struct commit *pop_commit(struct commit_list **stack) +{ + struct commit_list *top = *stack; + struct commit *item = top ? top->item : NULL; + + if (top) { + *stack = top->next; + free(top); + } + return item; +} + +int count_parents(struct commit * commit) +{ + int count; + struct commit_list * parents = commit->parents; + for (count = 0; parents; parents = parents->next,count++) + ; + return count; +} + +void topo_sort_default_setter(struct commit *c, void *data) +{ + c->util = data; +} + +void *topo_sort_default_getter(struct commit *c) +{ + return c->util; +} + +/* + * Performs an in-place topological sort on the list supplied. + */ +void sort_in_topological_order(struct commit_list ** list, int lifo) +{ + sort_in_topological_order_fn(list, lifo, topo_sort_default_setter, + topo_sort_default_getter); +} + +void sort_in_topological_order_fn(struct commit_list ** list, int lifo, + topo_sort_set_fn_t setter, + topo_sort_get_fn_t getter) +{ + struct commit_list * next = *list; + struct commit_list * work = NULL, **insert; + struct commit_list ** pptr = list; + struct sort_node * nodes; + struct sort_node * next_nodes; + int count = 0; + + /* determine the size of the list */ + while (next) { + next = next->next; + count++; + } + + if (!count) + return; + /* allocate an array to help sort the list */ + nodes = xcalloc(count, sizeof(*nodes)); + /* link the list to the array */ + next_nodes = nodes; + next=*list; + while (next) { + next_nodes->list_item = next; + setter(next->item, next_nodes); + next_nodes++; + next = next->next; + } + /* update the indegree */ + next=*list; + while (next) { + struct commit_list * parents = next->item->parents; + while (parents) { + struct commit * parent=parents->item; + struct sort_node * pn = (struct sort_node *) getter(parent); + + if (pn) + pn->indegree++; + parents=parents->next; + } + next=next->next; + } + /* + * find the tips + * + * tips are nodes not reachable from any other node in the list + * + * the tips serve as a starting set for the work queue. + */ + next=*list; + insert = &work; + while (next) { + struct sort_node * node = (struct sort_node *) getter(next->item); + + if (node->indegree == 0) { + insert = &commit_list_insert(next->item, insert)->next; + } + next=next->next; + } + + /* process the list in topological order */ + if (!lifo) + sort_by_date(&work); + while (work) { + struct commit * work_item = pop_commit(&work); + struct sort_node * work_node = (struct sort_node *) getter(work_item); + struct commit_list * parents = work_item->parents; + + while (parents) { + struct commit * parent=parents->item; + struct sort_node * pn = (struct sort_node *) getter(parent); + + if (pn) { + /* + * parents are only enqueued for emission + * when all their children have been emitted thereby + * guaranteeing topological order. + */ + pn->indegree--; + if (!pn->indegree) { + if (!lifo) + insert_by_date(parent, &work); + else + commit_list_insert(parent, &work); + } + } + parents=parents->next; + } + /* + * work_item is a commit all of whose children + * have already been emitted. we can emit it now. + */ + *pptr = work_node->list_item; + pptr = &(*pptr)->next; + *pptr = NULL; + setter(work_item, NULL); + } + free(nodes); +} + +/* merge-base stuff */ + +/* bits #0..15 in revision.h */ +#define PARENT1 (1u<<16) +#define PARENT2 (1u<<17) +#define STALE (1u<<18) +#define RESULT (1u<<19) + +static const unsigned all_flags = (PARENT1 | PARENT2 | STALE | RESULT); + +static struct commit *interesting(struct commit_list *list) +{ + while (list) { + struct commit *commit = list->item; + list = list->next; + if (commit->object.flags & STALE) + continue; + return commit; + } + return NULL; +} + +static struct commit_list *merge_bases(struct commit *one, struct commit *two) +{ + struct commit_list *list = NULL; + struct commit_list *result = NULL; + + if (one == two) + /* We do not mark this even with RESULT so we do not + * have to clean it up. + */ + return commit_list_insert(one, &result); + + parse_commit(one); + parse_commit(two); + + one->object.flags |= PARENT1; + two->object.flags |= PARENT2; + insert_by_date(one, &list); + insert_by_date(two, &list); + + while (interesting(list)) { + struct commit *commit; + struct commit_list *parents; + struct commit_list *n; + int flags; + + commit = list->item; + n = list->next; + free(list); + list = n; + + flags = commit->object.flags & (PARENT1 | PARENT2 | STALE); + if (flags == (PARENT1 | PARENT2)) { + if (!(commit->object.flags & RESULT)) { + commit->object.flags |= RESULT; + insert_by_date(commit, &result); + } + /* Mark parents of a found merge stale */ + flags |= STALE; + } + parents = commit->parents; + while (parents) { + struct commit *p = parents->item; + parents = parents->next; + if ((p->object.flags & flags) == flags) + continue; + parse_commit(p); + p->object.flags |= flags; + insert_by_date(p, &list); + } + } + + /* Clean up the result to remove stale ones */ + free_commit_list(list); + list = result; result = NULL; + while (list) { + struct commit_list *n = list->next; + if (!(list->item->object.flags & STALE)) + insert_by_date(list->item, &result); + free(list); + list = n; + } + return result; +} + +struct commit_list *get_merge_bases(struct commit *one, + struct commit *two, + int cleanup) +{ + struct commit_list *list; + struct commit **rslt; + struct commit_list *result; + int cnt, i, j; + + result = merge_bases(one, two); + if (one == two) + return result; + if (!result || !result->next) { + if (cleanup) { + clear_commit_marks(one, all_flags); + clear_commit_marks(two, all_flags); + } + return result; + } + + /* There are more than one */ + cnt = 0; + list = result; + while (list) { + list = list->next; + cnt++; + } + rslt = xcalloc(cnt, sizeof(*rslt)); + for (list = result, i = 0; list; list = list->next) + rslt[i++] = list->item; + free_commit_list(result); + + clear_commit_marks(one, all_flags); + clear_commit_marks(two, all_flags); + for (i = 0; i < cnt - 1; i++) { + for (j = i+1; j < cnt; j++) { + if (!rslt[i] || !rslt[j]) + continue; + result = merge_bases(rslt[i], rslt[j]); + clear_commit_marks(rslt[i], all_flags); + clear_commit_marks(rslt[j], all_flags); + for (list = result; list; list = list->next) { + if (rslt[i] == list->item) + rslt[i] = NULL; + if (rslt[j] == list->item) + rslt[j] = NULL; + } + } + } + + /* Surviving ones in rslt[] are the independent results */ + result = NULL; + for (i = 0; i < cnt; i++) { + if (rslt[i]) + insert_by_date(rslt[i], &result); + } + free(rslt); + return result; +} + +int in_merge_bases(struct commit *rev1, struct commit *rev2) +{ + struct commit_list *bases, *b; + int ret = 0; + + bases = get_merge_bases(rev1, rev2, 1); + for (b = bases; b; b = b->next) { + if (!hashcmp(rev1->object.sha1, b->item->object.sha1)) { + ret = 1; + break; + } + } + + free_commit_list(bases); + return ret; +} diff --git a/commit.h b/commit.h new file mode 100644 index 0000000000..491b0c4aac --- /dev/null +++ b/commit.h @@ -0,0 +1,118 @@ +#ifndef COMMIT_H +#define COMMIT_H + +#include "object.h" +#include "tree.h" + +struct commit_list { + struct commit *item; + struct commit_list *next; +}; + +struct commit { + struct object object; + void *util; + unsigned long date; + struct commit_list *parents; + struct tree *tree; + char *buffer; +}; + +extern int save_commit_buffer; +extern const char *commit_type; + +struct commit *lookup_commit(const unsigned char *sha1); +struct commit *lookup_commit_reference(const unsigned char *sha1); +struct commit *lookup_commit_reference_gently(const unsigned char *sha1, + int quiet); + +int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size); + +int parse_commit(struct commit *item); + +struct commit_list * commit_list_insert(struct commit *item, struct commit_list **list_p); +struct commit_list * insert_by_date(struct commit *item, struct commit_list **list); + +void free_commit_list(struct commit_list *list); + +void sort_by_date(struct commit_list **list); + +/* Commit formats */ +enum cmit_fmt { + CMIT_FMT_RAW, + CMIT_FMT_MEDIUM, + CMIT_FMT_DEFAULT = CMIT_FMT_MEDIUM, + CMIT_FMT_SHORT, + CMIT_FMT_FULL, + CMIT_FMT_FULLER, + CMIT_FMT_ONELINE, + CMIT_FMT_EMAIL, + + CMIT_FMT_UNSPECIFIED, +}; + +extern enum cmit_fmt get_commit_format(const char *arg); +extern unsigned long pretty_print_commit(enum cmit_fmt fmt, const struct commit *, unsigned long len, char *buf, unsigned long space, int abbrev, const char *subject, const char *after_subject, int relative_date); + +/** Removes the first commit from a list sorted by date, and adds all + * of its parents. + **/ +struct commit *pop_most_recent_commit(struct commit_list **list, + unsigned int mark); + +struct commit *pop_commit(struct commit_list **stack); + +void clear_commit_marks(struct commit *commit, unsigned int mark); + +int count_parents(struct commit * commit); + +/* + * Performs an in-place topological sort of list supplied. + * + * Pre-conditions for sort_in_topological_order: + * all commits in input list and all parents of those + * commits must have object.util == NULL + * + * Pre-conditions for sort_in_topological_order_fn: + * all commits in input list and all parents of those + * commits must have getter(commit) == NULL + * + * Post-conditions: + * invariant of resulting list is: + * a reachable from b => ord(b) < ord(a) + * in addition, when lifo == 0, commits on parallel tracks are + * sorted in the dates order. + */ + +typedef void (*topo_sort_set_fn_t)(struct commit*, void *data); +typedef void* (*topo_sort_get_fn_t)(struct commit*); + +void topo_sort_default_setter(struct commit *c, void *data); +void *topo_sort_default_getter(struct commit *c); + +void sort_in_topological_order(struct commit_list ** list, int lifo); +void sort_in_topological_order_fn(struct commit_list ** list, int lifo, + topo_sort_set_fn_t setter, + topo_sort_get_fn_t getter); + +struct commit_graft { + unsigned char sha1[20]; + int nr_parent; /* < 0 if shallow commit */ + unsigned char parent[FLEX_ARRAY][20]; /* more */ +}; + +struct commit_graft *read_graft_line(char *buf, int len); +int register_commit_graft(struct commit_graft *, int); +int read_graft_file(const char *graft_file); + +extern struct commit_list *get_merge_bases(struct commit *rev1, struct commit *rev2, int cleanup); + +extern int register_shallow(const unsigned char *sha1); +extern int unregister_shallow(const unsigned char *sha1); +extern int write_shallow_commits(int fd, int use_pack_protocol); +extern int is_repository_shallow(void); +extern struct commit_list *get_shallow_commits(struct object_array *heads, + int depth, int shallow_flag, int not_shallow_flag); + +int in_merge_bases(struct commit *rev1, struct commit *rev2); +#endif /* COMMIT_H */ diff --git a/compat/inet_ntop.c b/compat/inet_ntop.c new file mode 100644 index 0000000000..4d7ab9d975 --- /dev/null +++ b/compat/inet_ntop.c @@ -0,0 +1,200 @@ +/* + * Copyright (c) 1996-1999 by Internet Software Consortium. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS + * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE + * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR + * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + * SOFTWARE. + */ + +#include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <stdio.h> +#include <string.h> + +#ifndef NS_INADDRSZ +#define NS_INADDRSZ 4 +#endif +#ifndef NS_IN6ADDRSZ +#define NS_IN6ADDRSZ 16 +#endif +#ifndef NS_INT16SZ +#define NS_INT16SZ 2 +#endif + +/* + * WARNING: Don't even consider trying to compile this on a system where + * sizeof(int) < 4. sizeof(int) > 4 is fine; all the world's not a VAX. + */ + +/* const char * + * inet_ntop4(src, dst, size) + * format an IPv4 address + * return: + * `dst' (as a const) + * notes: + * (1) uses no statics + * (2) takes a u_char* not an in_addr as input + * author: + * Paul Vixie, 1996. + */ +static const char * +inet_ntop4(src, dst, size) + const u_char *src; + char *dst; + size_t size; +{ + static const char fmt[] = "%u.%u.%u.%u"; + char tmp[sizeof "255.255.255.255"]; + int nprinted; + + nprinted = snprintf(tmp, sizeof(tmp), fmt, src[0], src[1], src[2], src[3]); + if (nprinted < 0) + return (NULL); /* we assume "errno" was set by "snprintf()" */ + if ((size_t)nprinted > size) { + errno = ENOSPC; + return (NULL); + } + strcpy(dst, tmp); + return (dst); +} + +#ifndef NO_IPV6 +/* const char * + * inet_ntop6(src, dst, size) + * convert IPv6 binary address into presentation (printable) format + * author: + * Paul Vixie, 1996. + */ +static const char * +inet_ntop6(src, dst, size) + const u_char *src; + char *dst; + size_t size; +{ + /* + * Note that int32_t and int16_t need only be "at least" large enough + * to contain a value of the specified size. On some systems, like + * Crays, there is no such thing as an integer variable with 16 bits. + * Keep this in mind if you think this function should have been coded + * to use pointer overlays. All the world's not a VAX. + */ + char tmp[sizeof "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255"], *tp; + struct { int base, len; } best, cur; + unsigned int words[NS_IN6ADDRSZ / NS_INT16SZ]; + int i; + + /* + * Preprocess: + * Copy the input (bytewise) array into a wordwise array. + * Find the longest run of 0x00's in src[] for :: shorthanding. + */ + memset(words, '\0', sizeof words); + for (i = 0; i < NS_IN6ADDRSZ; i++) + words[i / 2] |= (src[i] << ((1 - (i % 2)) << 3)); + best.base = -1; + cur.base = -1; + for (i = 0; i < (NS_IN6ADDRSZ / NS_INT16SZ); i++) { + if (words[i] == 0) { + if (cur.base == -1) + cur.base = i, cur.len = 1; + else + cur.len++; + } else { + if (cur.base != -1) { + if (best.base == -1 || cur.len > best.len) + best = cur; + cur.base = -1; + } + } + } + if (cur.base != -1) { + if (best.base == -1 || cur.len > best.len) + best = cur; + } + if (best.base != -1 && best.len < 2) + best.base = -1; + + /* + * Format the result. + */ + tp = tmp; + for (i = 0; i < (NS_IN6ADDRSZ / NS_INT16SZ); i++) { + /* Are we inside the best run of 0x00's? */ + if (best.base != -1 && i >= best.base && + i < (best.base + best.len)) { + if (i == best.base) + *tp++ = ':'; + continue; + } + /* Are we following an initial run of 0x00s or any real hex? */ + if (i != 0) + *tp++ = ':'; + /* Is this address an encapsulated IPv4? */ + if (i == 6 && best.base == 0 && + (best.len == 6 || (best.len == 5 && words[5] == 0xffff))) { + if (!inet_ntop4(src+12, tp, sizeof tmp - (tp - tmp))) + return (NULL); + tp += strlen(tp); + break; + } + tp += snprintf(tp, sizeof tmp - (tp - tmp), "%x", words[i]); + } + /* Was it a trailing run of 0x00's? */ + if (best.base != -1 && (best.base + best.len) == + (NS_IN6ADDRSZ / NS_INT16SZ)) + *tp++ = ':'; + *tp++ = '\0'; + + /* + * Check for overflow, copy, and we're done. + */ + if ((size_t)(tp - tmp) > size) { + errno = ENOSPC; + return (NULL); + } + strcpy(dst, tmp); + return (dst); +} +#endif + +/* char * + * inet_ntop(af, src, dst, size) + * convert a network format address to presentation format. + * return: + * pointer to presentation format address (`dst'), or NULL (see errno). + * author: + * Paul Vixie, 1996. + */ +const char * +inet_ntop(af, src, dst, size) + int af; + const void *src; + char *dst; + size_t size; +{ + switch (af) { + case AF_INET: + return (inet_ntop4(src, dst, size)); +#ifndef NO_IPV6 + case AF_INET6: + return (inet_ntop6(src, dst, size)); +#endif + default: + errno = EAFNOSUPPORT; + return (NULL); + } + /* NOTREACHED */ +} diff --git a/compat/inet_pton.c b/compat/inet_pton.c new file mode 100644 index 0000000000..5704e0d2b6 --- /dev/null +++ b/compat/inet_pton.c @@ -0,0 +1,220 @@ +/* + * Copyright (C) 1996-2001 Internet Software Consortium. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM + * DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL + * INTERNET SOFTWARE CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING + * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION + * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <stdio.h> +#include <string.h> + +#ifndef NS_INT16SZ +#define NS_INT16SZ 2 +#endif + +#ifndef NS_INADDRSZ +#define NS_INADDRSZ 4 +#endif + +#ifndef NS_IN6ADDRSZ +#define NS_IN6ADDRSZ 16 +#endif + +/* + * WARNING: Don't even consider trying to compile this on a system where + * sizeof(int) < 4. sizeof(int) > 4 is fine; all the world's not a VAX. + */ + +static int inet_pton4(const char *src, unsigned char *dst); +static int inet_pton6(const char *src, unsigned char *dst); + +/* int + * inet_pton4(src, dst) + * like inet_aton() but without all the hexadecimal and shorthand. + * return: + * 1 if `src' is a valid dotted quad, else 0. + * notice: + * does not touch `dst' unless it's returning 1. + * author: + * Paul Vixie, 1996. + */ +static int +inet_pton4(const char *src, unsigned char *dst) +{ + static const char digits[] = "0123456789"; + int saw_digit, octets, ch; + unsigned char tmp[NS_INADDRSZ], *tp; + + saw_digit = 0; + octets = 0; + *(tp = tmp) = 0; + while ((ch = *src++) != '\0') { + const char *pch; + + if ((pch = strchr(digits, ch)) != NULL) { + unsigned int new = *tp * 10 + (pch - digits); + + if (new > 255) + return (0); + *tp = new; + if (! saw_digit) { + if (++octets > 4) + return (0); + saw_digit = 1; + } + } else if (ch == '.' && saw_digit) { + if (octets == 4) + return (0); + *++tp = 0; + saw_digit = 0; + } else + return (0); + } + if (octets < 4) + return (0); + memcpy(dst, tmp, NS_INADDRSZ); + return (1); +} + +/* int + * inet_pton6(src, dst) + * convert presentation level address to network order binary form. + * return: + * 1 if `src' is a valid [RFC1884 2.2] address, else 0. + * notice: + * (1) does not touch `dst' unless it's returning 1. + * (2) :: in a full address is silently ignored. + * credit: + * inspired by Mark Andrews. + * author: + * Paul Vixie, 1996. + */ + +#ifndef NO_IPV6 +static int +inet_pton6(const char *src, unsigned char *dst) +{ + static const char xdigits_l[] = "0123456789abcdef", + xdigits_u[] = "0123456789ABCDEF"; + unsigned char tmp[NS_IN6ADDRSZ], *tp, *endp, *colonp; + const char *xdigits, *curtok; + int ch, saw_xdigit; + unsigned int val; + + memset((tp = tmp), '\0', NS_IN6ADDRSZ); + endp = tp + NS_IN6ADDRSZ; + colonp = NULL; + /* Leading :: requires some special handling. */ + if (*src == ':') + if (*++src != ':') + return (0); + curtok = src; + saw_xdigit = 0; + val = 0; + while ((ch = *src++) != '\0') { + const char *pch; + + if ((pch = strchr((xdigits = xdigits_l), ch)) == NULL) + pch = strchr((xdigits = xdigits_u), ch); + if (pch != NULL) { + val <<= 4; + val |= (pch - xdigits); + if (val > 0xffff) + return (0); + saw_xdigit = 1; + continue; + } + if (ch == ':') { + curtok = src; + if (!saw_xdigit) { + if (colonp) + return (0); + colonp = tp; + continue; + } + if (tp + NS_INT16SZ > endp) + return (0); + *tp++ = (unsigned char) (val >> 8) & 0xff; + *tp++ = (unsigned char) val & 0xff; + saw_xdigit = 0; + val = 0; + continue; + } + if (ch == '.' && ((tp + NS_INADDRSZ) <= endp) && + inet_pton4(curtok, tp) > 0) { + tp += NS_INADDRSZ; + saw_xdigit = 0; + break; /* '\0' was seen by inet_pton4(). */ + } + return (0); + } + if (saw_xdigit) { + if (tp + NS_INT16SZ > endp) + return (0); + *tp++ = (unsigned char) (val >> 8) & 0xff; + *tp++ = (unsigned char) val & 0xff; + } + if (colonp != NULL) { + /* + * Since some memmove()'s erroneously fail to handle + * overlapping regions, we'll do the shift by hand. + */ + const int n = tp - colonp; + int i; + + for (i = 1; i <= n; i++) { + endp[- i] = colonp[n - i]; + colonp[n - i] = 0; + } + tp = endp; + } + if (tp != endp) + return (0); + memcpy(dst, tmp, NS_IN6ADDRSZ); + return (1); +} +#endif + +/* int + * isc_net_pton(af, src, dst) + * convert from presentation format (which usually means ASCII printable) + * to network format (which is usually some kind of binary format). + * return: + * 1 if the address was valid for the specified address family + * 0 if the address wasn't valid (`dst' is untouched in this case) + * -1 if some other error occurred (`dst' is untouched in this case, too) + * author: + * Paul Vixie, 1996. + */ +int +inet_pton(int af, const char *src, void *dst) +{ + switch (af) { + case AF_INET: + return (inet_pton4(src, dst)); +#ifndef NO_IPV6 + case AF_INET6: + return (inet_pton6(src, dst)); +#endif + default: + errno = EAFNOSUPPORT; + return (-1); + } + /* NOTREACHED */ +} diff --git a/compat/mmap.c b/compat/mmap.c new file mode 100644 index 0000000000..4cfaee3136 --- /dev/null +++ b/compat/mmap.c @@ -0,0 +1,43 @@ +#include "../git-compat-util.h" + +void *git_mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset) +{ + size_t n = 0; + + if (start != NULL || !(flags & MAP_PRIVATE)) + die("Invalid usage of mmap when built with NO_MMAP"); + + start = xmalloc(length); + if (start == NULL) { + errno = ENOMEM; + return MAP_FAILED; + } + + while (n < length) { + ssize_t count = pread(fd, (char *)start + n, length - n, offset + n); + + if (count == 0) { + memset((char *)start+n, 0, length-n); + break; + } + + if (count < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + free(start); + errno = EACCES; + return MAP_FAILED; + } + + n += count; + } + + return start; +} + +int git_munmap(void *start, size_t length) +{ + free(start); + return 0; +} + diff --git a/compat/pread.c b/compat/pread.c new file mode 100644 index 0000000000..978cac4ec9 --- /dev/null +++ b/compat/pread.c @@ -0,0 +1,18 @@ +#include "../git-compat-util.h" + +ssize_t git_pread(int fd, void *buf, size_t count, off_t offset) +{ + off_t current_offset; + ssize_t rc; + + current_offset = lseek(fd, 0, SEEK_CUR); + + if (lseek(fd, offset, SEEK_SET) < 0) + return -1; + + rc = read_in_full(fd, buf, count); + + if (current_offset != lseek(fd, current_offset, SEEK_SET)) + return -1; + return rc; +} diff --git a/compat/setenv.c b/compat/setenv.c new file mode 100644 index 0000000000..3a22ea7b75 --- /dev/null +++ b/compat/setenv.c @@ -0,0 +1,34 @@ +#include "../git-compat-util.h" + +int gitsetenv(const char *name, const char *value, int replace) +{ + int out; + size_t namelen, valuelen; + char *envstr; + + if (!name || !value) return -1; + if (!replace) { + char *oldval = NULL; + oldval = getenv(name); + if (oldval) return 0; + } + + namelen = strlen(name); + valuelen = strlen(value); + envstr = malloc((namelen + valuelen + 2)); + if (!envstr) return -1; + + memcpy(envstr, name, namelen); + envstr[namelen] = '='; + memcpy(envstr + namelen + 1, value, valuelen); + envstr[namelen + valuelen + 1] = 0; + + out = putenv(envstr); + /* putenv(3) makes the argument string part of the environment, + * and changing that string modifies the environment --- which + * means we do not own that storage anymore. Do not free + * envstr. + */ + + return out; +} diff --git a/compat/strcasestr.c b/compat/strcasestr.c new file mode 100644 index 0000000000..26896deca6 --- /dev/null +++ b/compat/strcasestr.c @@ -0,0 +1,22 @@ +#include "../git-compat-util.h" + +char *gitstrcasestr(const char *haystack, const char *needle) +{ + int nlen = strlen(needle); + int hlen = strlen(haystack) - nlen + 1; + int i; + + for (i = 0; i < hlen; i++) { + int j; + for (j = 0; j < nlen; j++) { + unsigned char c1 = haystack[i+j]; + unsigned char c2 = needle[j]; + if (toupper(c1) != toupper(c2)) + goto next; + } + return (char *) haystack + i; + next: + ; + } + return NULL; +} diff --git a/compat/strlcpy.c b/compat/strlcpy.c new file mode 100644 index 0000000000..4024c36030 --- /dev/null +++ b/compat/strlcpy.c @@ -0,0 +1,13 @@ +#include "../git-compat-util.h" + +size_t gitstrlcpy(char *dest, const char *src, size_t size) +{ + size_t ret = strlen(src); + + if (size) { + size_t len = (ret >= size) ? size - 1 : ret; + memcpy(dest, src, len); + dest[len] = '\0'; + } + return ret; +} diff --git a/compat/unsetenv.c b/compat/unsetenv.c new file mode 100644 index 0000000000..eb29f5e084 --- /dev/null +++ b/compat/unsetenv.c @@ -0,0 +1,25 @@ +#include "../git-compat-util.h" + +void gitunsetenv (const char *name) +{ + extern char **environ; + int src, dst; + size_t nmln; + + nmln = strlen(name); + + for (src = dst = 0; environ[src]; ++src) { + size_t enln; + enln = strlen(environ[src]); + if (enln > nmln) { + /* might match, and can test for '=' safely */ + if (0 == strncmp (environ[src], name, nmln) + && '=' == environ[src][nmln]) + /* matches, so skip */ + continue; + } + environ[dst] = environ[src]; + ++dst; + } + environ[dst] = NULL; +} diff --git a/config.c b/config.c new file mode 100644 index 0000000000..d82107124a --- /dev/null +++ b/config.c @@ -0,0 +1,923 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + * Copyright (C) Johannes Schindelin, 2005 + * + */ +#include "cache.h" + +#define MAXNAME (256) + +static FILE *config_file; +static const char *config_file_name; +static int config_linenr; +static int get_next_char(void) +{ + int c; + FILE *f; + + c = '\n'; + if ((f = config_file) != NULL) { + c = fgetc(f); + if (c == '\r') { + /* DOS like systems */ + c = fgetc(f); + if (c != '\n') { + ungetc(c, f); + c = '\r'; + } + } + if (c == '\n') + config_linenr++; + if (c == EOF) { + config_file = NULL; + c = '\n'; + } + } + return c; +} + +static char *parse_value(void) +{ + static char value[1024]; + int quote = 0, comment = 0, len = 0, space = 0; + + for (;;) { + int c = get_next_char(); + if (len >= sizeof(value)) + return NULL; + if (c == '\n') { + if (quote) + return NULL; + value[len] = 0; + return value; + } + if (comment) + continue; + if (isspace(c) && !quote) { + space = 1; + continue; + } + if (!quote) { + if (c == ';' || c == '#') { + comment = 1; + continue; + } + } + if (space) { + if (len) + value[len++] = ' '; + space = 0; + } + if (c == '\\') { + c = get_next_char(); + switch (c) { + case '\n': + continue; + case 't': + c = '\t'; + break; + case 'b': + c = '\b'; + break; + case 'n': + c = '\n'; + break; + /* Some characters escape as themselves */ + case '\\': case '"': + break; + /* Reject unknown escape sequences */ + default: + return NULL; + } + value[len++] = c; + continue; + } + if (c == '"') { + quote = 1-quote; + continue; + } + value[len++] = c; + } +} + +static inline int iskeychar(int c) +{ + return isalnum(c) || c == '-'; +} + +static int get_value(config_fn_t fn, char *name, unsigned int len) +{ + int c; + char *value; + + /* Get the full name */ + for (;;) { + c = get_next_char(); + if (c == EOF) + break; + if (!iskeychar(c)) + break; + name[len++] = tolower(c); + if (len >= MAXNAME) + return -1; + } + name[len] = 0; + while (c == ' ' || c == '\t') + c = get_next_char(); + + value = NULL; + if (c != '\n') { + if (c != '=') + return -1; + value = parse_value(); + if (!value) + return -1; + } + return fn(name, value); +} + +static int get_extended_base_var(char *name, int baselen, int c) +{ + do { + if (c == '\n') + return -1; + c = get_next_char(); + } while (isspace(c)); + + /* We require the format to be '[base "extension"]' */ + if (c != '"') + return -1; + name[baselen++] = '.'; + + for (;;) { + int c = get_next_char(); + if (c == '\n') + return -1; + if (c == '"') + break; + if (c == '\\') { + c = get_next_char(); + if (c == '\n') + return -1; + } + name[baselen++] = c; + if (baselen > MAXNAME / 2) + return -1; + } + + /* Final ']' */ + if (get_next_char() != ']') + return -1; + return baselen; +} + +static int get_base_var(char *name) +{ + int baselen = 0; + + for (;;) { + int c = get_next_char(); + if (c == EOF) + return -1; + if (c == ']') + return baselen; + if (isspace(c)) + return get_extended_base_var(name, baselen, c); + if (!iskeychar(c) && c != '.') + return -1; + if (baselen > MAXNAME / 2) + return -1; + name[baselen++] = tolower(c); + } +} + +static int git_parse_file(config_fn_t fn) +{ + int comment = 0; + int baselen = 0; + static char var[MAXNAME]; + + for (;;) { + int c = get_next_char(); + if (c == '\n') { + /* EOF? */ + if (!config_file) + return 0; + comment = 0; + continue; + } + if (comment || isspace(c)) + continue; + if (c == '#' || c == ';') { + comment = 1; + continue; + } + if (c == '[') { + baselen = get_base_var(var); + if (baselen <= 0) + break; + var[baselen++] = '.'; + var[baselen] = 0; + continue; + } + if (!isalpha(c)) + break; + var[baselen] = tolower(c); + if (get_value(fn, var, baselen+1) < 0) + break; + } + die("bad config file line %d in %s", config_linenr, config_file_name); +} + +int git_config_int(const char *name, const char *value) +{ + if (value && *value) { + char *end; + int val = strtol(value, &end, 0); + if (!*end) + return val; + if (!strcasecmp(end, "k")) + return val * 1024; + if (!strcasecmp(end, "m")) + return val * 1024 * 1024; + if (!strcasecmp(end, "g")) + return val * 1024 * 1024 * 1024; + } + die("bad config value for '%s' in %s", name, config_file_name); +} + +int git_config_bool(const char *name, const char *value) +{ + if (!value) + return 1; + if (!*value) + return 0; + if (!strcasecmp(value, "true") || !strcasecmp(value, "yes")) + return 1; + if (!strcasecmp(value, "false") || !strcasecmp(value, "no")) + return 0; + return git_config_int(name, value) != 0; +} + +int git_default_config(const char *var, const char *value) +{ + /* This needs a better name */ + if (!strcmp(var, "core.filemode")) { + trust_executable_bit = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "core.bare")) { + is_bare_repository_cfg = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "core.ignorestat")) { + assume_unchanged = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "core.prefersymlinkrefs")) { + prefer_symlink_refs = git_config_bool(var, value); + 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, "core.legacyheaders")) { + use_legacy_headers = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "core.compression")) { + int level = git_config_int(var, value); + if (level == -1) + level = Z_DEFAULT_COMPRESSION; + else if (level < 0 || level > Z_BEST_COMPRESSION) + die("bad zlib compression level %d", level); + zlib_compression_level = level; + return 0; + } + + if (!strcmp(var, "core.packedgitwindowsize")) { + int pgsz = getpagesize(); + packed_git_window_size = git_config_int(var, value); + packed_git_window_size /= pgsz; + if (packed_git_window_size < 2) + packed_git_window_size = 2; + packed_git_window_size *= pgsz; + return 0; + } + + if (!strcmp(var, "core.packedgitlimit")) { + packed_git_limit = git_config_int(var, value); + return 0; + } + + if (!strcmp(var, "user.name")) { + strlcpy(git_default_name, value, sizeof(git_default_name)); + return 0; + } + + if (!strcmp(var, "user.email")) { + strlcpy(git_default_email, value, sizeof(git_default_email)); + return 0; + } + + if (!strcmp(var, "i18n.commitencoding")) { + git_commit_encoding = strdup(value); + return 0; + } + + if (!strcmp(var, "i18n.logoutputencoding")) { + git_log_output_encoding = strdup(value); + return 0; + } + + + if (!strcmp(var, "pager.color") || !strcmp(var, "color.pager")) { + pager_use_color = git_config_bool(var,value); + return 0; + } + + /* Add other config variables here and to Documentation/config.txt. */ + return 0; +} + +int git_config_from_file(config_fn_t fn, const char *filename) +{ + int ret; + FILE *f = fopen(filename, "r"); + + ret = -1; + if (f) { + config_file = f; + config_file_name = filename; + config_linenr = 1; + ret = git_parse_file(fn); + fclose(f); + config_file_name = NULL; + } + return ret; +} + +int git_config(config_fn_t fn) +{ + int ret = 0; + char *repo_config = NULL; + const char *home = NULL, *filename; + + /* $GIT_CONFIG makes git read _only_ the given config file, + * $GIT_CONFIG_LOCAL will make it process it in addition to the + * global config file, the same way it would the per-repository + * config file otherwise. */ + filename = getenv(CONFIG_ENVIRONMENT); + if (!filename) { + home = getenv("HOME"); + filename = getenv(CONFIG_LOCAL_ENVIRONMENT); + if (!filename) + filename = repo_config = xstrdup(git_path("config")); + } + + if (home) { + char *user_config = xstrdup(mkpath("%s/.gitconfig", home)); + if (!access(user_config, R_OK)) + ret = git_config_from_file(fn, user_config); + free(user_config); + } + + ret += git_config_from_file(fn, filename); + free(repo_config); + return ret; +} + +/* + * Find all the stuff for git_config_set() below. + */ + +#define MAX_MATCHES 512 + +static struct { + int baselen; + char* key; + int do_not_match; + regex_t* value_regex; + int multi_replace; + off_t offset[MAX_MATCHES]; + enum { START, SECTION_SEEN, SECTION_END_SEEN, KEY_SEEN } state; + int seen; +} store; + +static int matches(const char* key, const char* value) +{ + return !strcmp(key, store.key) && + (store.value_regex == NULL || + (store.do_not_match ^ + !regexec(store.value_regex, value, 0, NULL, 0))); +} + +static int store_aux(const char* key, const char* value) +{ + switch (store.state) { + case KEY_SEEN: + if (matches(key, value)) { + if (store.seen == 1 && store.multi_replace == 0) { + fprintf(stderr, + "Warning: %s has multiple values\n", + key); + } else if (store.seen >= MAX_MATCHES) { + fprintf(stderr, "Too many matches\n"); + return 1; + } + + store.offset[store.seen] = ftell(config_file); + store.seen++; + } + break; + case SECTION_SEEN: + if (strncmp(key, store.key, store.baselen+1)) { + store.state = SECTION_END_SEEN; + break; + } else + /* do not increment matches: this is no match */ + store.offset[store.seen] = ftell(config_file); + /* fallthru */ + case SECTION_END_SEEN: + case START: + if (matches(key, value)) { + store.offset[store.seen] = ftell(config_file); + store.state = KEY_SEEN; + store.seen++; + } else { + if (strrchr(key, '.') - key == store.baselen && + !strncmp(key, store.key, store.baselen)) { + store.state = SECTION_SEEN; + store.offset[store.seen] = ftell(config_file); + } + } + } + return 0; +} + +static int write_error() +{ + fprintf(stderr, "Failed to write new configuration file\n"); + + /* Same error code as "failed to rename". */ + return 4; +} + +static int store_write_section(int fd, const char* key) +{ + const char *dot = strchr(key, '.'); + int len1 = store.baselen, len2 = -1; + + dot = strchr(key, '.'); + if (dot) { + int dotlen = dot - key; + if (dotlen < len1) { + len2 = len1 - dotlen - 1; + len1 = dotlen; + } + } + + if (write_in_full(fd, "[", 1) != 1 || + write_in_full(fd, key, len1) != len1) + return 0; + if (len2 >= 0) { + if (write_in_full(fd, " \"", 2) != 2) + return 0; + while (--len2 >= 0) { + unsigned char c = *++dot; + if (c == '"') + if (write_in_full(fd, "\\", 1) != 1) + return 0; + if (write_in_full(fd, &c, 1) != 1) + return 0; + } + if (write_in_full(fd, "\"", 1) != 1) + return 0; + } + if (write_in_full(fd, "]\n", 2) != 2) + return 0; + + return 1; +} + +static int store_write_pair(int fd, const char* key, const char* value) +{ + int i; + int length = strlen(key+store.baselen+1); + int quote = 0; + + /* Check to see if the value needs to be quoted. */ + if (value[0] == ' ') + quote = 1; + for (i = 0; value[i]; i++) + if (value[i] == ';' || value[i] == '#') + quote = 1; + if (value[i-1] == ' ') + quote = 1; + + if (write_in_full(fd, "\t", 1) != 1 || + write_in_full(fd, key+store.baselen+1, length) != length || + write_in_full(fd, " = ", 3) != 3) + return 0; + if (quote && write_in_full(fd, "\"", 1) != 1) + return 0; + for (i = 0; value[i]; i++) + switch (value[i]) { + case '\n': + if (write_in_full(fd, "\\n", 2) != 2) + return 0; + break; + case '\t': + if (write_in_full(fd, "\\t", 2) != 2) + return 0; + break; + case '"': + case '\\': + if (write_in_full(fd, "\\", 1) != 1) + return 0; + default: + if (write_in_full(fd, value+i, 1) != 1) + return 0; + break; + } + if (quote && write_in_full(fd, "\"", 1) != 1) + return 0; + if (write_in_full(fd, "\n", 1) != 1) + return 0; + return 1; +} + +static int find_beginning_of_line(const char* contents, int size, + int offset_, int* found_bracket) +{ + int equal_offset = size, bracket_offset = size; + int offset; + + for (offset = offset_-2; offset > 0 + && contents[offset] != '\n'; offset--) + switch (contents[offset]) { + case '=': equal_offset = offset; break; + case ']': bracket_offset = offset; break; + } + if (bracket_offset < equal_offset) { + *found_bracket = 1; + offset = bracket_offset+1; + } else + offset++; + + return offset; +} + +int git_config_set(const char* key, const char* value) +{ + return git_config_set_multivar(key, value, NULL, 0); +} + +/* + * If value==NULL, unset in (remove from) config, + * if value_regex!=NULL, disregard key/value pairs where value does not match. + * if multi_replace==0, nothing, or only one matching key/value is replaced, + * else all matching key/values (regardless how many) are removed, + * before the new pair is written. + * + * Returns 0 on success. + * + * This function does this: + * + * - it locks the config file by creating ".git/config.lock" + * + * - it then parses the config using store_aux() as validator to find + * the position on the key/value pair to replace. If it is to be unset, + * it must be found exactly once. + * + * - the config file is mmap()ed and the part before the match (if any) is + * written to the lock file, then the changed part and the rest. + * + * - the config file is removed and the lock file rename()d to it. + * + */ +int git_config_set_multivar(const char* key, const char* value, + const char* value_regex, int multi_replace) +{ + int i, dot; + int fd = -1, in_fd; + int ret; + char* config_filename; + char* lock_file; + const char* last_dot = strrchr(key, '.'); + + config_filename = getenv(CONFIG_ENVIRONMENT); + if (!config_filename) { + config_filename = getenv(CONFIG_LOCAL_ENVIRONMENT); + if (!config_filename) + config_filename = git_path("config"); + } + config_filename = xstrdup(config_filename); + lock_file = xstrdup(mkpath("%s.lock", config_filename)); + + /* + * Since "key" actually contains the section name and the real + * key name separated by a dot, we have to know where the dot is. + */ + + if (last_dot == NULL) { + fprintf(stderr, "key does not contain a section: %s\n", key); + ret = 2; + goto out_free; + } + store.baselen = last_dot - key; + + store.multi_replace = multi_replace; + + /* + * Validate the key and while at it, lower case it for matching. + */ + store.key = xmalloc(strlen(key) + 1); + dot = 0; + for (i = 0; key[i]; i++) { + unsigned char c = key[i]; + if (c == '.') + dot = 1; + /* Leave the extended basename untouched.. */ + if (!dot || i > store.baselen) { + if (!iskeychar(c) || (i == store.baselen+1 && !isalpha(c))) { + fprintf(stderr, "invalid key: %s\n", key); + free(store.key); + ret = 1; + goto out_free; + } + c = tolower(c); + } else if (c == '\n') { + fprintf(stderr, "invalid key (newline): %s\n", key); + free(store.key); + ret = 1; + goto out_free; + } + store.key[i] = c; + } + store.key[i] = 0; + + /* + * The lock_file serves a purpose in addition to locking: the new + * contents of .git/config will be written into it. + */ + fd = open(lock_file, O_WRONLY | O_CREAT | O_EXCL, 0666); + if (fd < 0 || adjust_shared_perm(lock_file)) { + fprintf(stderr, "could not lock config file\n"); + free(store.key); + ret = -1; + goto out_free; + } + + /* + * If .git/config does not exist yet, write a minimal version. + */ + in_fd = open(config_filename, O_RDONLY); + if ( in_fd < 0 ) { + free(store.key); + + if ( ENOENT != errno ) { + error("opening %s: %s", config_filename, + strerror(errno)); + ret = 3; /* same as "invalid config file" */ + goto out_free; + } + /* if nothing to unset, error out */ + if (value == NULL) { + ret = 5; + goto out_free; + } + + store.key = (char*)key; + if (!store_write_section(fd, key) || + !store_write_pair(fd, key, value)) + goto write_err_out; + } else { + struct stat st; + char* contents; + int i, copy_begin, copy_end, new_line = 0; + + if (value_regex == NULL) + store.value_regex = NULL; + else { + if (value_regex[0] == '!') { + store.do_not_match = 1; + value_regex++; + } else + store.do_not_match = 0; + + store.value_regex = (regex_t*)xmalloc(sizeof(regex_t)); + if (regcomp(store.value_regex, value_regex, + REG_EXTENDED)) { + fprintf(stderr, "Invalid pattern: %s\n", + value_regex); + free(store.value_regex); + ret = 6; + goto out_free; + } + } + + store.offset[0] = 0; + store.state = START; + store.seen = 0; + + /* + * After this, store.offset will contain the *end* offset + * of the last match, or remain at 0 if no match was found. + * As a side effect, we make sure to transform only a valid + * existing config file. + */ + if (git_config_from_file(store_aux, config_filename)) { + fprintf(stderr, "invalid config file\n"); + free(store.key); + if (store.value_regex != NULL) { + regfree(store.value_regex); + free(store.value_regex); + } + ret = 3; + goto out_free; + } + + free(store.key); + if (store.value_regex != NULL) { + regfree(store.value_regex); + free(store.value_regex); + } + + /* if nothing to unset, or too many matches, error out */ + if ((store.seen == 0 && value == NULL) || + (store.seen > 1 && multi_replace == 0)) { + ret = 5; + goto out_free; + } + + fstat(in_fd, &st); + contents = xmmap(NULL, st.st_size, PROT_READ, + MAP_PRIVATE, in_fd, 0); + close(in_fd); + + if (store.seen == 0) + store.seen = 1; + + for (i = 0, copy_begin = 0; i < store.seen; i++) { + if (store.offset[i] == 0) { + store.offset[i] = copy_end = st.st_size; + } else if (store.state != KEY_SEEN) { + copy_end = store.offset[i]; + } else + copy_end = find_beginning_of_line( + contents, st.st_size, + store.offset[i]-2, &new_line); + + /* write the first part of the config */ + if (copy_end > copy_begin) { + if (write_in_full(fd, contents + copy_begin, + copy_end - copy_begin) < + copy_end - copy_begin) + goto write_err_out; + if (new_line && + write_in_full(fd, "\n", 1) != 1) + goto write_err_out; + } + copy_begin = store.offset[i]; + } + + /* write the pair (value == NULL means unset) */ + if (value != NULL) { + if (store.state == START) { + if (!store_write_section(fd, key)) + goto write_err_out; + } + if (!store_write_pair(fd, key, value)) + goto write_err_out; + } + + /* write the rest of the config */ + if (copy_begin < st.st_size) + if (write_in_full(fd, contents + copy_begin, + st.st_size - copy_begin) < + st.st_size - copy_begin) + goto write_err_out; + + munmap(contents, st.st_size); + unlink(config_filename); + } + + if (rename(lock_file, config_filename) < 0) { + fprintf(stderr, "Could not rename the lock file?\n"); + ret = 4; + goto out_free; + } + + ret = 0; + +out_free: + if (0 <= fd) + close(fd); + free(config_filename); + if (lock_file) { + unlink(lock_file); + free(lock_file); + } + return ret; + +write_err_out: + ret = write_error(); + goto out_free; + +} + +int git_config_rename_section(const char *old_name, const char *new_name) +{ + int ret = 0; + char *config_filename; + struct lock_file *lock = xcalloc(sizeof(struct lock_file), 1); + int out_fd; + char buf[1024]; + + config_filename = getenv(CONFIG_ENVIRONMENT); + if (!config_filename) { + config_filename = getenv(CONFIG_LOCAL_ENVIRONMENT); + if (!config_filename) + config_filename = git_path("config"); + } + config_filename = xstrdup(config_filename); + out_fd = hold_lock_file_for_update(lock, config_filename, 0); + if (out_fd < 0) { + ret = error("Could not lock config file!"); + goto out; + } + + if (!(config_file = fopen(config_filename, "rb"))) { + ret = error("Could not open config file!"); + goto out; + } + + while (fgets(buf, sizeof(buf), config_file)) { + int i; + int length; + for (i = 0; buf[i] && isspace(buf[i]); i++) + ; /* do nothing */ + if (buf[i] == '[') { + /* it's a section */ + int j = 0, dot = 0; + for (i++; buf[i] && buf[i] != ']'; i++) { + if (!dot && isspace(buf[i])) { + dot = 1; + if (old_name[j++] != '.') + break; + for (i++; isspace(buf[i]); i++) + ; /* do nothing */ + if (buf[i] != '"') + break; + continue; + } + if (buf[i] == '\\' && dot) + i++; + else if (buf[i] == '"' && dot) { + for (i++; isspace(buf[i]); i++) + ; /* do_nothing */ + break; + } + if (buf[i] != old_name[j++]) + break; + } + if (buf[i] == ']' && old_name[j] == 0) { + /* old_name matches */ + ret++; + store.baselen = strlen(new_name); + if (!store_write_section(out_fd, new_name)) { + ret = write_error(); + goto out; + } + continue; + } + } + length = strlen(buf); + if (write_in_full(out_fd, buf, length) != length) { + ret = write_error(); + goto out; + } + } + fclose(config_file); + if (close(out_fd) || commit_lock_file(lock) < 0) + ret = error("Cannot commit config file!"); + out: + free(config_filename); + return ret; +} + diff --git a/config.mak.in b/config.mak.in new file mode 100644 index 0000000000..9a578405d8 --- /dev/null +++ b/config.mak.in @@ -0,0 +1,40 @@ +# git Makefile configuration, included in main Makefile +# @configure_input@ + +CC = @CC@ +CFLAGS = @CFLAGS@ +AR = @AR@ +TAR = @TAR@ +#INSTALL = @INSTALL@ # needs install-sh or install.sh in sources + +prefix = @prefix@ +exec_prefix = @exec_prefix@ +bindir = @bindir@ +#gitexecdir = @libexecdir@/git-core/ +datarootdir = @datarootdir@ +template_dir = @datadir@/git-core/templates/ + +mandir=@mandir@ + +srcdir = @srcdir@ +VPATH = @srcdir@ + +export exec_prefix mandir +export srcdir VPATH + +NEEDS_SSL_WITH_CRYPTO=@NEEDS_SSL_WITH_CRYPTO@ +NO_OPENSSL=@NO_OPENSSL@ +NO_CURL=@NO_CURL@ +NO_EXPAT=@NO_EXPAT@ +NEEDS_LIBICONV=@NEEDS_LIBICONV@ +NEEDS_SOCKET=@NEEDS_SOCKET@ +NO_D_INO_IN_DIRENT=@NO_D_INO_IN_DIRENT@ +NO_D_TYPE_IN_DIRENT=@NO_D_TYPE_IN_DIRENT@ +NO_SOCKADDR_STORAGE=@NO_SOCKADDR_STORAGE@ +NO_IPV6=@NO_IPV6@ +NO_C99_FORMAT=@NO_C99_FORMAT@ +NO_STRCASESTR=@NO_STRCASESTR@ +NO_STRLCPY=@NO_STRLCPY@ +NO_SETENV=@NO_SETENV@ +NO_ICONV=@NO_ICONV@ + diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000000..7cfb3a0666 --- /dev/null +++ b/configure.ac @@ -0,0 +1,332 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ(2.59) +AC_INIT([git], [@@GIT_VERSION@@], [git@vger.kernel.org]) + +AC_CONFIG_SRCDIR([git.c]) + +config_file=config.mak.autogen +config_append=config.mak.append +config_in=config.mak.in + +echo "# ${config_append}. Generated by configure." > "${config_append}" + + +## Definitions of macros +# GIT_CONF_APPEND_LINE(LINE) +# -------------------------- +# Append LINE to file ${config_append} +AC_DEFUN([GIT_CONF_APPEND_LINE], +[echo "$1" >> "${config_append}"])# GIT_CONF_APPEND_LINE +# +# GIT_ARG_SET_PATH(PROGRAM) +# ------------------------- +# Provide --with-PROGRAM=PATH option to set PATH to PROGRAM +AC_DEFUN([GIT_ARG_SET_PATH], +[AC_ARG_WITH([$1], + [AS_HELP_STRING([--with-$1=PATH], + [provide PATH to $1])], + [GIT_CONF_APPEND_PATH($1)],[]) +])# GIT_ARG_SET_PATH +# +# GIT_CONF_APPEND_PATH(PROGRAM) +# ------------------------------ +# Parse --with-PROGRAM=PATH option to set PROGRAM_PATH=PATH +# Used by GIT_ARG_SET_PATH(PROGRAM) +AC_DEFUN([GIT_CONF_APPEND_PATH], +[PROGRAM=m4_toupper($1); \ +if test "$withval" = "no"; then \ + AC_MSG_ERROR([You cannot use git without $1]); \ +else \ + if test "$withval" = "yes"; then \ + AC_MSG_WARN([You should provide path for --with-$1=PATH]); \ + else \ + GIT_CONF_APPEND_LINE(${PROGRAM}_PATH=$withval); \ + fi; \ +fi; \ +]) # GIT_CONF_APPEND_PATH +# +# GIT_PARSE_WITH(PACKAGE) +# ----------------------- +# For use in AC_ARG_WITH action-if-found, for packages default ON. +# * Set NO_PACKAGE=YesPlease for --without-PACKAGE +# * Set PACKAGEDIR=PATH for --with-PACKAGE=PATH +# * Unset NO_PACKAGE for --with-PACKAGE without ARG +AC_DEFUN([GIT_PARSE_WITH], +[PACKAGE=m4_toupper($1); \ +if test "$withval" = "no"; then \ + m4_toupper(NO_$1)=YesPlease; \ +elif test "$withval" = "yes"; then \ + m4_toupper(NO_$1)=; \ +else \ + m4_toupper(NO_$1)=; \ + GIT_CONF_APPEND_LINE(${PACKAGE}DIR=$withval); \ +fi \ +])# GIT_PARSE_WITH + + +## Site configuration related to programs (before tests) +## --with-PACKAGE[=ARG] and --without-PACKAGE +# +# Define SHELL_PATH to provide path to shell. +GIT_ARG_SET_PATH(shell) +# +# Define PERL_PATH to provide path to Perl. +GIT_ARG_SET_PATH(perl) +# + + +## Checks for programs. +AC_MSG_NOTICE([CHECKS for programs]) +# +AC_PROG_CC([cc gcc]) +#AC_PROG_INSTALL # needs install-sh or install.sh in sources +AC_CHECK_TOOL(AR, ar, :) +AC_CHECK_PROGS(TAR, [gtar tar]) + +## Checks for libraries. +AC_MSG_NOTICE([CHECKS for libraries]) +# +# Define NO_OPENSSL environment variable if you do not have OpenSSL. +# Define NEEDS_SSL_WITH_CRYPTO if you need -lcrypto with -lssl (Darwin). +AC_CHECK_LIB([crypto], [SHA1_Init], +[NEEDS_SSL_WITH_CRYPTO=], +[AC_CHECK_LIB([ssl], [SHA1_Init], + [NEEDS_SSL_WITH_CRYPTO=YesPlease + NEEDS_SSL_WITH_CRYPTO=], + [NO_OPENSSL=YesPlease])]) +AC_SUBST(NEEDS_SSL_WITH_CRYPTO) +AC_SUBST(NO_OPENSSL) +# +# Define NO_CURL if you do not have curl installed. git-http-pull and +# git-http-push are not built, and you cannot use http:// and https:// +# transports. +AC_CHECK_LIB([curl], [curl_global_init], +[NO_CURL=], +[NO_CURL=YesPlease]) +AC_SUBST(NO_CURL) +# +# Define NO_EXPAT if you do not have expat installed. git-http-push is +# not built, and you cannot push using http:// and https:// transports. +AC_CHECK_LIB([expat], [XML_ParserCreate], +[NO_EXPAT=], +[NO_EXPAT=YesPlease]) +AC_SUBST(NO_EXPAT) +# +# Define NEEDS_LIBICONV if linking with libc is not enough (Darwin). +# Define NO_ICONV if neither libc nor libiconv support iconv. +AC_CHECK_LIB([c], [iconv], + [NEEDS_LIBICONV=], + AC_CHECK_LIB([iconv], [iconv], + [NEEDS_LIBICONV=YesPlease], + [NO_ICONV=YesPlease])) +AC_SUBST(NEEDS_LIBICONV) +AC_SUBST(NO_ICONV) +test -n "$NEEDS_LIBICONV" && LIBS="$LIBS -liconv" +# +# Define NEEDS_SOCKET if linking with libc is not enough (SunOS, +# Patrick Mauritz). +AC_CHECK_LIB([c], [socket], +[NEEDS_SOCKET=], +[NEEDS_SOCKET=YesPlease]) +AC_SUBST(NEEDS_SOCKET) +test -n "$NEEDS_SOCKET" && LIBS="$LIBS -lsocket" + + +## Checks for header files. + + +## Checks for typedefs, structures, and compiler characteristics. +AC_MSG_NOTICE([CHECKS for typedefs, structures, and compiler characteristics]) +# +# Define NO_D_INO_IN_DIRENT if you don't have d_ino in your struct dirent. +AC_CHECK_MEMBER(struct dirent.d_ino, +[NO_D_INO_IN_DIRENT=], +[NO_D_INO_IN_DIRENT=YesPlease], +[#include <dirent.h>]) +AC_SUBST(NO_D_INO_IN_DIRENT) +# +# Define NO_D_TYPE_IN_DIRENT if your platform defines DT_UNKNOWN but lacks +# d_type in struct dirent (latest Cygwin -- will be fixed soonish). +AC_CHECK_MEMBER(struct dirent.d_type, +[NO_D_TYPE_IN_DIRENT=], +[NO_D_TYPE_IN_DIRENT=YesPlease], +[#include <dirent.h>]) +AC_SUBST(NO_D_TYPE_IN_DIRENT) +# +# Define NO_SOCKADDR_STORAGE if your platform does not have struct +# sockaddr_storage. +AC_CHECK_TYPE(struct sockaddr_storage, +[NO_SOCKADDR_STORAGE=], +[NO_SOCKADDR_STORAGE=YesPlease],[ +#include <sys/types.h> +#include <sys/socket.h> +]) +AC_SUBST(NO_SOCKADDR_STORAGE) +# +# Define NO_IPV6 if you lack IPv6 support and getaddrinfo(). +AC_CHECK_TYPE([struct addrinfo],[ + AC_CHECK_FUNC([getaddrinfo], + [NO_IPV6=], + [NO_IPV6=YesPlease]) +],[NO_IPV6=YesPlease],[ +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +]) +AC_SUBST(NO_IPV6) +# +# Define NO_C99_FORMAT if your formatted IO functions (printf/scanf et.al.) +# do not support the 'size specifiers' introduced by C99, namely ll, hh, +# j, z, t. (representing long long int, char, intmax_t, size_t, ptrdiff_t). +# some C compilers supported these specifiers prior to C99 as an extension. +AC_CACHE_CHECK([whether formatted IO functions support C99 size specifiers], + [ac_cv_c_c99_format], +[# Actually git uses only %z (%zu) in alloc.c, and %t (%td) in mktag.c +AC_RUN_IFELSE( + [AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT], + [[char buf[64]; + if (sprintf(buf, "%lld%hhd%jd%zd%td", (long long int)1, (char)2, (intmax_t)3, (size_t)4, (ptrdiff_t)5) != 5) + exit(1); + else if (strcmp(buf, "12345")) + exit(2);]])], + [ac_cv_c_c99_format=yes], + [ac_cv_c_c99_format=no]) +]) +if test $ac_cv_c_c99_format = no; then + NO_C99_FORMAT=YesPlease +else + NO_C99_FORMAT= +fi +AC_SUBST(NO_C99_FORMAT) + + +## Checks for library functions. +## (in default C library and libraries checked by AC_CHECK_LIB) +AC_MSG_NOTICE([CHECKS for library functions]) +# +# Define NO_STRCASESTR if you don't have strcasestr. +AC_CHECK_FUNC(strcasestr, +[NO_STRCASESTR=], +[NO_STRCASESTR=YesPlease]) +AC_SUBST(NO_STRCASESTR) +# +# Define NO_STRLCPY if you don't have strlcpy. +AC_CHECK_FUNC(strlcpy, +[NO_STRLCPY=], +[NO_STRLCPY=YesPlease]) +AC_SUBST(NO_STRLCPY) +# +# Define NO_SETENV if you don't have setenv in the C library. +AC_CHECK_FUNC(setenv, +[NO_SETENV=], +[NO_SETENV=YesPlease]) +AC_SUBST(NO_SETENV) +# +# Define NO_MMAP if you want to avoid mmap. +# +# Define NO_ICONV if your libc does not properly support iconv. + + +## Other checks. +# Define USE_PIC if you need the main git objects to be built with -fPIC +# in order to build and link perl/Git.so. x86-64 seems to need this. +# +# Define NO_SYMLINK_HEAD if you never want .git/HEAD to be a symbolic link. +# Enable it on Windows. By default, symrefs are still used. + +## Site configuration (override autodetection) +## --with-PACKAGE[=ARG] and --without-PACKAGE +AC_MSG_NOTICE([CHECKS for site configuration]) +# +# Define NO_SVN_TESTS if you want to skip time-consuming SVN interoperability +# tests. These tests take up a significant amount of the total test time +# but are not needed unless you plan to talk to SVN repos. +# +# Define MOZILLA_SHA1 environment variable when running make to make use of +# a bundled SHA1 routine coming from Mozilla. It is GPL'd and should be fast +# on non-x86 architectures (e.g. PowerPC), while the OpenSSL version (default +# choice) has very fast version optimized for i586. +# +# Define PPC_SHA1 environment variable when running make to make use of +# a bundled SHA1 routine optimized for PowerPC. +# +# Define ARM_SHA1 environment variable when running make to make use of +# a bundled SHA1 routine optimized for ARM. +# +# Define NO_OPENSSL environment variable if you do not have OpenSSL. +# This also implies MOZILLA_SHA1. +# +# Define OPENSSLDIR=/foo/bar if your openssl header and library files are in +# /foo/bar/include and /foo/bar/lib directories. +AC_ARG_WITH(openssl, +AS_HELP_STRING([--with-openssl],[use OpenSSL library (default is YES)]) +AS_HELP_STRING([], [ARG can be prefix for openssl library and headers]),\ +GIT_PARSE_WITH(openssl)) +# +# Define NO_CURL if you do not have curl installed. git-http-pull and +# git-http-push are not built, and you cannot use http:// and https:// +# transports. +# +# Define CURLDIR=/foo/bar if your curl header and library files are in +# /foo/bar/include and /foo/bar/lib directories. +AC_ARG_WITH(curl, +AS_HELP_STRING([--with-curl],[support http(s):// transports (default is YES)]) +AS_HELP_STRING([], [ARG can be also prefix for curl library and headers]), +GIT_PARSE_WITH(curl)) +# +# Define NO_EXPAT if you do not have expat installed. git-http-push is +# not built, and you cannot push using http:// and https:// transports. +# +# Define EXPATDIR=/foo/bar if your expat header and library files are in +# /foo/bar/include and /foo/bar/lib directories. +AC_ARG_WITH(expat, +AS_HELP_STRING([--with-expat], +[support git-push using http:// and https:// transports via WebDAV (default is YES)]) +AS_HELP_STRING([], [ARG can be also prefix for expat library and headers]), +GIT_PARSE_WITH(expat)) +# +# Define NO_FINK if you are building on Darwin/Mac OS X, have Fink +# installed in /sw, but don't want GIT to link against any libraries +# installed there. If defined you may specify your own (or Fink's) +# include directories and library directories by defining CFLAGS +# and LDFLAGS appropriately. +# +# Define NO_DARWIN_PORTS if you are building on Darwin/Mac OS X, +# have DarwinPorts installed in /opt/local, but don't want GIT to +# link against any libraries installed there. If defined you may +# specify your own (or DarwinPort's) include directories and +# library directories by defining CFLAGS and LDFLAGS appropriately. +# +# Define NO_MMAP if you want to avoid mmap. +# +# Define NO_ICONV if your libc does not properly support iconv. +AC_ARG_WITH(iconv, +AS_HELP_STRING([--without-iconv], +[if your architecture doesn't properly support iconv]) +AS_HELP_STRING([--with-iconv=PATH], +[PATH is prefix for libiconv library and headers]) +AS_HELP_STRING([], +[used only if you need linking with libiconv]), +GIT_PARSE_WITH(iconv)) + +## --enable-FEATURE[=ARG] and --disable-FEATURE +# +# Define USE_NSEC below if you want git to care about sub-second file mtimes +# and ctimes. Note that you need recent glibc (at least 2.2.4) for this, and +# it will BREAK YOUR LOCAL DIFFS! show-diff and anything using it will likely +# randomly break unless your underlying filesystem supports those sub-second +# times (my ext3 doesn't). +# +# Define USE_STDEV below if you want git to care about the underlying device +# change being considered an inode change from the update-cache perspective. + + +## Output files +AC_CONFIG_FILES(["${config_file}":"${config_in}":"${config_append}"]) +AC_OUTPUT + + +## Cleanup +rm -f "${config_append}" diff --git a/connect.c b/connect.c new file mode 100644 index 0000000000..78448889da --- /dev/null +++ b/connect.c @@ -0,0 +1,805 @@ +#include "git-compat-util.h" +#include "cache.h" +#include "pkt-line.h" +#include "quote.h" +#include "refs.h" + +static char *server_capabilities; + +static int check_ref(const char *name, int len, unsigned int flags) +{ + if (!flags) + return 1; + + if (len < 5 || memcmp(name, "refs/", 5)) + return 0; + + /* Skip the "refs/" part */ + name += 5; + len -= 5; + + /* REF_NORMAL means that we don't want the magic fake tag refs */ + if ((flags & REF_NORMAL) && check_ref_format(name) < 0) + return 0; + + /* REF_HEADS means that we want regular branch heads */ + if ((flags & REF_HEADS) && !memcmp(name, "heads/", 6)) + return 1; + + /* REF_TAGS means that we want tags */ + if ((flags & REF_TAGS) && !memcmp(name, "tags/", 5)) + return 1; + + /* All type bits clear means that we are ok with anything */ + return !(flags & ~REF_NORMAL); +} + +/* + * Read all the refs from the other end + */ +struct ref **get_remote_heads(int in, struct ref **list, + int nr_match, char **match, + unsigned int flags) +{ + *list = NULL; + for (;;) { + struct ref *ref; + unsigned char old_sha1[20]; + static char buffer[1000]; + char *name; + int len, name_len; + + len = packet_read_line(in, buffer, sizeof(buffer)); + if (!len) + break; + if (buffer[len-1] == '\n') + buffer[--len] = 0; + + if (len < 42 || get_sha1_hex(buffer, old_sha1) || buffer[40] != ' ') + die("protocol error: expected sha/ref, got '%s'", buffer); + name = buffer + 41; + + name_len = strlen(name); + if (len != name_len + 41) { + if (server_capabilities) + free(server_capabilities); + server_capabilities = xstrdup(name + name_len + 1); + } + + if (!check_ref(name, name_len, flags)) + continue; + if (nr_match && !path_match(name, nr_match, match)) + continue; + ref = xcalloc(1, sizeof(*ref) + len - 40); + hashcpy(ref->old_sha1, old_sha1); + memcpy(ref->name, buffer + 41, len - 40); + *list = ref; + list = &ref->next; + } + return list; +} + +int server_supports(const char *feature) +{ + return server_capabilities && + strstr(server_capabilities, feature) != NULL; +} + +int get_ack(int fd, unsigned char *result_sha1) +{ + static char line[1000]; + int len = packet_read_line(fd, line, sizeof(line)); + + if (!len) + die("git-fetch-pack: expected ACK/NAK, got EOF"); + if (line[len-1] == '\n') + line[--len] = 0; + if (!strcmp(line, "NAK")) + return 0; + if (!strncmp(line, "ACK ", 4)) { + if (!get_sha1_hex(line+4, result_sha1)) { + if (strstr(line+45, "continue")) + return 2; + return 1; + } + } + die("git-fetch_pack: expected ACK/NAK, got '%s'", line); +} + +int path_match(const char *path, int nr, char **match) +{ + int i; + int pathlen = strlen(path); + + for (i = 0; i < nr; i++) { + char *s = match[i]; + int len = strlen(s); + + if (!len || len > pathlen) + continue; + if (memcmp(path + pathlen - len, s, len)) + continue; + if (pathlen > len && path[pathlen - len - 1] != '/') + continue; + *s = 0; + return (i + 1); + } + return 0; +} + +struct refspec { + char *src; + char *dst; + char force; +}; + +/* + * A:B means fast forward remote B with local A. + * +A:B means overwrite remote B with local A. + * +A is a shorthand for +A:A. + * A is a shorthand for A:A. + * :B means delete remote B. + */ +static struct refspec *parse_ref_spec(int nr_refspec, char **refspec) +{ + int i; + struct refspec *rs = xcalloc(sizeof(*rs), (nr_refspec + 1)); + for (i = 0; i < nr_refspec; i++) { + char *sp, *dp, *ep; + sp = refspec[i]; + if (*sp == '+') { + rs[i].force = 1; + sp++; + } + ep = strchr(sp, ':'); + if (ep) { + dp = ep + 1; + *ep = 0; + } + else + dp = sp; + rs[i].src = sp; + rs[i].dst = dp; + } + rs[nr_refspec].src = rs[nr_refspec].dst = NULL; + return rs; +} + +static int count_refspec_match(const char *pattern, + struct ref *refs, + struct ref **matched_ref) +{ + int patlen = strlen(pattern); + struct ref *matched_weak = NULL; + struct ref *matched = NULL; + int weak_match = 0; + int match = 0; + + for (weak_match = match = 0; refs; refs = refs->next) { + char *name = refs->name; + int namelen = strlen(name); + int weak_match; + + if (namelen < patlen || + memcmp(name + namelen - patlen, pattern, patlen)) + continue; + if (namelen != patlen && name[namelen - patlen - 1] != '/') + continue; + + /* A match is "weak" if it is with refs outside + * heads or tags, and did not specify the pattern + * in full (e.g. "refs/remotes/origin/master") or at + * least from the toplevel (e.g. "remotes/origin/master"); + * otherwise "git push $URL master" would result in + * ambiguity between remotes/origin/master and heads/master + * at the remote site. + */ + if (namelen != patlen && + patlen != namelen - 5 && + strncmp(name, "refs/heads/", 11) && + strncmp(name, "refs/tags/", 10)) { + /* We want to catch the case where only weak + * matches are found and there are multiple + * matches, and where more than one strong + * matches are found, as ambiguous. One + * strong match with zero or more weak matches + * are acceptable as a unique match. + */ + matched_weak = refs; + weak_match++; + } + else { + matched = refs; + match++; + } + } + if (!matched) { + *matched_ref = matched_weak; + return weak_match; + } + else { + *matched_ref = matched; + return match; + } +} + +static void link_dst_tail(struct ref *ref, struct ref ***tail) +{ + **tail = ref; + *tail = &ref->next; + **tail = NULL; +} + +static struct ref *try_explicit_object_name(const char *name) +{ + unsigned char sha1[20]; + struct ref *ref; + int len; + + if (!*name) { + ref = xcalloc(1, sizeof(*ref) + 20); + strcpy(ref->name, "(delete)"); + hashclr(ref->new_sha1); + return ref; + } + if (get_sha1(name, sha1)) + return NULL; + len = strlen(name) + 1; + ref = xcalloc(1, sizeof(*ref) + len); + memcpy(ref->name, name, len); + hashcpy(ref->new_sha1, sha1); + return ref; +} + +static int match_explicit_refs(struct ref *src, struct ref *dst, + struct ref ***dst_tail, struct refspec *rs) +{ + int i, errs; + for (i = errs = 0; rs[i].src; i++) { + struct ref *matched_src, *matched_dst; + + matched_src = matched_dst = NULL; + switch (count_refspec_match(rs[i].src, src, &matched_src)) { + case 1: + break; + case 0: + /* The source could be in the get_sha1() format + * not a reference name. :refs/other is a + * way to delete 'other' ref at the remote end. + */ + matched_src = try_explicit_object_name(rs[i].src); + if (matched_src) + break; + errs = 1; + error("src refspec %s does not match any.", + rs[i].src); + break; + default: + errs = 1; + error("src refspec %s matches more than one.", + rs[i].src); + break; + } + switch (count_refspec_match(rs[i].dst, dst, &matched_dst)) { + case 1: + break; + case 0: + if (!memcmp(rs[i].dst, "refs/", 5)) { + int len = strlen(rs[i].dst) + 1; + matched_dst = xcalloc(1, sizeof(*dst) + len); + memcpy(matched_dst->name, rs[i].dst, len); + link_dst_tail(matched_dst, dst_tail); + } + else if (!strcmp(rs[i].src, rs[i].dst) && + matched_src) { + /* pushing "master:master" when + * remote does not have master yet. + */ + int len = strlen(matched_src->name) + 1; + matched_dst = xcalloc(1, sizeof(*dst) + len); + memcpy(matched_dst->name, matched_src->name, + len); + link_dst_tail(matched_dst, dst_tail); + } + else { + errs = 1; + error("dst refspec %s does not match any " + "existing ref on the remote and does " + "not start with refs/.", rs[i].dst); + } + break; + default: + errs = 1; + error("dst refspec %s matches more than one.", + rs[i].dst); + break; + } + if (errs) + continue; + if (matched_dst->peer_ref) { + errs = 1; + error("dst ref %s receives from more than one src.", + matched_dst->name); + } + else { + matched_dst->peer_ref = matched_src; + matched_dst->force = rs[i].force; + } + } + return -errs; +} + +static struct ref *find_ref_by_name(struct ref *list, const char *name) +{ + for ( ; list; list = list->next) + if (!strcmp(list->name, name)) + return list; + return NULL; +} + +int match_refs(struct ref *src, struct ref *dst, struct ref ***dst_tail, + int nr_refspec, char **refspec, int all) +{ + struct refspec *rs = parse_ref_spec(nr_refspec, refspec); + + if (nr_refspec) + return match_explicit_refs(src, dst, dst_tail, rs); + + /* pick the remainder */ + for ( ; src; src = src->next) { + struct ref *dst_peer; + if (src->peer_ref) + continue; + dst_peer = find_ref_by_name(dst, src->name); + if ((dst_peer && dst_peer->peer_ref) || (!dst_peer && !all)) + continue; + if (!dst_peer) { + /* Create a new one and link it */ + int len = strlen(src->name) + 1; + dst_peer = xcalloc(1, sizeof(*dst_peer) + len); + memcpy(dst_peer->name, src->name, len); + hashcpy(dst_peer->new_sha1, src->new_sha1); + link_dst_tail(dst_peer, dst_tail); + } + dst_peer->peer_ref = src; + } + return 0; +} + +enum protocol { + PROTO_LOCAL = 1, + PROTO_SSH, + PROTO_GIT, +}; + +static enum protocol get_protocol(const char *name) +{ + if (!strcmp(name, "ssh")) + return PROTO_SSH; + if (!strcmp(name, "git")) + return PROTO_GIT; + if (!strcmp(name, "git+ssh")) + return PROTO_SSH; + if (!strcmp(name, "ssh+git")) + return PROTO_SSH; + die("I don't handle protocol '%s'", name); +} + +#define STR_(s) # s +#define STR(s) STR_(s) + +#ifndef NO_IPV6 + +/* + * Returns a connected socket() fd, or else die()s. + */ +static int git_tcp_connect_sock(char *host) +{ + int sockfd = -1, saved_errno = 0; + char *colon, *end; + const 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) { + *colon = 0; + 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) { + saved_errno = errno; + continue; + } + if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) { + saved_errno = errno; + close(sockfd); + sockfd = -1; + continue; + } + break; + } + + freeaddrinfo(ai0); + + if (sockfd < 0) + die("unable to connect a socket (%s)", strerror(saved_errno)); + + return sockfd; +} + +#else /* NO_IPV6 */ + +/* + * Returns a connected socket() fd, or else die()s. + */ +static int git_tcp_connect_sock(char *host) +{ + int sockfd = -1, saved_errno = 0; + char *colon, *end; + char *port = STR(DEFAULT_GIT_PORT), *ep; + struct hostent *he; + struct sockaddr_in sa; + char **ap; + unsigned int nport; + + if (host[0] == '[') { + end = strchr(host + 1, ']'); + if (end) { + *end = 0; + end++; + host++; + } else + end = host; + } else + end = host; + colon = strchr(end, ':'); + + if (colon) { + *colon = 0; + port = colon + 1; + } + + he = gethostbyname(host); + if (!he) + die("Unable to look up %s (%s)", host, hstrerror(h_errno)); + nport = strtoul(port, &ep, 10); + if ( ep == port || *ep ) { + /* Not numeric */ + struct servent *se = getservbyname(port,"tcp"); + if ( !se ) + die("Unknown port %s\n", port); + nport = se->s_port; + } + + for (ap = he->h_addr_list; *ap; ap++) { + sockfd = socket(he->h_addrtype, SOCK_STREAM, 0); + if (sockfd < 0) { + saved_errno = errno; + continue; + } + + memset(&sa, 0, sizeof sa); + sa.sin_family = he->h_addrtype; + sa.sin_port = htons(nport); + memcpy(&sa.sin_addr, *ap, he->h_length); + + if (connect(sockfd, (struct sockaddr *)&sa, sizeof sa) < 0) { + saved_errno = errno; + close(sockfd); + sockfd = -1; + continue; + } + break; + } + + if (sockfd < 0) + die("unable to connect a socket (%s)", strerror(saved_errno)); + + return sockfd; +} + +#endif /* NO_IPV6 */ + + +static void git_tcp_connect(int fd[2], char *host) +{ + int sockfd = git_tcp_connect_sock(host); + + fd[0] = sockfd; + fd[1] = dup(sockfd); +} + + +static char *git_proxy_command; +static const char *rhost_name; +static int rhost_len; + +static int git_proxy_command_options(const char *var, const char *value) +{ + if (!strcmp(var, "core.gitproxy")) { + const char *for_pos; + int matchlen = -1; + int hostlen; + + if (git_proxy_command) + return 0; + /* [core] + * ;# matches www.kernel.org as well + * gitproxy = netcatter-1 for kernel.org + * gitproxy = netcatter-2 for sample.xz + * gitproxy = netcatter-default + */ + for_pos = strstr(value, " for "); + if (!for_pos) + /* matches everybody */ + matchlen = strlen(value); + else { + hostlen = strlen(for_pos + 5); + if (rhost_len < hostlen) + matchlen = -1; + else if (!strncmp(for_pos + 5, + rhost_name + rhost_len - hostlen, + hostlen) && + ((rhost_len == hostlen) || + rhost_name[rhost_len - hostlen -1] == '.')) + matchlen = for_pos - value; + else + matchlen = -1; + } + if (0 <= matchlen) { + /* core.gitproxy = none for kernel.org */ + if (matchlen == 4 && + !memcmp(value, "none", 4)) + matchlen = 0; + git_proxy_command = xmalloc(matchlen + 1); + memcpy(git_proxy_command, value, matchlen); + git_proxy_command[matchlen] = 0; + } + return 0; + } + + return git_default_config(var, value); +} + +static int git_use_proxy(const char *host) +{ + rhost_name = host; + rhost_len = strlen(host); + git_proxy_command = getenv("GIT_PROXY_COMMAND"); + git_config(git_proxy_command_options); + rhost_name = NULL; + return (git_proxy_command && *git_proxy_command); +} + +static void git_proxy_connect(int fd[2], char *host) +{ + const char *port = STR(DEFAULT_GIT_PORT); + char *colon, *end; + int pipefd[2][2]; + pid_t pid; + + if (host[0] == '[') { + end = strchr(host + 1, ']'); + if (end) { + *end = 0; + end++; + host++; + } else + end = host; + } else + end = host; + colon = strchr(end, ':'); + + if (colon) { + *colon = 0; + port = colon + 1; + } + + if (pipe(pipefd[0]) < 0 || pipe(pipefd[1]) < 0) + die("unable to create pipe pair for communication"); + pid = fork(); + if (!pid) { + dup2(pipefd[1][0], 0); + dup2(pipefd[0][1], 1); + close(pipefd[0][0]); + close(pipefd[0][1]); + close(pipefd[1][0]); + close(pipefd[1][1]); + 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]); +} + +#define MAX_CMD_LEN 1024 + +/* + * This returns 0 if the transport protocol does not need fork(2), + * or a process id if it does. Once done, finish the connection + * with finish_connect() with the value returned from this function + * (it is safe to call finish_connect() with 0 to support the former + * case). + * + * Does not return a negative value on error; it just dies. + */ +pid_t git_connect(int fd[2], char *url, const char *prog) +{ + char *host, *path = url; + char *end; + int c; + int pipefd[2][2]; + pid_t pid; + enum protocol protocol = PROTO_LOCAL; + int free_path = 0; + + /* Without this we cannot rely on waitpid() to tell + * what happened to our children. + */ + signal(SIGCHLD, SIG_DFL); + + host = strstr(url, "://"); + if(host) { + *host = '\0'; + protocol = get_protocol(url); + host += 3; + c = '/'; + } else { + host = url; + c = ':'; + } + + if (host[0] == '[') { + end = strchr(host + 1, ']'); + if (end) { + *end = 0; + end++; + host++; + } else + end = host; + } else + end = host; + + path = strchr(end, c); + if (c == ':') { + if (path) { + protocol = PROTO_SSH; + *path++ = '\0'; + } else + path = host; + } + + if (!path || !*path) + die("No path specified. See 'man git-pull' for valid url syntax"); + + /* + * null-terminate hostname and point path to ~ for URL's like this: + * ssh://host.xz/~user/repo + */ + if (protocol != PROTO_LOCAL && host != url) { + char *ptr = path; + if (path[1] == '~') + path++; + else { + path = xstrdup(ptr); + free_path = 1; + } + + *ptr = '\0'; + } + + if (protocol == PROTO_GIT) { + /* These underlying connection commands die() if they + * cannot connect. + */ + char *target_host = xstrdup(host); + if (git_use_proxy(host)) + git_proxy_connect(fd, host); + else + git_tcp_connect(fd, host); + /* + * 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 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) { + char command[MAX_CMD_LEN]; + char *posn = command; + int size = MAX_CMD_LEN; + int of = 0; + + of |= add_to_string(&posn, &size, prog, 0); + of |= add_to_string(&posn, &size, " ", 0); + of |= add_to_string(&posn, &size, path, 1); + + if (of) + die("command line too long"); + + dup2(pipefd[1][0], 0); + dup2(pipefd[0][1], 1); + close(pipefd[0][0]); + close(pipefd[0][1]); + close(pipefd[1][0]); + close(pipefd[1][1]); + if (protocol == PROTO_SSH) { + const char *ssh, *ssh_basename; + ssh = getenv("GIT_SSH"); + if (!ssh) ssh = "ssh"; + ssh_basename = strrchr(ssh, '/'); + if (!ssh_basename) + ssh_basename = ssh; + else + ssh_basename++; + execlp(ssh, ssh_basename, host, command, NULL); + } + else { + unsetenv(ALTERNATE_DB_ENVIRONMENT); + unsetenv(DB_ENVIRONMENT); + unsetenv(GIT_DIR_ENVIRONMENT); + unsetenv(GRAFT_ENVIRONMENT); + unsetenv(INDEX_ENVIRONMENT); + execlp("sh", "sh", "-c", command, NULL); + } + die("exec failed"); + } + fd[0] = pipefd[0][0]; + fd[1] = pipefd[1][1]; + close(pipefd[0][1]); + close(pipefd[1][0]); + if (free_path) + free(path); + return pid; +} + +int finish_connect(pid_t pid) +{ + if (pid == 0) + return 0; + + while (waitpid(pid, NULL, 0) < 0) { + if (errno != EINTR) + return -1; + } + return 0; +} diff --git a/contrib/README b/contrib/README new file mode 100644 index 0000000000..e1c0a01ff3 --- /dev/null +++ b/contrib/README @@ -0,0 +1,44 @@ +Contributed Software + +Although these pieces are available as part of the official git +source tree, they are in somewhat different status. The +intention is to keep interesting tools around git here, maybe +even experimental ones, to give users an easier access to them, +and to give tools wider exposure, so that they can be improved +faster. + +I am not expecting to touch these myself that much. As far as +my day-to-day operation is concerned, these subdirectories are +owned by their respective primary authors. I am willing to help +if users of these components and the contrib/ subtree "owners" +have technical/design issues to resolve, but the initiative to +fix and/or enhance things _must_ be on the side of the subtree +owners. IOW, I won't be actively looking for bugs and rooms for +enhancements in them as the git maintainer -- I may only do so +just as one of the users when I want to scratch my own itch. If +you have patches to things in contrib/ area, the patch should be +first sent to the primary author, and then the primary author +should ack and forward it to me (git pull request is nicer). +This is the same way as how I have been treating gitk, and to a +lesser degree various foreign SCM interfaces, so you know the +drill. + +I expect that things that start their life in the contrib/ area +to graduate out of contrib/ once they mature, either by becoming +projects on their own, or moving to the toplevel directory. On +the other hand, I expect I'll be proposing removal of disused +and inactive ones from time to time. + +If you have new things to add to this area, please first propose +it on the git mailing list, and after a list discussion proves +there are some general interests (it does not have to be a +list-wide consensus for a tool targeted to a relatively narrow +audience -- for example I do not work with projects whose +upstream is svn, so I have no use for git-svn myself, but it is +of general interest for people who need to interoperate with SVN +repositories in a way git-svn works better than git-svnimport), +submit a patch to create a subdirectory of contrib/ and put your +stuff there. + +-jc + diff --git a/contrib/blameview/README b/contrib/blameview/README new file mode 100644 index 0000000000..50a6f67fd6 --- /dev/null +++ b/contrib/blameview/README @@ -0,0 +1,10 @@ +This is a sample program to use 'git-blame --incremental', based +on this message. + +From: Jeff King <peff@peff.net> +Subject: Re: More precise tag following +To: Linus Torvalds <torvalds@linux-foundation.org> +Cc: git@vger.kernel.org +Date: Sat, 27 Jan 2007 18:52:38 -0500 +Message-ID: <20070127235238.GA28706@coredump.intra.peff.net> + diff --git a/contrib/blameview/blameview.perl b/contrib/blameview/blameview.perl new file mode 100755 index 0000000000..807d01fe3d --- /dev/null +++ b/contrib/blameview/blameview.perl @@ -0,0 +1,136 @@ +#!/usr/bin/perl + +use Gtk2 -init; +use Gtk2::SimpleList; + +my $hash; +my $fn; +if ( @ARGV == 1 ) { + $hash = "HEAD"; + $fn = shift; +} elsif ( @ARGV == 2 ) { + $hash = shift; + $fn = shift; +} else { + die "Usage blameview [<rev>] <filename>"; +} + +Gtk2::Rc->parse_string(<<'EOS'); +style "treeview_style" +{ + GtkTreeView::vertical-separator = 0 +} +class "GtkTreeView" style "treeview_style" +EOS + +my $window = Gtk2::Window->new('toplevel'); +$window->signal_connect(destroy => sub { Gtk2->main_quit }); +my $scrolled_window = Gtk2::ScrolledWindow->new; +$window->add($scrolled_window); +my $fileview = Gtk2::SimpleList->new( + 'Commit' => 'text', + 'CommitInfo' => 'text', + 'FileLine' => 'text', + 'Data' => 'text' +); +$scrolled_window->add($fileview); +$fileview->get_column(0)->set_spacing(0); +$fileview->set_size_request(1024, 768); +$fileview->set_rules_hint(1); +$fileview->signal_connect (row_activated => sub { + my ($sl, $path, $column) = @_; + my $row_ref = $sl->get_row_data_from_path ($path); + system("blameview @$row_ref[0] $fn"); + # $row_ref is now an array ref to the double-clicked row's data. + }); + +my $fh; +open($fh, '-|', "git cat-file blob $hash:$fn") + or die "unable to open $fn: $!"; + +while(<$fh>) { + chomp; + $fileview->{data}->[$.] = ['HEAD', '?', "$fn:$.", $_]; +} + +my $blame; +open($blame, '-|', qw(git blame --incremental --), $fn, $hash) + or die "cannot start git-blame $fn"; + +Glib::IO->add_watch(fileno($blame), 'in', \&read_blame_line); + +$window->show_all; +Gtk2->main; +exit 0; + +my %commitinfo = (); + +sub flush_blame_line { + my ($attr) = @_; + + return unless defined $attr; + + my ($commit, $s_lno, $lno, $cnt) = + @{$attr}{qw(COMMIT S_LNO LNO CNT)}; + + my ($filename, $author, $author_time, $author_tz) = + @{$commitinfo{$commit}}{qw(FILENAME AUTHOR AUTHOR-TIME AUTHOR-TZ)}; + my $info = $author . ' ' . format_time($author_time, $author_tz); + + for(my $i = 0; $i < $cnt; $i++) { + @{$fileview->{data}->[$lno+$i-1]}[0,1,2] = + (substr($commit, 0, 8), $info, + $filename . ':' . ($s_lno+$i)); + } +} + +my $buf; +my $current; +sub read_blame_line { + + my $r = sysread($blame, $buf, 1024, length($buf)); + die "I/O error" unless defined $r; + + if ($r == 0) { + flush_blame_line($current); + $current = undef; + return 0; + } + + while ($buf =~ s/([^\n]*)\n//) { + my $line = $1; + + if (($commit, $s_lno, $lno, $cnt) = + ($line =~ /^([0-9a-f]{40}) (\d+) (\d+) (\d+)$/)) { + flush_blame_line($current); + $current = +{ + COMMIT => $1, + S_LNO => $2, + LNO => $3, + CNT => $4, + }; + next; + } + + # extended attribute values + if ($line =~ /^(author|author-mail|author-time|author-tz|committer|committer-mail|committer-time|committer-tz|summary|filename) (.*)$/) { + my $commit = $current->{COMMIT}; + $commitinfo{$commit}{uc($1)} = $2; + next; + } + } + return 1; +} + +sub format_time { + my $time = shift; + my $tz = shift; + + my $minutes = $tz < 0 ? 0-$tz : $tz; + $minutes = ($minutes / 100)*60 + ($minutes % 100); + $minutes = $tz < 0 ? 0-$minutes : $minutes; + $time += $minutes * 60; + my @t = gmtime($time); + return sprintf('%04d-%02d-%02d %02d:%02d:%02d %s', + $t[5] + 1900, @t[4,3,2,1,0], $tz); +} diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash new file mode 100755 index 0000000000..5d3d402051 --- /dev/null +++ b/contrib/completion/git-completion.bash @@ -0,0 +1,1038 @@ +# +# bash completion support for core Git. +# +# Copyright (C) 2006,2007 Shawn Pearce +# Conceptually based on gitcompletion (http://gitweb.hawaga.org.uk/). +# +# The contained completion routines provide support for completing: +# +# *) local and remote branch names +# *) local and remote tag names +# *) .git/remotes file names +# *) git 'subcommands' +# *) tree paths within 'ref:path/to/file' expressions +# +# To use these routines: +# +# 1) Copy this file to somewhere (e.g. ~/.git-completion.sh). +# 2) Added the following line to your .bashrc: +# source ~/.git-completion.sh +# +# 3) You may want to make sure the git executable is available +# in your PATH before this script is sourced, as some caching +# is performed while the script loads. If git isn't found +# at source time then all lookups will be done on demand, +# which may be slightly slower. +# +# 4) Consider changing your PS1 to also show the current branch: +# PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' +# +# The argument to __git_ps1 will be displayed only if you +# are currently in a git repository. The %s token will be +# the name of the current branch. +# + +__gitdir () +{ + if [ -z "$1" ]; then + if [ -n "$__git_dir" ]; then + echo "$__git_dir" + elif [ -d .git ]; then + echo .git + else + git rev-parse --git-dir 2>/dev/null + fi + elif [ -d "$1/.git" ]; then + echo "$1/.git" + else + echo "$1" + fi +} + +__git_ps1 () +{ + local b="$(git symbolic-ref HEAD 2>/dev/null)" + if [ -n "$b" ]; then + if [ -n "$1" ]; then + printf "$1" "${b##refs/heads/}" + else + printf " (%s)" "${b##refs/heads/}" + fi + fi +} + +__gitcomp () +{ + local all c s=$'\n' IFS=' '$'\t'$'\n' + local cur="${COMP_WORDS[COMP_CWORD]}" + if [ $# -gt 2 ]; then + cur="$3" + fi + for c in $1; do + case "$c$4" in + --*=*) all="$all$c$4$s" ;; + *.) all="$all$c$4$s" ;; + *) all="$all$c$4 $s" ;; + esac + done + IFS=$s + COMPREPLY=($(compgen -P "$2" -W "$all" -- "$cur")) + return +} + +__git_heads () +{ + local cmd i is_hash=y dir="$(__gitdir "$1")" + if [ -d "$dir" ]; then + for i in $(git --git-dir="$dir" \ + for-each-ref --format='%(refname)' \ + refs/heads ); do + echo "${i#refs/heads/}" + done + return + fi + for i in $(git-ls-remote "$1" 2>/dev/null); do + case "$is_hash,$i" in + y,*) is_hash=n ;; + n,*^{}) is_hash=y ;; + n,refs/heads/*) is_hash=y; echo "${i#refs/heads/}" ;; + n,*) is_hash=y; echo "$i" ;; + esac + done +} + +__git_refs () +{ + local cmd i is_hash=y dir="$(__gitdir "$1")" + if [ -d "$dir" ]; then + if [ -e "$dir/HEAD" ]; then echo HEAD; fi + for i in $(git --git-dir="$dir" \ + for-each-ref --format='%(refname)' \ + refs/tags refs/heads refs/remotes); do + case "$i" in + refs/tags/*) echo "${i#refs/tags/}" ;; + refs/heads/*) echo "${i#refs/heads/}" ;; + refs/remotes/*) echo "${i#refs/remotes/}" ;; + *) echo "$i" ;; + esac + done + return + fi + for i in $(git-ls-remote "$dir" 2>/dev/null); do + case "$is_hash,$i" in + y,*) is_hash=n ;; + n,*^{}) is_hash=y ;; + n,refs/tags/*) is_hash=y; echo "${i#refs/tags/}" ;; + n,refs/heads/*) is_hash=y; echo "${i#refs/heads/}" ;; + n,refs/remotes/*) is_hash=y; echo "${i#refs/remotes/}" ;; + n,*) is_hash=y; echo "$i" ;; + esac + done +} + +__git_refs2 () +{ + local i + for i in $(__git_refs "$1"); do + echo "$i:$i" + done +} + +__git_refs_remotes () +{ + local cmd i is_hash=y + for i in $(git-ls-remote "$1" 2>/dev/null); do + case "$is_hash,$i" in + n,refs/heads/*) + is_hash=y + echo "$i:refs/remotes/$1/${i#refs/heads/}" + ;; + y,*) is_hash=n ;; + n,*^{}) is_hash=y ;; + n,refs/tags/*) is_hash=y;; + n,*) is_hash=y; ;; + esac + done +} + +__git_remotes () +{ + local i ngoff IFS=$'\n' d="$(__gitdir)" + shopt -q nullglob || ngoff=1 + shopt -s nullglob + for i in "$d/remotes"/*; do + echo ${i#$d/remotes/} + done + [ "$ngoff" ] && shopt -u nullglob + for i in $(git --git-dir="$d" config --list); do + case "$i" in + remote.*.url=*) + i="${i#remote.}" + echo "${i/.url=*/}" + ;; + esac + done +} + +__git_merge_strategies () +{ + if [ -n "$__git_merge_strategylist" ]; then + echo "$__git_merge_strategylist" + return + fi + sed -n "/^all_strategies='/{ + s/^all_strategies='// + s/'// + p + q + }" "$(git --exec-path)/git-merge" +} +__git_merge_strategylist= +__git_merge_strategylist="$(__git_merge_strategies 2>/dev/null)" + +__git_complete_file () +{ + local pfx ls ref cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + ?*:*) + ref="${cur%%:*}" + cur="${cur#*:}" + case "$cur" in + ?*/*) + pfx="${cur%/*}" + cur="${cur##*/}" + ls="$ref:$pfx" + pfx="$pfx/" + ;; + *) + ls="$ref" + ;; + esac + COMPREPLY=($(compgen -P "$pfx" \ + -W "$(git --git-dir="$(__gitdir)" ls-tree "$ls" \ + | sed '/^100... blob /s,^.* ,, + /^040000 tree /{ + s,^.* ,, + s,$,/, + } + s/^.* //')" \ + -- "$cur")) + ;; + *) + __gitcomp "$(__git_refs)" + ;; + esac +} + +__git_complete_revlist () +{ + local pfx cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + *...*) + pfx="${cur%...*}..." + cur="${cur#*...}" + __gitcomp "$(__git_refs)" "$pfx" "$cur" + ;; + *..*) + pfx="${cur%..*}.." + cur="${cur#*..}" + __gitcomp "$(__git_refs)" "$pfx" "$cur" + ;; + *.) + __gitcomp "$cur." + ;; + *) + __gitcomp "$(__git_refs)" + ;; + esac +} + +__git_commands () +{ + if [ -n "$__git_commandlist" ]; then + echo "$__git_commandlist" + return + fi + local i IFS=" "$'\n' + for i in $(git help -a|egrep '^ ') + do + case $i in + add--interactive) : plumbing;; + applymbox) : ask gittus;; + applypatch) : ask gittus;; + archimport) : import;; + cat-file) : plumbing;; + check-ref-format) : plumbing;; + commit-tree) : plumbing;; + convert-objects) : plumbing;; + cvsexportcommit) : export;; + cvsimport) : import;; + cvsserver) : daemon;; + daemon) : daemon;; + diff-stages) : nobody uses it;; + fast-import) : import;; + fsck-objects) : plumbing;; + fetch-pack) : plumbing;; + fmt-merge-msg) : plumbing;; + hash-object) : plumbing;; + http-*) : transport;; + index-pack) : plumbing;; + init-db) : deprecated;; + local-fetch) : plumbing;; + mailinfo) : plumbing;; + mailsplit) : plumbing;; + merge-*) : plumbing;; + mktree) : plumbing;; + mktag) : plumbing;; + pack-objects) : plumbing;; + pack-redundant) : plumbing;; + pack-refs) : plumbing;; + parse-remote) : plumbing;; + patch-id) : plumbing;; + peek-remote) : plumbing;; + prune) : plumbing;; + prune-packed) : plumbing;; + quiltimport) : import;; + read-tree) : plumbing;; + receive-pack) : plumbing;; + reflog) : plumbing;; + repo-config) : plumbing;; + rerere) : plumbing;; + resolve) : dead dont use;; + rev-list) : plumbing;; + rev-parse) : plumbing;; + runstatus) : plumbing;; + sh-setup) : internal;; + shell) : daemon;; + send-pack) : plumbing;; + show-index) : plumbing;; + ssh-*) : transport;; + stripspace) : plumbing;; + svn) : import export;; + svnimport) : import;; + symbolic-ref) : plumbing;; + tar-tree) : deprecated;; + unpack-file) : plumbing;; + unpack-objects) : plumbing;; + update-index) : plumbing;; + update-ref) : plumbing;; + update-server-info) : daemon;; + upload-archive) : plumbing;; + upload-pack) : plumbing;; + write-tree) : plumbing;; + verify-tag) : plumbing;; + *) echo $i;; + esac + done +} +__git_commandlist= +__git_commandlist="$(__git_commands 2>/dev/null)" + +__git_aliases () +{ + local i IFS=$'\n' + for i in $(git --git-dir="$(__gitdir)" config --list); do + case "$i" in + alias.*) + i="${i#alias.}" + echo "${i/=*/}" + ;; + esac + done +} + +__git_aliased_command () +{ + local word cmdline=$(git --git-dir="$(__gitdir)" \ + config --get "alias.$1") + for word in $cmdline; do + if [ "${word##-*}" ]; then + echo $word + return + fi + done +} + +__git_whitespacelist="nowarn warn error error-all strip" + +_git_am () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + if [ -d .dotest ]; then + __gitcomp "--skip --resolved" + return + fi + case "$cur" in + --whitespace=*) + __gitcomp "$__git_whitespacelist" "" "${cur##--whitespace=}" + return + ;; + --*) + __gitcomp " + --signoff --utf8 --binary --3way --interactive + --whitespace= + " + return + esac + COMPREPLY=() +} + +_git_apply () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --whitespace=*) + __gitcomp "$__git_whitespacelist" "" "${cur##--whitespace=}" + return + ;; + --*) + __gitcomp " + --stat --numstat --summary --check --index + --cached --index-info --reverse --reject --unidiff-zero + --apply --no-add --exclude= + --whitespace= --inaccurate-eof --verbose + " + return + esac + COMPREPLY=() +} + +_git_add () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --*) + __gitcomp "--interactive" + return + esac + COMPREPLY=() +} + +_git_bisect () +{ + local i c=1 command + while [ $c -lt $COMP_CWORD ]; do + i="${COMP_WORDS[c]}" + case "$i" in + start|bad|good|reset|visualize|replay|log) + command="$i" + break + ;; + esac + c=$((++c)) + done + + if [ $c -eq $COMP_CWORD -a -z "$command" ]; then + __gitcomp "start bad good reset visualize replay log" + return + fi + + case "$command" in + bad|good|reset) + __gitcomp "$(__git_refs)" + ;; + *) + COMPREPLY=() + ;; + esac +} + +_git_branch () +{ + __gitcomp "$(__git_refs)" +} + +_git_checkout () +{ + __gitcomp "$(__git_refs)" +} + +_git_cherry () +{ + __gitcomp "$(__git_refs)" +} + +_git_cherry_pick () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --*) + __gitcomp "--edit --no-commit" + ;; + *) + __gitcomp "$(__git_refs)" + ;; + esac +} + +_git_commit () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --*) + __gitcomp " + --all --author= --signoff --verify --no-verify + --edit --amend --include --only + " + return + esac + COMPREPLY=() +} + +_git_diff () +{ + __git_complete_file +} + +_git_diff_tree () +{ + __gitcomp "$(__git_refs)" +} + +_git_fetch () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + + case "${COMP_WORDS[0]},$COMP_CWORD" in + git-fetch*,1) + __gitcomp "$(__git_remotes)" + ;; + git,2) + __gitcomp "$(__git_remotes)" + ;; + *) + case "$cur" in + *:*) + __gitcomp "$(__git_refs)" "" "${cur#*:}" + ;; + *) + local remote + case "${COMP_WORDS[0]}" in + git-fetch) remote="${COMP_WORDS[1]}" ;; + git) remote="${COMP_WORDS[2]}" ;; + esac + __gitcomp "$(__git_refs2 "$remote")" + ;; + esac + ;; + esac +} + +_git_format_patch () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --*) + __gitcomp " + --stdout --attach --thread + --output-directory + --numbered --start-number + --keep-subject + --signoff + --in-reply-to= + --full-index --binary + --not --all + " + return + ;; + esac + __git_complete_revlist +} + +_git_gc () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --*) + __gitcomp "--prune" + return + ;; + esac + COMPREPLY=() +} + +_git_ls_remote () +{ + __gitcomp "$(__git_remotes)" +} + +_git_ls_tree () +{ + __git_complete_file +} + +_git_log () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --pretty=*) + __gitcomp " + oneline short medium full fuller email raw + " "" "${cur##--pretty=}" + return + ;; + --*) + __gitcomp " + --max-count= --max-age= --since= --after= + --min-age= --before= --until= + --root --not --topo-order --date-order + --no-merges + --abbrev-commit --abbrev= + --relative-date + --author= --committer= --grep= + --all-match + --pretty= --name-status --name-only + --not --all + " + return + ;; + esac + __git_complete_revlist +} + +_git_merge () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "${COMP_WORDS[COMP_CWORD-1]}" in + -s|--strategy) + __gitcomp "$(__git_merge_strategies)" + return + esac + case "$cur" in + --strategy=*) + __gitcomp "$(__git_merge_strategies)" "" "${cur##--strategy=}" + return + ;; + --*) + __gitcomp " + --no-commit --no-summary --squash --strategy + " + return + esac + __gitcomp "$(__git_refs)" +} + +_git_merge_base () +{ + __gitcomp "$(__git_refs)" +} + +_git_name_rev () +{ + __gitcomp "--tags --all --stdin" +} + +_git_pull () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + + case "${COMP_WORDS[0]},$COMP_CWORD" in + git-pull*,1) + __gitcomp "$(__git_remotes)" + ;; + git,2) + __gitcomp "$(__git_remotes)" + ;; + *) + local remote + case "${COMP_WORDS[0]}" in + git-pull) remote="${COMP_WORDS[1]}" ;; + git) remote="${COMP_WORDS[2]}" ;; + esac + __gitcomp "$(__git_refs "$remote")" + ;; + esac +} + +_git_push () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + + case "${COMP_WORDS[0]},$COMP_CWORD" in + git-push*,1) + __gitcomp "$(__git_remotes)" + ;; + git,2) + __gitcomp "$(__git_remotes)" + ;; + *) + case "$cur" in + *:*) + local remote + case "${COMP_WORDS[0]}" in + git-push) remote="${COMP_WORDS[1]}" ;; + git) remote="${COMP_WORDS[2]}" ;; + esac + __gitcomp "$(__git_refs "$remote")" "" "${cur#*:}" + ;; + *) + __gitcomp "$(__git_refs2)" + ;; + esac + ;; + esac +} + +_git_rebase () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + if [ -d .dotest ] || [ -d .git/.dotest-merge ]; then + __gitcomp "--continue --skip --abort" + return + fi + case "${COMP_WORDS[COMP_CWORD-1]}" in + -s|--strategy) + __gitcomp "$(__git_merge_strategies)" + return + esac + case "$cur" in + --strategy=*) + __gitcomp "$(__git_merge_strategies)" "" "${cur##--strategy=}" + return + ;; + --*) + __gitcomp "--onto --merge --strategy" + return + esac + __gitcomp "$(__git_refs)" +} + +_git_config () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + local prv="${COMP_WORDS[COMP_CWORD-1]}" + case "$prv" in + branch.*.remote) + __gitcomp "$(__git_remotes)" + return + ;; + branch.*.merge) + __gitcomp "$(__git_refs)" + return + ;; + remote.*.fetch) + local remote="${prv#remote.}" + remote="${remote%.fetch}" + __gitcomp "$(__git_refs_remotes "$remote")" + return + ;; + remote.*.push) + local remote="${prv#remote.}" + remote="${remote%.push}" + __gitcomp "$(git --git-dir="$(__gitdir)" \ + for-each-ref --format='%(refname):%(refname)' \ + refs/heads)" + return + ;; + pull.twohead|pull.octopus) + __gitcomp "$(__git_merge_strategies)" + return + ;; + color.branch|color.diff|color.status) + __gitcomp "always never auto" + return + ;; + color.*.*) + __gitcomp " + black red green yellow blue magenta cyan white + bold dim ul blink reverse + " + return + ;; + *.*) + COMPREPLY=() + return + ;; + esac + case "$cur" in + --*) + __gitcomp " + --global --list --replace-all + --get --get-all --get-regexp + --add --unset --unset-all + " + return + ;; + branch.*.*) + local pfx="${cur%.*}." + cur="${cur##*.}" + __gitcomp "remote merge" "$pfx" "$cur" + return + ;; + branch.*) + local pfx="${cur%.*}." + cur="${cur#*.}" + __gitcomp "$(__git_heads)" "$pfx" "$cur" "." + return + ;; + remote.*.*) + local pfx="${cur%.*}." + cur="${cur##*.}" + __gitcomp "url fetch push" "$pfx" "$cur" + return + ;; + remote.*) + local pfx="${cur%.*}." + cur="${cur#*.}" + __gitcomp "$(__git_remotes)" "$pfx" "$cur" "." + return + ;; + esac + __gitcomp " + apply.whitespace + core.fileMode + core.gitProxy + core.ignoreStat + core.preferSymlinkRefs + core.logAllRefUpdates + core.repositoryFormatVersion + core.sharedRepository + core.warnAmbiguousRefs + core.compression + core.legacyHeaders + core.packedGitWindowSize + core.packedGitLimit + color.branch + color.branch.current + color.branch.local + color.branch.remote + color.branch.plain + color.diff + color.diff.plain + color.diff.meta + color.diff.frag + color.diff.old + color.diff.new + color.diff.commit + color.diff.whitespace + color.pager + color.status + color.status.header + color.status.added + color.status.changed + color.status.untracked + diff.renameLimit + diff.renames + fetch.unpackLimit + format.headers + gitcvs.enabled + gitcvs.logfile + gc.reflogexpire + gc.reflogexpireunreachable + gc.rerereresolved + gc.rerereunresolved + http.sslVerify + http.sslCert + http.sslKey + http.sslCAInfo + http.sslCAPath + http.maxRequests + http.lowSpeedLimit + http.lowSpeedTime + http.noEPSV + i18n.commitEncoding + i18n.logOutputEncoding + log.showroot + merge.summary + merge.verbosity + pack.window + pull.octopus + pull.twohead + repack.useDeltaBaseOffset + show.difftree + showbranch.default + tar.umask + transfer.unpackLimit + receive.unpackLimit + receive.denyNonFastForwards + user.name + user.email + user.signingkey + whatchanged.difftree + branch. remote. + " +} + +_git_remote () +{ + local i c=1 command + while [ $c -lt $COMP_CWORD ]; do + i="${COMP_WORDS[c]}" + case "$i" in + add|show|prune) command="$i"; break ;; + esac + c=$((++c)) + done + + if [ $c -eq $COMP_CWORD -a -z "$command" ]; then + __gitcomp "add show prune" + return + fi + + case "$command" in + show|prune) + __gitcomp "$(__git_remotes)" + ;; + *) + COMPREPLY=() + ;; + esac +} + +_git_reset () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --*) + __gitcomp "--mixed --hard --soft" + return + ;; + esac + __gitcomp "$(__git_refs)" +} + +_git_show () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --pretty=*) + __gitcomp " + oneline short medium full fuller email raw + " "" "${cur##--pretty=}" + return + ;; + --*) + __gitcomp "--pretty=" + return + ;; + esac + __git_complete_file +} + +_git () +{ + local i c=1 command __git_dir + + while [ $c -lt $COMP_CWORD ]; do + i="${COMP_WORDS[c]}" + case "$i" in + --git-dir=*) __git_dir="${i#--git-dir=}" ;; + --bare) __git_dir="." ;; + --version|--help|-p|--paginate) ;; + *) command="$i"; break ;; + esac + c=$((++c)) + done + + if [ $c -eq $COMP_CWORD -a -z "$command" ]; then + case "${COMP_WORDS[COMP_CWORD]}" in + --*=*) COMPREPLY=() ;; + --*) __gitcomp "--git-dir= --bare --version --exec-path" ;; + *) __gitcomp "$(__git_commands) $(__git_aliases)" ;; + esac + return + fi + + local expansion=$(__git_aliased_command "$command") + [ "$expansion" ] && command="$expansion" + + case "$command" in + am) _git_am ;; + add) _git_add ;; + apply) _git_apply ;; + bisect) _git_bisect ;; + branch) _git_branch ;; + checkout) _git_checkout ;; + cherry) _git_cherry ;; + cherry-pick) _git_cherry_pick ;; + commit) _git_commit ;; + config) _git_config ;; + diff) _git_diff ;; + diff-tree) _git_diff_tree ;; + fetch) _git_fetch ;; + format-patch) _git_format_patch ;; + gc) _git_gc ;; + log) _git_log ;; + ls-remote) _git_ls_remote ;; + ls-tree) _git_ls_tree ;; + merge) _git_merge;; + merge-base) _git_merge_base ;; + name-rev) _git_name_rev ;; + pull) _git_pull ;; + push) _git_push ;; + rebase) _git_rebase ;; + remote) _git_remote ;; + reset) _git_reset ;; + show) _git_show ;; + show-branch) _git_log ;; + whatchanged) _git_log ;; + *) COMPREPLY=() ;; + esac +} + +_gitk () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + case "$cur" in + --*) + __gitcomp "--not --all" + return + ;; + esac + __git_complete_revlist +} + +complete -o default -o nospace -F _git git +complete -o default -o nospace -F _gitk gitk +complete -o default -o nospace -F _git_am git-am +complete -o default -o nospace -F _git_apply git-apply +complete -o default -o nospace -F _git_bisect git-bisect +complete -o default -o nospace -F _git_branch git-branch +complete -o default -o nospace -F _git_checkout git-checkout +complete -o default -o nospace -F _git_cherry git-cherry +complete -o default -o nospace -F _git_cherry_pick git-cherry-pick +complete -o default -o nospace -F _git_commit git-commit +complete -o default -o nospace -F _git_diff git-diff +complete -o default -o nospace -F _git_diff_tree git-diff-tree +complete -o default -o nospace -F _git_fetch git-fetch +complete -o default -o nospace -F _git_format_patch git-format-patch +complete -o default -o nospace -F _git_gc git-gc +complete -o default -o nospace -F _git_log git-log +complete -o default -o nospace -F _git_ls_remote git-ls-remote +complete -o default -o nospace -F _git_ls_tree git-ls-tree +complete -o default -o nospace -F _git_merge git-merge +complete -o default -o nospace -F _git_merge_base git-merge-base +complete -o default -o nospace -F _git_name_rev git-name-rev +complete -o default -o nospace -F _git_pull git-pull +complete -o default -o nospace -F _git_push git-push +complete -o default -o nospace -F _git_rebase git-rebase +complete -o default -o nospace -F _git_config git-config +complete -o default -o nospace -F _git_remote git-remote +complete -o default -o nospace -F _git_reset git-reset +complete -o default -o nospace -F _git_show git-show +complete -o default -o nospace -F _git_log git-show-branch +complete -o default -o nospace -F _git_log git-whatchanged + +# The following are necessary only for Cygwin, and only are needed +# when the user has tab-completed the executable name and consequently +# included the '.exe' suffix. +# +if [ Cygwin = "$(uname -o 2>/dev/null)" ]; then +complete -o default -o nospace -F _git_add git-add.exe +complete -o default -o nospace -F _git_apply git-apply.exe +complete -o default -o nospace -F _git git.exe +complete -o default -o nospace -F _git_branch git-branch.exe +complete -o default -o nospace -F _git_cherry git-cherry.exe +complete -o default -o nospace -F _git_diff git-diff.exe +complete -o default -o nospace -F _git_diff_tree git-diff-tree.exe +complete -o default -o nospace -F _git_format_patch git-format-patch.exe +complete -o default -o nospace -F _git_log git-log.exe +complete -o default -o nospace -F _git_ls_tree git-ls-tree.exe +complete -o default -o nospace -F _git_merge_base git-merge-base.exe +complete -o default -o nospace -F _git_name_rev git-name-rev.exe +complete -o default -o nospace -F _git_push git-push.exe +complete -o default -o nospace -F _git_config git-config +complete -o default -o nospace -F _git_show git-show.exe +complete -o default -o nospace -F _git_log git-show-branch.exe +complete -o default -o nospace -F _git_log git-whatchanged.exe +fi diff --git a/contrib/emacs/.gitignore b/contrib/emacs/.gitignore new file mode 100644 index 0000000000..c531d9867f --- /dev/null +++ b/contrib/emacs/.gitignore @@ -0,0 +1 @@ +*.elc diff --git a/contrib/emacs/Makefile b/contrib/emacs/Makefile new file mode 100644 index 0000000000..350846de90 --- /dev/null +++ b/contrib/emacs/Makefile @@ -0,0 +1,20 @@ +## Build and install stuff + +EMACS = emacs + +ELC = git.elc vc-git.elc +INSTALL ?= install +INSTALL_ELC = $(INSTALL) -m 644 +prefix ?= $(HOME) +emacsdir = $(prefix)/share/emacs/site-lisp + +all: $(ELC) + +install: all + $(INSTALL) -d $(emacsdir) + $(INSTALL_ELC) $(ELC) $(emacsdir) + +%.elc: %.el + $(EMACS) --batch --eval '(byte-compile-file "$<")' + +clean:; rm -f $(ELC) diff --git a/contrib/emacs/git-blame.el b/contrib/emacs/git-blame.el new file mode 100644 index 0000000000..64ad50b327 --- /dev/null +++ b/contrib/emacs/git-blame.el @@ -0,0 +1,380 @@ +;;; git-blame.el --- Minor mode for incremental blame for Git -*- coding: utf-8 -*- +;; +;; Copyright (C) 2007 David KÃ¥gedal +;; +;; Authors: David KÃ¥gedal <davidk@lysator.liu.se> +;; Created: 31 Jan 2007 +;; Message-ID: <87iren2vqx.fsf@morpheus.local> +;; License: GPL +;; Keywords: git, version control, release management +;; +;; Compatibility: Emacs21 + + +;; This file is *NOT* part of GNU Emacs. +;; This file is distributed under the same terms as GNU Emacs. + +;; This program is free software; you can redistribute it and/or +;; modify it under the terms of the GNU General Public License as +;; published by the Free Software Foundation; either version 2 of +;; the License, or (at your option) any later version. + +;; This program is distributed in the hope that it will be +;; useful, but WITHOUT ANY WARRANTY; without even the implied +;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +;; PURPOSE. See the GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public +;; License along with this program; if not, write to the Free +;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +;; MA 02111-1307 USA + +;; http://www.fsf.org/copyleft/gpl.html + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Commentary: +;; +;; Here is an Emacs implementation of incremental git-blame. When you +;; turn it on while viewing a file, the editor buffer will be updated by +;; setting the background of individual lines to a color that reflects +;; which commit it comes from. And when you move around the buffer, a +;; one-line summary will be shown in the echo area. + +;;; Installation: +;; +;; To use this package, put it somewhere in `load-path' (or add +;; directory with git-blame.el to `load-path'), and add the following +;; line to your .emacs: +;; +;; (require 'git-blame) +;; +;; If you do not want to load this package before it is necessary, you +;; can make use of the `autoload' feature, e.g. by adding to your .emacs +;; the following lines +;; +;; (autoload 'git-blame-mode "git-blame" +;; "Minor mode for incremental blame for Git." t) +;; +;; Then first use of `M-x git-blame-mode' would load the package. + +;;; Compatibility: +;; +;; It requires GNU Emacs 21. If you'are using Emacs 20, try +;; changing this: +;; +;; (overlay-put ovl 'face (list :background +;; (cdr (assq 'color (cddddr info))))) +;; +;; to +;; +;; (overlay-put ovl 'face (cons 'background-color +;; (cdr (assq 'color (cddddr info))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;;; Code: + +(require 'cl) ; to use `push', `pop' + +(defun color-scale (l) + (let* ((colors ()) + r g b) + (setq r l) + (while r + (setq g l) + (while g + (setq b l) + (while b + (push (concat "#" (car r) (car g) (car b)) colors) + (pop b)) + (pop g)) + (pop r)) + colors)) + +(defvar git-blame-dark-colors + (color-scale '("0c" "04" "24" "1c" "2c" "34" "14" "3c"))) + +(defvar git-blame-light-colors + (color-scale '("c4" "d4" "cc" "dc" "f4" "e4" "fc" "ec"))) + +(defvar git-blame-ancient-color "dark green") + +(defvar git-blame-autoupdate t + "*Automatically update the blame display while editing") + +(defvar git-blame-proc nil + "The running git-blame process") +(make-variable-buffer-local 'git-blame-proc) + +(defvar git-blame-overlays nil + "The git-blame overlays used in the current buffer.") +(make-variable-buffer-local 'git-blame-overlays) + +(defvar git-blame-cache nil + "A cache of git-blame information for the current buffer") +(make-variable-buffer-local 'git-blame-cache) + +(defvar git-blame-idle-timer nil + "An idle timer that updates the blame") +(make-variable-buffer-local 'git-blame-cache) + +(defvar git-blame-update-queue nil + "A queue of update requests") +(make-variable-buffer-local 'git-blame-update-queue) + +(defvar git-blame-mode nil) +(make-variable-buffer-local 'git-blame-mode) +(unless (assq 'git-blame-mode minor-mode-alist) + (setq minor-mode-alist + (cons (list 'git-blame-mode " blame") + minor-mode-alist))) + +;;;###autoload +(defun git-blame-mode (&optional arg) + "Minor mode for displaying Git blame" + (interactive "P") + (if arg + (setq git-blame-mode (eq arg 1)) + (setq git-blame-mode (not git-blame-mode))) + (make-local-variable 'git-blame-colors) + (if git-blame-autoupdate + (add-hook 'after-change-functions 'git-blame-after-change nil t) + (remove-hook 'after-change-functions 'git-blame-after-change t)) + (git-blame-cleanup) + (if git-blame-mode + (progn + (let ((bgmode (cdr (assoc 'background-mode (frame-parameters))))) + (if (eq bgmode 'dark) + (setq git-blame-colors git-blame-dark-colors) + (setq git-blame-colors git-blame-light-colors))) + (setq git-blame-cache (make-hash-table :test 'equal)) + (git-blame-run)) + (cancel-timer git-blame-idle-timer))) + +;;;###autoload +(defun git-reblame () + "Recalculate all blame information in the current buffer" + (unless git-blame-mode + (error "git-blame is not active")) + (interactive) + (git-blame-cleanup) + (git-blame-run)) + +(defun git-blame-run (&optional startline endline) + (if git-blame-proc + ;; Should maybe queue up a new run here + (message "Already running git blame") + (let ((display-buf (current-buffer)) + (blame-buf (get-buffer-create + (concat " git blame for " (buffer-name)))) + (args '("--incremental" "--contents" "-"))) + (if startline + (setq args (append args + (list "-L" (format "%d,%d" startline endline))))) + (setq args (append args + (list (file-name-nondirectory buffer-file-name)))) + (setq git-blame-proc + (apply 'start-process + "git-blame" blame-buf + "git" "blame" + args)) + (with-current-buffer blame-buf + (erase-buffer) + (make-local-variable 'git-blame-file) + (make-local-variable 'git-blame-current) + (setq git-blame-file display-buf) + (setq git-blame-current nil)) + (set-process-filter git-blame-proc 'git-blame-filter) + (set-process-sentinel git-blame-proc 'git-blame-sentinel) + (process-send-region git-blame-proc (point-min) (point-max)) + (process-send-eof git-blame-proc)))) + +(defun remove-git-blame-text-properties (start end) + (let ((modified (buffer-modified-p)) + (inhibit-read-only t)) + (remove-text-properties start end '(point-entered nil)) + (set-buffer-modified-p modified))) + +(defun git-blame-cleanup () + "Remove all blame properties" + (mapcar 'delete-overlay git-blame-overlays) + (setq git-blame-overlays nil) + (remove-git-blame-text-properties (point-min) (point-max))) + +(defun git-blame-update-region (start end) + "Rerun blame to get updates between START and END" + (let ((overlays (overlays-in start end))) + (while overlays + (let ((overlay (pop overlays))) + (if (< (overlay-start overlay) start) + (setq start (overlay-start overlay))) + (if (> (overlay-end overlay) end) + (setq end (overlay-end overlay))) + (setq git-blame-overlays (delete overlay git-blame-overlays)) + (delete-overlay overlay)))) + (remove-git-blame-text-properties start end) + ;; We can be sure that start and end are at line breaks + (git-blame-run (1+ (count-lines (point-min) start)) + (count-lines (point-min) end))) + +(defun git-blame-sentinel (proc status) + (with-current-buffer (process-buffer proc) + (with-current-buffer git-blame-file + (setq git-blame-proc nil) + (if git-blame-update-queue + (git-blame-delayed-update)))) + ;;(kill-buffer (process-buffer proc)) + ;;(message "git blame finished") + ) + +(defvar in-blame-filter nil) + +(defun git-blame-filter (proc str) + (save-excursion + (set-buffer (process-buffer proc)) + (goto-char (process-mark proc)) + (insert-before-markers str) + (goto-char 0) + (unless in-blame-filter + (let ((more t) + (in-blame-filter t)) + (while more + (setq more (git-blame-parse))))))) + +(defun git-blame-parse () + (cond ((looking-at "\\([0-9a-f]\\{40\\}\\) \\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\)\n") + (let ((hash (match-string 1)) + (src-line (string-to-number (match-string 2))) + (res-line (string-to-number (match-string 3))) + (num-lines (string-to-number (match-string 4)))) + (setq git-blame-current + (if (string= hash "0000000000000000000000000000000000000000") + nil + (git-blame-new-commit + hash src-line res-line num-lines)))) + (delete-region (point) (match-end 0)) + t) + ((looking-at "filename \\(.+\\)\n") + (let ((filename (match-string 1))) + (git-blame-add-info "filename" filename)) + (delete-region (point) (match-end 0)) + t) + ((looking-at "\\([a-z-]+\\) \\(.+\\)\n") + (let ((key (match-string 1)) + (value (match-string 2))) + (git-blame-add-info key value)) + (delete-region (point) (match-end 0)) + t) + ((looking-at "boundary\n") + (setq git-blame-current nil) + (delete-region (point) (match-end 0)) + t) + (t + nil))) + + +(defun git-blame-new-commit (hash src-line res-line num-lines) + (save-excursion + (set-buffer git-blame-file) + (let ((info (gethash hash git-blame-cache)) + (inhibit-point-motion-hooks t) + (inhibit-modification-hooks t)) + (when (not info) + (let ((color (pop git-blame-colors))) + (unless color + (setq color git-blame-ancient-color)) + (setq info (list hash src-line res-line num-lines + (git-describe-commit hash) + (cons 'color color)))) + (puthash hash info git-blame-cache)) + (goto-line res-line) + (while (> num-lines 0) + (if (get-text-property (point) 'git-blame) + (forward-line) + (let* ((start (point)) + (end (progn (forward-line 1) (point))) + (ovl (make-overlay start end))) + (push ovl git-blame-overlays) + (overlay-put ovl 'git-blame info) + (overlay-put ovl 'help-echo hash) + (overlay-put ovl 'face (list :background + (cdr (assq 'color (nthcdr 5 info))))) + ;; the point-entered property doesn't seem to work in overlays + ;;(overlay-put ovl 'point-entered + ;; `(lambda (x y) (git-blame-identify ,hash))) + (let ((modified (buffer-modified-p))) + (put-text-property (if (= start 1) start (1- start)) (1- end) + 'point-entered + `(lambda (x y) (git-blame-identify ,hash))) + (set-buffer-modified-p modified)))) + (setq num-lines (1- num-lines)))))) + +(defun git-blame-add-info (key value) + (if git-blame-current + (nconc git-blame-current (list (cons (intern key) value))))) + +(defun git-blame-current-commit () + (let ((info (get-char-property (point) 'git-blame))) + (if info + (car info) + (error "No commit info")))) + +(defun git-describe-commit (hash) + (with-temp-buffer + (call-process "git" nil t nil + "log" "-1" "--pretty=oneline" + hash) + (buffer-substring (point-min) (1- (point-max))))) + +(defvar git-blame-last-identification nil) +(make-variable-buffer-local 'git-blame-last-identification) +(defun git-blame-identify (&optional hash) + (interactive) + (let ((info (gethash (or hash (git-blame-current-commit)) git-blame-cache))) + (when (and info (not (eq info git-blame-last-identification))) + (message "%s" (nth 4 info)) + (setq git-blame-last-identification info)))) + +;; (defun git-blame-after-save () +;; (when git-blame-mode +;; (git-blame-cleanup) +;; (git-blame-run))) +;; (add-hook 'after-save-hook 'git-blame-after-save) + +(defun git-blame-after-change (start end length) + (when git-blame-mode + (git-blame-enq-update start end))) + +(defvar git-blame-last-update nil) +(make-variable-buffer-local 'git-blame-last-update) +(defun git-blame-enq-update (start end) + "Mark the region between START and END as needing blame update" + ;; Try to be smart and avoid multiple callouts for sequential + ;; editing + (cond ((and git-blame-last-update + (= start (cdr git-blame-last-update))) + (setcdr git-blame-last-update end)) + ((and git-blame-last-update + (= end (car git-blame-last-update))) + (setcar git-blame-last-update start)) + (t + (setq git-blame-last-update (cons start end)) + (setq git-blame-update-queue (nconc git-blame-update-queue + (list git-blame-last-update))))) + (unless (or git-blame-proc git-blame-idle-timer) + (setq git-blame-idle-timer + (run-with-idle-timer 0.5 nil 'git-blame-delayed-update)))) + +(defun git-blame-delayed-update () + (setq git-blame-idle-timer nil) + (if git-blame-update-queue + (let ((first (pop git-blame-update-queue)) + (inhibit-point-motion-hooks t)) + (git-blame-update-region (car first) (cdr first))))) + +(provide 'git-blame) + +;;; git-blame.el ends here diff --git a/contrib/emacs/git.el b/contrib/emacs/git.el new file mode 100644 index 0000000000..24629eb3e2 --- /dev/null +++ b/contrib/emacs/git.el @@ -0,0 +1,1099 @@ +;;; git.el --- A user interface for git + +;; Copyright (C) 2005, 2006 Alexandre Julliard <julliard@winehq.org> + +;; Version: 1.0 + +;; This program is free software; you can redistribute it and/or +;; modify it under the terms of the GNU General Public License as +;; published by the Free Software Foundation; either version 2 of +;; the License, or (at your option) any later version. +;; +;; This program is distributed in the hope that it will be +;; useful, but WITHOUT ANY WARRANTY; without even the implied +;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +;; PURPOSE. See the GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public +;; License along with this program; if not, write to the Free +;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +;; MA 02111-1307 USA + +;;; Commentary: + +;; This file contains an interface for the git version control +;; system. It provides easy access to the most frequently used git +;; commands. The user interface is as far as possible identical to +;; that of the PCL-CVS mode. +;; +;; To install: put this file on the load-path and place the following +;; in your .emacs file: +;; +;; (require 'git) +;; +;; To start: `M-x git-status' +;; +;; TODO +;; - portability to XEmacs +;; - better handling of subprocess errors +;; - hook into file save (after-save-hook) +;; - diff against other branch +;; - renaming files from the status buffer +;; - creating tags +;; - fetch/pull +;; - switching branches +;; - revlist browser +;; - git-show-branch browser +;; - menus +;; + +(eval-when-compile (require 'cl)) +(require 'ewoc) +(require 'log-edit) + + +;;;; Customizations +;;;; ------------------------------------------------------------ + +(defgroup git nil + "A user interface for the git versioning system." + :group 'tools) + +(defcustom git-committer-name nil + "User name to use for commits. +The default is to fall back to the repository config, +then to `add-log-full-name' and then to `user-full-name'." + :group 'git + :type '(choice (const :tag "Default" nil) + (string :tag "Name"))) + +(defcustom git-committer-email nil + "Email address to use for commits. +The default is to fall back to the git repository config, +then to `add-log-mailing-address' and then to `user-mail-address'." + :group 'git + :type '(choice (const :tag "Default" nil) + (string :tag "Email"))) + +(defcustom git-commits-coding-system 'utf-8 + "Default coding system for the log message of git commits." + :group 'git + :type 'coding-system) + +(defcustom git-append-signed-off-by nil + "Whether to append a Signed-off-by line to the commit message before editing." + :group 'git + :type 'boolean) + +(defcustom git-reuse-status-buffer t + "Whether `git-status' should try to reuse an existing buffer +if there is already one that displays the same directory." + :group 'git + :type 'boolean) + +(defcustom git-per-dir-ignore-file ".gitignore" + "Name of the per-directory ignore file." + :group 'git + :type 'string) + + +(defface git-status-face + '((((class color) (background light)) (:foreground "purple"))) + "Git mode face used to highlight added and modified files." + :group 'git) + +(defface git-unmerged-face + '((((class color) (background light)) (:foreground "red" :bold t))) + "Git mode face used to highlight unmerged files." + :group 'git) + +(defface git-unknown-face + '((((class color) (background light)) (:foreground "goldenrod" :bold t))) + "Git mode face used to highlight unknown files." + :group 'git) + +(defface git-uptodate-face + '((((class color) (background light)) (:foreground "grey60"))) + "Git mode face used to highlight up-to-date files." + :group 'git) + +(defface git-ignored-face + '((((class color) (background light)) (:foreground "grey60"))) + "Git mode face used to highlight ignored files." + :group 'git) + +(defface git-mark-face + '((((class color) (background light)) (:foreground "red" :bold t))) + "Git mode face used for the file marks." + :group 'git) + +(defface git-header-face + '((((class color) (background light)) (:foreground "blue"))) + "Git mode face used for commit headers." + :group 'git) + +(defface git-separator-face + '((((class color) (background light)) (:foreground "brown"))) + "Git mode face used for commit separator." + :group 'git) + +(defface git-permission-face + '((((class color) (background light)) (:foreground "green" :bold t))) + "Git mode face used for permission changes." + :group 'git) + + +;;;; Utilities +;;;; ------------------------------------------------------------ + +(defconst git-log-msg-separator "--- log message follows this line ---") + +(defvar git-log-edit-font-lock-keywords + `(("^\\(Author:\\|Date:\\|Parent:\\|Signed-off-by:\\)\\(.*\\)$" + (1 font-lock-keyword-face) + (2 font-lock-function-name-face)) + (,(concat "^\\(" (regexp-quote git-log-msg-separator) "\\)$") + (1 font-lock-comment-face)))) + +(defun git-get-env-strings (env) + "Build a list of NAME=VALUE strings from a list of environment strings." + (mapcar (lambda (entry) (concat (car entry) "=" (cdr entry))) env)) + +(defun git-call-process-env (buffer env &rest args) + "Wrapper for call-process that sets environment strings." + (if env + (apply #'call-process "env" nil buffer nil + (append (git-get-env-strings env) (list "git") args)) + (apply #'call-process "git" nil buffer nil args))) + +(defun git-call-process-env-string (env &rest args) + "Wrapper for call-process that sets environment strings, +and returns the process output as a string." + (with-temp-buffer + (and (eq 0 (apply #' git-call-process-env t env args)) + (buffer-string)))) + +(defun git-run-process-region (buffer start end program args) + "Run a git process with a buffer region as input." + (let ((output-buffer (current-buffer)) + (dir default-directory)) + (with-current-buffer buffer + (cd dir) + (apply #'call-process-region start end program + nil (list output-buffer nil) nil args)))) + +(defun git-run-command-buffer (buffer-name &rest args) + "Run a git command, sending the output to a buffer named BUFFER-NAME." + (let ((dir default-directory) + (buffer (get-buffer-create buffer-name))) + (message "Running git %s..." (car args)) + (with-current-buffer buffer + (let ((default-directory dir) + (buffer-read-only nil)) + (erase-buffer) + (apply #'git-call-process-env buffer nil args))) + (message "Running git %s...done" (car args)) + buffer)) + +(defun git-run-command (buffer env &rest args) + (message "Running git %s..." (car args)) + (apply #'git-call-process-env buffer env args) + (message "Running git %s...done" (car args))) + +(defun git-run-command-region (buffer start end env &rest args) + "Run a git command with specified buffer region as input." + (message "Running git %s..." (car args)) + (unless (eq 0 (if env + (git-run-process-region + buffer start end "env" + (append (git-get-env-strings env) (list "git") args)) + (git-run-process-region + buffer start end "git" args))) + (error "Failed to run \"git %s\":\n%s" (mapconcat (lambda (x) x) args " ") (buffer-string))) + (message "Running git %s...done" (car args))) + +(defun git-get-string-sha1 (string) + "Read a SHA1 from the specified string." + (and string + (string-match "[0-9a-f]\\{40\\}" string) + (match-string 0 string))) + +(defun git-get-committer-name () + "Return the name to use as GIT_COMMITTER_NAME." + ; copied from log-edit + (or git-committer-name + (git-config "user.name") + (and (boundp 'add-log-full-name) add-log-full-name) + (and (fboundp 'user-full-name) (user-full-name)) + (and (boundp 'user-full-name) user-full-name))) + +(defun git-get-committer-email () + "Return the email address to use as GIT_COMMITTER_EMAIL." + ; copied from log-edit + (or git-committer-email + (git-config "user.email") + (and (boundp 'add-log-mailing-address) add-log-mailing-address) + (and (fboundp 'user-mail-address) (user-mail-address)) + (and (boundp 'user-mail-address) user-mail-address))) + +(defun git-escape-file-name (name) + "Escape a file name if necessary." + (if (string-match "[\n\t\"\\]" name) + (concat "\"" + (mapconcat (lambda (c) + (case c + (?\n "\\n") + (?\t "\\t") + (?\\ "\\\\") + (?\" "\\\"") + (t (char-to-string c)))) + name "") + "\"") + name)) + +(defun git-get-top-dir (dir) + "Retrieve the top-level directory of a git tree." + (let ((cdup (with-output-to-string + (with-current-buffer standard-output + (cd dir) + (unless (eq 0 (call-process "git" nil t nil "rev-parse" "--show-cdup")) + (error "cannot find top-level git tree for %s." dir)))))) + (expand-file-name (concat (file-name-as-directory dir) + (car (split-string cdup "\n")))))) + +;stolen from pcl-cvs +(defun git-append-to-ignore (file) + "Add a file name to the ignore file in its directory." + (let* ((fullname (expand-file-name file)) + (dir (file-name-directory fullname)) + (name (file-name-nondirectory fullname)) + (ignore-name (expand-file-name git-per-dir-ignore-file dir)) + (created (not (file-exists-p ignore-name)))) + (save-window-excursion + (set-buffer (find-file-noselect ignore-name)) + (goto-char (point-max)) + (unless (zerop (current-column)) (insert "\n")) + (insert "/" name "\n") + (sort-lines nil (point-min) (point-max)) + (save-buffer)) + (when created + (git-run-command nil nil "update-index" "--info-only" "--add" "--" (file-relative-name ignore-name))) + (git-add-status-file (if created 'added 'modified) (file-relative-name ignore-name)))) + +; propertize definition for XEmacs, stolen from erc-compat +(eval-when-compile + (unless (fboundp 'propertize) + (defun propertize (string &rest props) + (let ((string (copy-sequence string))) + (while props + (put-text-property 0 (length string) (nth 0 props) (nth 1 props) string) + (setq props (cddr props))) + string)))) + +;;;; Wrappers for basic git commands +;;;; ------------------------------------------------------------ + +(defun git-rev-parse (rev) + "Parse a revision name and return its SHA1." + (git-get-string-sha1 + (git-call-process-env-string nil "rev-parse" rev))) + +(defun git-config (key) + "Retrieve the value associated to KEY in the git repository config file." + (let ((str (git-call-process-env-string nil "config" key))) + (and str (car (split-string str "\n"))))) + +(defun git-symbolic-ref (ref) + "Wrapper for the git-symbolic-ref command." + (let ((str (git-call-process-env-string nil "symbolic-ref" ref))) + (and str (car (split-string str "\n"))))) + +(defun git-update-ref (ref val &optional oldval) + "Update a reference by calling git-update-ref." + (apply #'git-call-process-env nil nil "update-ref" ref val (if oldval (list oldval)))) + +(defun git-read-tree (tree &optional index-file) + "Read a tree into the index file." + (apply #'git-call-process-env nil + (if index-file `(("GIT_INDEX_FILE" . ,index-file)) nil) + "read-tree" (if tree (list tree)))) + +(defun git-write-tree (&optional index-file) + "Call git-write-tree and return the resulting tree SHA1 as a string." + (git-get-string-sha1 + (git-call-process-env-string (and index-file `(("GIT_INDEX_FILE" . ,index-file))) "write-tree"))) + +(defun git-commit-tree (buffer tree head) + "Call git-commit-tree with buffer as input and return the resulting commit SHA1." + (let ((author-name (git-get-committer-name)) + (author-email (git-get-committer-email)) + author-date log-start log-end args) + (when head + (push "-p" args) + (push head args)) + (with-current-buffer buffer + (goto-char (point-min)) + (if + (setq log-start (re-search-forward (concat "^" (regexp-quote git-log-msg-separator) "\n") nil t)) + (save-restriction + (narrow-to-region (point-min) log-start) + (goto-char (point-min)) + (when (re-search-forward "^Author: +\\(.*?\\) *<\\(.*\\)> *$" nil t) + (setq author-name (match-string 1) + author-email (match-string 2))) + (goto-char (point-min)) + (when (re-search-forward "^Date: +\\(.*\\)$" nil t) + (setq author-date (match-string 1))) + (goto-char (point-min)) + (while (re-search-forward "^Parent: +\\([0-9a-f]+\\)" nil t) + (unless (string-equal head (match-string 1)) + (push "-p" args) + (push (match-string 1) args)))) + (setq log-start (point-min))) + (setq log-end (point-max))) + (git-get-string-sha1 + (with-output-to-string + (with-current-buffer standard-output + (let ((coding-system-for-write git-commits-coding-system) + (env `(("GIT_AUTHOR_NAME" . ,author-name) + ("GIT_AUTHOR_EMAIL" . ,author-email) + ("GIT_COMMITTER_NAME" . ,(git-get-committer-name)) + ("GIT_COMMITTER_EMAIL" . ,(git-get-committer-email))))) + (when author-date (push `("GIT_AUTHOR_DATE" . ,author-date) env)) + (apply #'git-run-command-region + buffer log-start log-end env + "commit-tree" tree (nreverse args)))))))) + +(defun git-empty-db-p () + "Check if the git db is empty (no commit done yet)." + (not (eq 0 (call-process "git" nil nil nil "rev-parse" "--verify" "HEAD")))) + +(defun git-get-merge-heads () + "Retrieve the merge heads from the MERGE_HEAD file if present." + (let (heads) + (when (file-readable-p ".git/MERGE_HEAD") + (with-temp-buffer + (insert-file-contents ".git/MERGE_HEAD" nil nil nil t) + (goto-char (point-min)) + (while (re-search-forward "[0-9a-f]\\{40\\}" nil t) + (push (match-string 0) heads)))) + (nreverse heads))) + +;;;; File info structure +;;;; ------------------------------------------------------------ + +; fileinfo structure stolen from pcl-cvs +(defstruct (git-fileinfo + (:copier nil) + (:constructor git-create-fileinfo (state name &optional old-perm new-perm rename-state orig-name marked)) + (:conc-name git-fileinfo->)) + marked ;; t/nil + state ;; current state + name ;; file name + old-perm new-perm ;; permission flags + rename-state ;; rename or copy state + orig-name ;; original name for renames or copies + needs-refresh) ;; whether file needs to be refreshed + +(defvar git-status nil) + +(defun git-clear-status (status) + "Remove everything from the status list." + (ewoc-filter status (lambda (info) nil))) + +(defun git-set-files-state (files state) + "Set the state of a list of files." + (dolist (info files) + (unless (eq (git-fileinfo->state info) state) + (setf (git-fileinfo->state info) state) + (setf (git-fileinfo->rename-state info) nil) + (setf (git-fileinfo->orig-name info) nil) + (setf (git-fileinfo->needs-refresh info) t)))) + +(defun git-state-code (code) + "Convert from a string to a added/deleted/modified state." + (case (string-to-char code) + (?M 'modified) + (?? 'unknown) + (?A 'added) + (?D 'deleted) + (?U 'unmerged) + (t nil))) + +(defun git-status-code-as-string (code) + "Format a git status code as string." + (case code + ('modified (propertize "Modified" 'face 'git-status-face)) + ('unknown (propertize "Unknown " 'face 'git-unknown-face)) + ('added (propertize "Added " 'face 'git-status-face)) + ('deleted (propertize "Deleted " 'face 'git-status-face)) + ('unmerged (propertize "Unmerged" 'face 'git-unmerged-face)) + ('uptodate (propertize "Uptodate" 'face 'git-uptodate-face)) + ('ignored (propertize "Ignored " 'face 'git-ignored-face)) + (t "? "))) + +(defun git-rename-as-string (info) + "Return a string describing the copy or rename associated with INFO, or an empty string if none." + (let ((state (git-fileinfo->rename-state info))) + (if state + (propertize + (concat " (" + (if (eq state 'copy) "copied from " + (if (eq (git-fileinfo->state info) 'added) "renamed from " + "renamed to ")) + (git-escape-file-name (git-fileinfo->orig-name info)) + ")") 'face 'git-status-face) + ""))) + +(defun git-permissions-as-string (old-perm new-perm) + "Format a permission change as string." + (propertize + (if (or (not old-perm) + (not new-perm) + (eq 0 (logand ?\111 (logxor old-perm new-perm)))) + " " + (if (eq 0 (logand ?\111 old-perm)) "+x" "-x")) + 'face 'git-permission-face)) + +(defun git-fileinfo-prettyprint (info) + "Pretty-printer for the git-fileinfo structure." + (insert (concat " " (if (git-fileinfo->marked info) (propertize "*" 'face 'git-mark-face) " ") + " " (git-status-code-as-string (git-fileinfo->state info)) + " " (git-permissions-as-string (git-fileinfo->old-perm info) (git-fileinfo->new-perm info)) + " " (git-escape-file-name (git-fileinfo->name info)) + (git-rename-as-string info)))) + +(defun git-parse-status (status) + "Parse the output of git-diff-index in the current buffer." + (goto-char (point-min)) + (while (re-search-forward + ":\\([0-7]\\{6\\}\\) \\([0-7]\\{6\\}\\) [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} \\(\\([ADMU]\\)\0\\([^\0]+\\)\\|\\([CR]\\)[0-9]*\0\\([^\0]+\\)\0\\([^\0]+\\)\\)\0" + nil t 1) + (let ((old-perm (string-to-number (match-string 1) 8)) + (new-perm (string-to-number (match-string 2) 8)) + (state (or (match-string 4) (match-string 6))) + (name (or (match-string 5) (match-string 7))) + (new-name (match-string 8))) + (if new-name ; copy or rename + (if (eq ?C (string-to-char state)) + (ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'copy name)) + (ewoc-enter-last status (git-create-fileinfo 'deleted name 0 0 'rename new-name)) + (ewoc-enter-last status (git-create-fileinfo 'added new-name old-perm new-perm 'rename name))) + (ewoc-enter-last status (git-create-fileinfo (git-state-code state) name old-perm new-perm)))))) + +(defun git-find-status-file (status file) + "Find a given file in the status ewoc and return its node." + (let ((node (ewoc-nth status 0))) + (while (and node (not (string= file (git-fileinfo->name (ewoc-data node))))) + (setq node (ewoc-next status node))) + node)) + +(defun git-parse-ls-files (status default-state &optional skip-existing) + "Parse the output of git-ls-files in the current buffer." + (goto-char (point-min)) + (let (infolist) + (while (re-search-forward "\\([HMRCK?]\\) \\([^\0]*\\)\0" nil t 1) + (let ((state (match-string 1)) + (name (match-string 2))) + (unless (and skip-existing (git-find-status-file status name)) + (push (git-create-fileinfo (or (git-state-code state) default-state) name) infolist)))) + (dolist (info (nreverse infolist)) + (ewoc-enter-last status info)))) + +(defun git-parse-ls-unmerged (status) + "Parse the output of git-ls-files -u in the current buffer." + (goto-char (point-min)) + (let (files) + (while (re-search-forward "[0-7]\\{6\\} [0-9a-f]\\{40\\} [123]\t\\([^\0]+\\)\0" nil t) + (let ((node (git-find-status-file status (match-string 1)))) + (when node (push (ewoc-data node) files)))) + (git-set-files-state files 'unmerged))) + +(defun git-add-status-file (state name) + "Add a new file to the status list (if not existing already) and return its node." + (unless git-status (error "Not in git-status buffer.")) + (or (git-find-status-file git-status name) + (ewoc-enter-last git-status (git-create-fileinfo state name)))) + +(defun git-marked-files () + "Return a list of all marked files, or if none a list containing just the file at cursor position." + (unless git-status (error "Not in git-status buffer.")) + (or (ewoc-collect git-status (lambda (info) (git-fileinfo->marked info))) + (list (ewoc-data (ewoc-locate git-status))))) + +(defun git-marked-files-state (&rest states) + "Return marked files that are in the specified states." + (let ((files (git-marked-files)) + result) + (dolist (info files) + (when (memq (git-fileinfo->state info) states) + (push info result))) + result)) + +(defun git-refresh-files () + "Refresh all files that need it and clear the needs-refresh flag." + (unless git-status (error "Not in git-status buffer.")) + (ewoc-map + (lambda (info) + (let ((refresh (git-fileinfo->needs-refresh info))) + (setf (git-fileinfo->needs-refresh info) nil) + refresh)) + git-status) + ; move back to goal column + (when goal-column (move-to-column goal-column))) + +(defun git-refresh-ewoc-hf (status) + "Refresh the ewoc header and footer." + (let ((branch (git-symbolic-ref "HEAD")) + (head (if (git-empty-db-p) "Nothing committed yet" + (substring (git-rev-parse "HEAD") 0 10))) + (merge-heads (git-get-merge-heads))) + (ewoc-set-hf status + (format "Directory: %s\nBranch: %s\nHead: %s%s\n" + default-directory + (if (string-match "^refs/heads/" branch) + (substring branch (match-end 0)) + branch) + head + (if merge-heads + (concat "\nMerging: " + (mapconcat (lambda (str) (substring str 0 10)) merge-heads " ")) + "")) + (if (ewoc-nth status 0) "" " No changes.")))) + +(defun git-get-filenames (files) + (mapcar (lambda (info) (git-fileinfo->name info)) files)) + +(defun git-update-index (index-file files) + "Run git-update-index on a list of files." + (let ((env (and index-file `(("GIT_INDEX_FILE" . ,index-file)))) + added deleted modified) + (dolist (info files) + (case (git-fileinfo->state info) + ('added (push info added)) + ('deleted (push info deleted)) + ('modified (push info modified)))) + (when added + (apply #'git-run-command nil env "update-index" "--add" "--" (git-get-filenames added))) + (when deleted + (apply #'git-run-command nil env "update-index" "--remove" "--" (git-get-filenames deleted))) + (when modified + (apply #'git-run-command nil env "update-index" "--" (git-get-filenames modified))))) + +(defun git-do-commit () + "Perform the actual commit using the current buffer as log message." + (interactive) + (let ((buffer (current-buffer)) + (index-file (make-temp-file "gitidx"))) + (with-current-buffer log-edit-parent-buffer + (if (git-marked-files-state 'unmerged) + (message "You cannot commit unmerged files, resolve them first.") + (unwind-protect + (let ((files (git-marked-files-state 'added 'deleted 'modified)) + head head-tree) + (unless (git-empty-db-p) + (setq head (git-rev-parse "HEAD") + head-tree (git-rev-parse "HEAD^{tree}"))) + (if files + (progn + (git-read-tree head-tree index-file) + (git-update-index nil files) ;update both the default index + (git-update-index index-file files) ;and the temporary one + (let ((tree (git-write-tree index-file))) + (if (or (not (string-equal tree head-tree)) + (yes-or-no-p "The tree was not modified, do you really want to perform an empty commit? ")) + (let ((commit (git-commit-tree buffer tree head))) + (git-update-ref "HEAD" commit head) + (condition-case nil (delete-file ".git/MERGE_HEAD") (error nil)) + (condition-case nil (delete-file ".git/MERGE_MSG") (error nil)) + (with-current-buffer buffer (erase-buffer)) + (git-set-files-state files 'uptodate) + (when (file-directory-p ".git/rr-cache") + (git-run-command nil nil "rerere")) + (git-refresh-files) + (git-refresh-ewoc-hf git-status) + (message "Committed %s." commit)) + (message "Commit aborted.")))) + (message "No files to commit."))) + (delete-file index-file)))))) + + +;;;; Interactive functions +;;;; ------------------------------------------------------------ + +(defun git-mark-file () + "Mark the file that the cursor is on and move to the next one." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (let* ((pos (ewoc-locate git-status)) + (info (ewoc-data pos))) + (setf (git-fileinfo->marked info) t) + (ewoc-invalidate git-status pos) + (ewoc-goto-next git-status 1))) + +(defun git-unmark-file () + "Unmark the file that the cursor is on and move to the next one." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (let* ((pos (ewoc-locate git-status)) + (info (ewoc-data pos))) + (setf (git-fileinfo->marked info) nil) + (ewoc-invalidate git-status pos) + (ewoc-goto-next git-status 1))) + +(defun git-unmark-file-up () + "Unmark the file that the cursor is on and move to the previous one." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (let* ((pos (ewoc-locate git-status)) + (info (ewoc-data pos))) + (setf (git-fileinfo->marked info) nil) + (ewoc-invalidate git-status pos) + (ewoc-goto-prev git-status 1))) + +(defun git-mark-all () + "Mark all files." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) t) t) git-status) + ; move back to goal column after invalidate + (when goal-column (move-to-column goal-column))) + +(defun git-unmark-all () + "Unmark all files." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) nil) t) git-status) + ; move back to goal column after invalidate + (when goal-column (move-to-column goal-column))) + +(defun git-toggle-all-marks () + "Toggle all file marks." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (ewoc-map (lambda (info) (setf (git-fileinfo->marked info) (not (git-fileinfo->marked info))) t) git-status) + ; move back to goal column after invalidate + (when goal-column (move-to-column goal-column))) + +(defun git-next-file (&optional n) + "Move the selection down N files." + (interactive "p") + (unless git-status (error "Not in git-status buffer.")) + (ewoc-goto-next git-status n)) + +(defun git-prev-file (&optional n) + "Move the selection up N files." + (interactive "p") + (unless git-status (error "Not in git-status buffer.")) + (ewoc-goto-prev git-status n)) + +(defun git-next-unmerged-file (&optional n) + "Move the selection down N unmerged files." + (interactive "p") + (unless git-status (error "Not in git-status buffer.")) + (let* ((last (ewoc-locate git-status)) + (node (ewoc-next git-status last))) + (while (and node (> n 0)) + (when (eq 'unmerged (git-fileinfo->state (ewoc-data node))) + (setq n (1- n)) + (setq last node)) + (setq node (ewoc-next git-status node))) + (ewoc-goto-node git-status last))) + +(defun git-prev-unmerged-file (&optional n) + "Move the selection up N unmerged files." + (interactive "p") + (unless git-status (error "Not in git-status buffer.")) + (let* ((last (ewoc-locate git-status)) + (node (ewoc-prev git-status last))) + (while (and node (> n 0)) + (when (eq 'unmerged (git-fileinfo->state (ewoc-data node))) + (setq n (1- n)) + (setq last node)) + (setq node (ewoc-prev git-status node))) + (ewoc-goto-node git-status last))) + +(defun git-add-file () + "Add marked file(s) to the index cache." + (interactive) + (let ((files (git-marked-files-state 'unknown))) + (unless files + (push (ewoc-data + (git-add-status-file 'added (file-relative-name + (read-file-name "File to add: " nil nil t)))) + files)) + (apply #'git-run-command nil nil "update-index" "--info-only" "--add" "--" (git-get-filenames files)) + (git-set-files-state files 'added) + (git-refresh-files))) + +(defun git-ignore-file () + "Add marked file(s) to the ignore list." + (interactive) + (let ((files (git-marked-files-state 'unknown))) + (unless files + (push (ewoc-data + (git-add-status-file 'unknown (file-relative-name + (read-file-name "File to ignore: " nil nil t)))) + files)) + (dolist (info files) (git-append-to-ignore (git-fileinfo->name info))) + (git-set-files-state files 'ignored) + (git-refresh-files))) + +(defun git-remove-file () + "Remove the marked file(s)." + (interactive) + (let ((files (git-marked-files-state 'added 'modified 'unknown 'uptodate))) + (unless files + (push (ewoc-data + (git-add-status-file 'unknown (file-relative-name + (read-file-name "File to remove: " nil nil t)))) + files)) + (if (yes-or-no-p + (format "Remove %d file%s? " (length files) (if (> (length files) 1) "s" ""))) + (progn + (dolist (info files) + (let ((name (git-fileinfo->name info))) + (when (file-exists-p name) (delete-file name)))) + (apply #'git-run-command nil nil "update-index" "--info-only" "--remove" "--" (git-get-filenames files)) + ; remove unknown files from the list, set the others to deleted + (ewoc-filter git-status + (lambda (info files) + (not (and (memq info files) (eq (git-fileinfo->state info) 'unknown)))) + files) + (git-set-files-state files 'deleted) + (git-refresh-files) + (unless (ewoc-nth git-status 0) ; refresh header if list is empty + (git-refresh-ewoc-hf git-status))) + (message "Aborting")))) + +(defun git-revert-file () + "Revert changes to the marked file(s)." + (interactive) + (let ((files (git-marked-files)) + added modified) + (when (and files + (yes-or-no-p + (format "Revert %d file%s? " (length files) (if (> (length files) 1) "s" "")))) + (dolist (info files) + (case (git-fileinfo->state info) + ('added (push info added)) + ('deleted (push info modified)) + ('unmerged (push info modified)) + ('modified (push info modified)))) + (when added + (apply #'git-run-command nil nil "update-index" "--force-remove" "--" (git-get-filenames added)) + (git-set-files-state added 'unknown)) + (when modified + (apply #'git-run-command nil nil "checkout" "HEAD" (git-get-filenames modified)) + (git-set-files-state modified 'uptodate)) + (git-refresh-files)))) + +(defun git-resolve-file () + "Resolve conflicts in marked file(s)." + (interactive) + (let ((files (git-marked-files-state 'unmerged))) + (when files + (apply #'git-run-command nil nil "update-index" "--" (git-get-filenames files)) + (git-set-files-state files 'modified) + (git-refresh-files)))) + +(defun git-remove-handled () + "Remove handled files from the status list." + (interactive) + (ewoc-filter git-status + (lambda (info) + (not (or (eq (git-fileinfo->state info) 'ignored) + (eq (git-fileinfo->state info) 'uptodate))))) + (unless (ewoc-nth git-status 0) ; refresh header if list is empty + (git-refresh-ewoc-hf git-status))) + +(defun git-setup-diff-buffer (buffer) + "Setup a buffer for displaying a diff." + (with-current-buffer buffer + (diff-mode) + (goto-char (point-min)) + (setq buffer-read-only t)) + (display-buffer buffer) + (shrink-window-if-larger-than-buffer)) + +(defun git-diff-file () + "Diff the marked file(s) against HEAD." + (interactive) + (let ((files (git-marked-files))) + (git-setup-diff-buffer + (apply #'git-run-command-buffer "*git-diff*" "diff-index" "-p" "-M" "HEAD" "--" (git-get-filenames files))))) + +(defun git-diff-file-merge-head (arg) + "Diff the marked file(s) against the first merge head (or the nth one with a numeric prefix)." + (interactive "p") + (let ((files (git-marked-files)) + (merge-heads (git-get-merge-heads))) + (unless merge-heads (error "No merge in progress")) + (git-setup-diff-buffer + (apply #'git-run-command-buffer "*git-diff*" "diff-index" "-p" "-M" + (or (nth (1- arg) merge-heads) "HEAD") "--" (git-get-filenames files))))) + +(defun git-diff-unmerged-file (stage) + "Diff the marked unmerged file(s) against the specified stage." + (let ((files (git-marked-files))) + (git-setup-diff-buffer + (apply #'git-run-command-buffer "*git-diff*" "diff-files" "-p" stage "--" (git-get-filenames files))))) + +(defun git-diff-file-base () + "Diff the marked unmerged file(s) against the common base file." + (interactive) + (git-diff-unmerged-file "-1")) + +(defun git-diff-file-mine () + "Diff the marked unmerged file(s) against my pre-merge version." + (interactive) + (git-diff-unmerged-file "-2")) + +(defun git-diff-file-other () + "Diff the marked unmerged file(s) against the other's pre-merge version." + (interactive) + (git-diff-unmerged-file "-3")) + +(defun git-diff-file-combined () + "Do a combined diff of the marked unmerged file(s)." + (interactive) + (git-diff-unmerged-file "-c")) + +(defun git-diff-file-idiff () + "Perform an interactive diff on the current file." + (interactive) + (error "Interactive diffs not implemented yet.")) + +(defun git-log-file () + "Display a log of changes to the marked file(s)." + (interactive) + (let* ((files (git-marked-files)) + (coding-system-for-read git-commits-coding-system) + (buffer (apply #'git-run-command-buffer "*git-log*" "rev-list" "--pretty" "HEAD" "--" (git-get-filenames files)))) + (with-current-buffer buffer + ; (git-log-mode) FIXME: implement log mode + (goto-char (point-min)) + (setq buffer-read-only t)) + (display-buffer buffer))) + +(defun git-log-edit-files () + "Return a list of marked files for use in the log-edit buffer." + (with-current-buffer log-edit-parent-buffer + (git-get-filenames (git-marked-files-state 'added 'deleted 'modified)))) + +(defun git-commit-file () + "Commit the marked file(s), asking for a commit message." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (let ((buffer (get-buffer-create "*git-commit*")) + (merge-heads (git-get-merge-heads)) + (dir default-directory) + (sign-off git-append-signed-off-by)) + (with-current-buffer buffer + (when (eq 0 (buffer-size)) + (cd dir) + (erase-buffer) + (insert + (propertize + (format "Author: %s <%s>\n%s" + (git-get-committer-name) (git-get-committer-email) + (if merge-heads + (format "Parent: %s\n%s\n" + (git-rev-parse "HEAD") + (mapconcat (lambda (str) (concat "Parent: " str)) merge-heads "\n")) + "")) + 'face 'git-header-face) + (propertize git-log-msg-separator 'face 'git-separator-face) + "\n") + (cond ((file-readable-p ".git/MERGE_MSG") + (insert-file-contents ".git/MERGE_MSG")) + (sign-off + (insert (format "\n\nSigned-off-by: %s <%s>\n" + (git-get-committer-name) (git-get-committer-email))))))) + (log-edit #'git-do-commit nil #'git-log-edit-files buffer) + (setq font-lock-keywords (font-lock-compile-keywords git-log-edit-font-lock-keywords)) + (re-search-forward (regexp-quote (concat git-log-msg-separator "\n")) nil t))) + +(defun git-find-file () + "Visit the current file in its own buffer." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (let ((info (ewoc-data (ewoc-locate git-status)))) + (find-file (git-fileinfo->name info)) + (when (eq 'unmerged (git-fileinfo->state info)) + (smerge-mode)))) + +(defun git-find-file-other-window () + "Visit the current file in its own buffer in another window." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (let ((info (ewoc-data (ewoc-locate git-status)))) + (find-file-other-window (git-fileinfo->name info)) + (when (eq 'unmerged (git-fileinfo->state info)) + (smerge-mode)))) + +(defun git-find-file-imerge () + "Visit the current file in interactive merge mode." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (let ((info (ewoc-data (ewoc-locate git-status)))) + (find-file (git-fileinfo->name info)) + (smerge-ediff))) + +(defun git-view-file () + "View the current file in its own buffer." + (interactive) + (unless git-status (error "Not in git-status buffer.")) + (let ((info (ewoc-data (ewoc-locate git-status)))) + (view-file (git-fileinfo->name info)))) + +(defun git-refresh-status () + "Refresh the git status buffer." + (interactive) + (let* ((status git-status) + (pos (ewoc-locate status)) + (cur-name (and pos (git-fileinfo->name (ewoc-data pos))))) + (unless status (error "Not in git-status buffer.")) + (git-clear-status status) + (git-run-command nil nil "update-index" "--info-only" "--refresh") + (if (git-empty-db-p) + ; we need some special handling for an empty db + (with-temp-buffer + (git-run-command t nil "ls-files" "-z" "-t" "-c") + (git-parse-ls-files status 'added)) + (with-temp-buffer + (git-run-command t nil "diff-index" "-z" "-M" "HEAD") + (git-parse-status status))) + (with-temp-buffer + (git-run-command t nil "ls-files" "-z" "-u") + (git-parse-ls-unmerged status)) + (when (file-readable-p ".git/info/exclude") + (with-temp-buffer + (git-run-command t nil "ls-files" "-z" "-t" "-o" + "--exclude-from=.git/info/exclude" + (concat "--exclude-per-directory=" git-per-dir-ignore-file)) + (git-parse-ls-files status 'unknown))) + (git-refresh-files) + (git-refresh-ewoc-hf status) + ; move point to the current file name if any + (let ((node (and cur-name (git-find-status-file status cur-name)))) + (when node (ewoc-goto-node status node))))) + +(defun git-status-quit () + "Quit git-status mode." + (interactive) + (bury-buffer)) + +;;;; Major Mode +;;;; ------------------------------------------------------------ + +(defvar git-status-mode-hook nil + "Run after `git-status-mode' is setup.") + +(defvar git-status-mode-map nil + "Keymap for git major mode.") + +(defvar git-status nil + "List of all files managed by the git-status mode.") + +(unless git-status-mode-map + (let ((map (make-keymap)) + (diff-map (make-sparse-keymap))) + (suppress-keymap map) + (define-key map "?" 'git-help) + (define-key map "h" 'git-help) + (define-key map " " 'git-next-file) + (define-key map "a" 'git-add-file) + (define-key map "c" 'git-commit-file) + (define-key map "d" diff-map) + (define-key map "=" 'git-diff-file) + (define-key map "f" 'git-find-file) + (define-key map "\r" 'git-find-file) + (define-key map "g" 'git-refresh-status) + (define-key map "i" 'git-ignore-file) + (define-key map "l" 'git-log-file) + (define-key map "m" 'git-mark-file) + (define-key map "M" 'git-mark-all) + (define-key map "n" 'git-next-file) + (define-key map "N" 'git-next-unmerged-file) + (define-key map "o" 'git-find-file-other-window) + (define-key map "p" 'git-prev-file) + (define-key map "P" 'git-prev-unmerged-file) + (define-key map "q" 'git-status-quit) + (define-key map "r" 'git-remove-file) + (define-key map "R" 'git-resolve-file) + (define-key map "T" 'git-toggle-all-marks) + (define-key map "u" 'git-unmark-file) + (define-key map "U" 'git-revert-file) + (define-key map "v" 'git-view-file) + (define-key map "x" 'git-remove-handled) + (define-key map "\C-?" 'git-unmark-file-up) + (define-key map "\M-\C-?" 'git-unmark-all) + ; the diff submap + (define-key diff-map "b" 'git-diff-file-base) + (define-key diff-map "c" 'git-diff-file-combined) + (define-key diff-map "=" 'git-diff-file) + (define-key diff-map "e" 'git-diff-file-idiff) + (define-key diff-map "E" 'git-find-file-imerge) + (define-key diff-map "h" 'git-diff-file-merge-head) + (define-key diff-map "m" 'git-diff-file-mine) + (define-key diff-map "o" 'git-diff-file-other) + (setq git-status-mode-map map))) + +;; git mode should only run in the *git status* buffer +(put 'git-status-mode 'mode-class 'special) + +(defun git-status-mode () + "Major mode for interacting with Git. +Commands: +\\{git-status-mode-map}" + (kill-all-local-variables) + (buffer-disable-undo) + (setq mode-name "git status" + major-mode 'git-status-mode + goal-column 17 + buffer-read-only t) + (use-local-map git-status-mode-map) + (let ((buffer-read-only nil)) + (erase-buffer) + (let ((status (ewoc-create 'git-fileinfo-prettyprint "" ""))) + (set (make-local-variable 'git-status) status)) + (set (make-local-variable 'list-buffers-directory) default-directory) + (run-hooks 'git-status-mode-hook))) + +(defun git-find-status-buffer (dir) + "Find the git status buffer handling a specified directory." + (let ((list (buffer-list)) + (fulldir (expand-file-name dir)) + found) + (while (and list (not found)) + (let ((buffer (car list))) + (with-current-buffer buffer + (when (and list-buffers-directory + (string-equal fulldir (expand-file-name list-buffers-directory)) + (string-match "\\*git-status\\*$" (buffer-name buffer))) + (setq found buffer)))) + (setq list (cdr list))) + found)) + +(defun git-status (dir) + "Entry point into git-status mode." + (interactive "DSelect directory: ") + (setq dir (git-get-top-dir dir)) + (if (file-directory-p (concat (file-name-as-directory dir) ".git")) + (let ((buffer (or (and git-reuse-status-buffer (git-find-status-buffer dir)) + (create-file-buffer (expand-file-name "*git-status*" dir))))) + (switch-to-buffer buffer) + (cd dir) + (git-status-mode) + (git-refresh-status) + (goto-char (point-min))) + (message "%s is not a git working tree." dir))) + +(defun git-help () + "Display help for Git mode." + (interactive) + (describe-function 'git-status-mode)) + +(provide 'git) +;;; git.el ends here diff --git a/contrib/emacs/vc-git.el b/contrib/emacs/vc-git.el new file mode 100644 index 0000000000..e456ab9712 --- /dev/null +++ b/contrib/emacs/vc-git.el @@ -0,0 +1,151 @@ +;;; vc-git.el --- VC backend for the git version control system + +;; Copyright (C) 2006 Alexandre Julliard + +;; This program is free software; you can redistribute it and/or +;; modify it under the terms of the GNU General Public License as +;; published by the Free Software Foundation; either version 2 of +;; the License, or (at your option) any later version. +;; +;; This program is distributed in the hope that it will be +;; useful, but WITHOUT ANY WARRANTY; without even the implied +;; warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +;; PURPOSE. See the GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public +;; License along with this program; if not, write to the Free +;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +;; MA 02111-1307 USA + +;;; Commentary: + +;; This file contains a VC backend for the git version control +;; system. +;; +;; To install: put this file on the load-path and add GIT to the list +;; of supported backends in `vc-handled-backends'; the following line, +;; placed in your ~/.emacs, will accomplish this: +;; +;; (add-to-list 'vc-handled-backends 'GIT) +;; +;; TODO +;; - changelog generation +;; - working with revisions other than HEAD +;; + +(eval-when-compile (require 'cl)) + +(defvar git-commits-coding-system 'utf-8 + "Default coding system for git commits.") + +(defun vc-git--run-command-string (file &rest args) + "Run a git command on FILE and return its output as string." + (let* ((ok t) + (str (with-output-to-string + (with-current-buffer standard-output + (unless (eq 0 (apply #'call-process "git" nil '(t nil) nil + (append args (list (file-relative-name file))))) + (setq ok nil)))))) + (and ok str))) + +(defun vc-git--run-command (file &rest args) + "Run a git command on FILE, discarding any output." + (let ((name (file-relative-name file))) + (eq 0 (apply #'call-process "git" nil (get-buffer "*Messages") nil (append args (list name)))))) + +(defun vc-git-registered (file) + "Check whether FILE is registered with git." + (with-temp-buffer + (let* ((dir (file-name-directory file)) + (name (file-relative-name file dir))) + (and (ignore-errors + (when dir (cd dir)) + (eq 0 (call-process "git" nil '(t nil) nil "ls-files" "-c" "-z" "--" name))) + (let ((str (buffer-string))) + (and (> (length str) (length name)) + (string= (substring str 0 (1+ (length name))) (concat name "\0")))))))) + +(defun vc-git-state (file) + "git-specific version of `vc-state'." + (let ((diff (vc-git--run-command-string file "diff-index" "-z" "HEAD" "--"))) + (if (and diff (string-match ":[0-7]\\{6\\} [0-7]\\{6\\} [0-9a-f]\\{40\\} [0-9a-f]\\{40\\} [ADMU]\0[^\0]+\0" diff)) + 'edited + 'up-to-date))) + +(defun vc-git-workfile-version (file) + "git-specific version of `vc-workfile-version'." + (let ((str (with-output-to-string + (with-current-buffer standard-output + (call-process "git" nil '(t nil) nil "symbolic-ref" "HEAD"))))) + (if (string-match "^\\(refs/heads/\\)?\\(.+\\)$" str) + (match-string 2 str) + str))) + +(defun vc-git-revert (file &optional contents-done) + "Revert FILE to the version stored in the git repository." + (if contents-done + (vc-git--run-command file "update-index" "--") + (vc-git--run-command file "checkout" "HEAD"))) + +(defun vc-git-checkout-model (file) + 'implicit) + +(defun vc-git-workfile-unchanged-p (file) + (let ((sha1 (vc-git--run-command-string file "hash-object" "--")) + (head (vc-git--run-command-string file "ls-tree" "-z" "HEAD" "--"))) + (and head + (string-match "[0-7]\\{6\\} blob \\([0-9a-f]\\{40\\}\\)\t[^\0]+\0" head) + (string= (car (split-string sha1 "\n")) (match-string 1 head))))) + +(defun vc-git-register (file &optional rev comment) + "Register FILE into the git version-control system." + (vc-git--run-command file "update-index" "--add" "--")) + +(defun vc-git-print-log (file &optional buffer) + (let ((name (file-relative-name file)) + (coding-system-for-read git-commits-coding-system)) + (vc-do-command buffer 'async "git" name "rev-list" "--pretty" "HEAD" "--"))) + +(defun vc-git-diff (file &optional rev1 rev2 buffer) + (let ((name (file-relative-name file)) + (buf (or buffer "*vc-diff*"))) + (if (and rev1 rev2) + (vc-do-command buf 0 "git" name "diff-tree" "-p" rev1 rev2 "--") + (vc-do-command buf 0 "git" name "diff-index" "-p" (or rev1 "HEAD") "--")) + ; git-diff-index doesn't set exit status like diff does + (if (vc-git-workfile-unchanged-p file) 0 1))) + +(defun vc-git-checkin (file rev comment) + (let ((coding-system-for-write git-commits-coding-system)) + (vc-git--run-command file "commit" "-m" comment "--only" "--"))) + +(defun vc-git-checkout (file &optional editable rev destfile) + (if destfile + (let ((fullname (substring + (vc-git--run-command-string file "ls-files" "-z" "--full-name" "--") + 0 -1)) + (coding-system-for-read 'no-conversion) + (coding-system-for-write 'no-conversion)) + (with-temp-file destfile + (eq 0 (call-process "git" nil t nil "cat-file" "blob" + (concat (or rev "HEAD") ":" fullname))))) + (vc-git--run-command file "checkout" (or rev "HEAD")))) + +(defun vc-git-annotate-command (file buf &optional rev) + ; FIXME: rev is ignored + (let ((name (file-relative-name file))) + (call-process "git" nil buf nil "blame" name))) + +(defun vc-git-annotate-time () + (and (re-search-forward "[0-9a-f]+ (.* \\([0-9]+\\)-\\([0-9]+\\)-\\([0-9]+\\) \\([0-9]+\\):\\([0-9]+\\):\\([0-9]+\\) \\([-+0-9]+\\) +[0-9]+)" nil t) + (vc-annotate-convert-time + (apply #'encode-time (mapcar (lambda (match) (string-to-number (match-string match))) '(6 5 4 3 2 1 7)))))) + +;; Not really useful since we can't do anything with the revision yet +;;(defun vc-annotate-extract-revision-at-line () +;; (save-excursion +;; (move-beginning-of-line 1) +;; (and (looking-at "[0-9a-f]+") +;; (buffer-substring (match-beginning 0) (match-end 0))))) + +(provide 'vc-git) diff --git a/contrib/fast-import/import-tars.perl b/contrib/fast-import/import-tars.perl new file mode 100755 index 0000000000..26c42c9780 --- /dev/null +++ b/contrib/fast-import/import-tars.perl @@ -0,0 +1,105 @@ +#!/usr/bin/perl + +## tar archive frontend for git-fast-import +## +## For example: +## +## mkdir project; cd project; git init +## perl import-tars.perl *.tar.bz2 +## git whatchanged import-tars +## + +use strict; +die "usage: import-tars *.tar.{gz,bz2,Z}\n" unless @ARGV; + +my $branch_name = 'import-tars'; +my $branch_ref = "refs/heads/$branch_name"; +my $committer_name = 'T Ar Creator'; +my $committer_email = 'tar@example.com'; + +open(FI, '|-', 'git', 'fast-import', '--quiet') + or die "Unable to start git fast-import: $!\n"; +foreach my $tar_file (@ARGV) +{ + $tar_file =~ m,([^/]+)$,; + my $tar_name = $1; + + if ($tar_name =~ s/\.(tar\.gz|tgz)$//) { + open(I, '-|', 'gzcat', $tar_file) or die "Unable to gzcat $tar_file: $!\n"; + } elsif ($tar_name =~ s/\.(tar\.bz2|tbz2)$//) { + open(I, '-|', 'bzcat', $tar_file) or die "Unable to bzcat $tar_file: $!\n"; + } elsif ($tar_name =~ s/\.tar\.Z$//) { + open(I, '-|', 'zcat', $tar_file) or die "Unable to zcat $tar_file: $!\n"; + } elsif ($tar_name =~ s/\.tar$//) { + open(I, $tar_file) or die "Unable to open $tar_file: $!\n"; + } else { + die "Unrecognized compression format: $tar_file\n"; + } + + my $commit_time = 0; + my $next_mark = 1; + my $have_top_dir = 1; + my ($top_dir, %files); + + while (read(I, $_, 512) == 512) { + my ($name, $mode, $uid, $gid, $size, $mtime, + $chksum, $typeflag, $linkname, $magic, + $version, $uname, $gname, $devmajor, $devminor, + $prefix) = unpack 'Z100 Z8 Z8 Z8 Z12 Z12 + Z8 Z1 Z100 Z6 + Z2 Z32 Z32 Z8 Z8 Z*', $_; + last unless $name; + $mode = oct $mode; + $size = oct $size; + $mtime = oct $mtime; + next if $mode & 0040000; + + print FI "blob\n", "mark :$next_mark\n", "data $size\n"; + while ($size > 0 && read(I, $_, 512) == 512) { + print FI substr($_, 0, $size); + $size -= 512; + } + print FI "\n"; + + my $path = "$prefix$name"; + $files{$path} = [$next_mark++, $mode]; + + $commit_time = $mtime if $mtime > $commit_time; + $path =~ m,^([^/]+)/,; + $top_dir = $1 unless $top_dir; + $have_top_dir = 0 if $top_dir ne $1; + } + + print FI <<EOF; +commit $branch_ref +committer $committer_name <$committer_email> $commit_time +0000 +data <<END_OF_COMMIT_MESSAGE +Imported from $tar_file. +END_OF_COMMIT_MESSAGE + +deleteall +EOF + + foreach my $path (keys %files) + { + my ($mark, $mode) = @{$files{$path}}; + my $git_mode = 0644; + $git_mode |= 0700 if $mode & 0111; + $path =~ s,^([^/]+)/,, if $have_top_dir; + printf FI "M %o :%i %s\n", $git_mode, $mark, $path; + } + print FI "\n"; + + print FI <<EOF; +tag $tar_name +from $branch_ref +tagger $committer_name <$committer_email> $commit_time +0000 +data <<END_OF_TAG_MESSAGE +Package $tar_name +END_OF_TAG_MESSAGE + +EOF + + close I; +} +close FI; diff --git a/contrib/gitview/gitview b/contrib/gitview/gitview new file mode 100755 index 0000000000..521b2fcd32 --- /dev/null +++ b/contrib/gitview/gitview @@ -0,0 +1,1029 @@ +#! /usr/bin/env python + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +""" gitview +GUI browser for git repository +This program is based on bzrk by Scott James Remnant <scott@ubuntu.com> +""" +__copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P." +__author__ = "Aneesh Kumar K.V <aneesh.kumar@hp.com>" + + +import sys +import os +import gtk +import pygtk +import pango +import re +import time +import gobject +import cairo +import math +import string + +try: + import gtksourceview + have_gtksourceview = True +except ImportError: + have_gtksourceview = False + print "Running without gtksourceview module" + +re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})') + +def list_to_string(args, skip): + count = len(args) + i = skip + str_arg=" " + while (i < count ): + str_arg = str_arg + args[i] + str_arg = str_arg + " " + i = i+1 + + return str_arg + +def show_date(epoch, tz): + secs = float(epoch) + tzsecs = float(tz[1:3]) * 3600 + tzsecs += float(tz[3:5]) * 60 + if (tz[0] == "+"): + secs += tzsecs + else: + secs -= tzsecs + + return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs)) + + +class CellRendererGraph(gtk.GenericCellRenderer): + """Cell renderer for directed graph. + + This module contains the implementation of a custom GtkCellRenderer that + draws part of the directed graph based on the lines suggested by the code + in graph.py. + + Because we're shiny, we use Cairo to do this, and because we're naughty + we cheat and draw over the bits of the TreeViewColumn that are supposed to + just be for the background. + + Properties: + node (column, colour, [ names ]) tuple to draw revision node, + in_lines (start, end, colour) tuple list to draw inward lines, + out_lines (start, end, colour) tuple list to draw outward lines. + """ + + __gproperties__ = { + "node": ( gobject.TYPE_PYOBJECT, "node", + "revision node instruction", + gobject.PARAM_WRITABLE + ), + "in-lines": ( gobject.TYPE_PYOBJECT, "in-lines", + "instructions to draw lines into the cell", + gobject.PARAM_WRITABLE + ), + "out-lines": ( gobject.TYPE_PYOBJECT, "out-lines", + "instructions to draw lines out of the cell", + gobject.PARAM_WRITABLE + ), + } + + def do_set_property(self, property, value): + """Set properties from GObject properties.""" + if property.name == "node": + self.node = value + elif property.name == "in-lines": + self.in_lines = value + elif property.name == "out-lines": + self.out_lines = value + else: + raise AttributeError, "no such property: '%s'" % property.name + + def box_size(self, widget): + """Calculate box size based on widget's font. + + Cache this as it's probably expensive to get. It ensures that we + draw the graph at least as large as the text. + """ + try: + return self._box_size + except AttributeError: + pango_ctx = widget.get_pango_context() + font_desc = widget.get_style().font_desc + metrics = pango_ctx.get_metrics(font_desc) + + ascent = pango.PIXELS(metrics.get_ascent()) + descent = pango.PIXELS(metrics.get_descent()) + + self._box_size = ascent + descent + 6 + return self._box_size + + def set_colour(self, ctx, colour, bg, fg): + """Set the context source colour. + + Picks a distinct colour based on an internal wheel; the bg + parameter provides the value that should be assigned to the 'zero' + colours and the fg parameter provides the multiplier that should be + applied to the foreground colours. + """ + colours = [ + ( 1.0, 0.0, 0.0 ), + ( 1.0, 1.0, 0.0 ), + ( 0.0, 1.0, 0.0 ), + ( 0.0, 1.0, 1.0 ), + ( 0.0, 0.0, 1.0 ), + ( 1.0, 0.0, 1.0 ), + ] + + colour %= len(colours) + red = (colours[colour][0] * fg) or bg + green = (colours[colour][1] * fg) or bg + blue = (colours[colour][2] * fg) or bg + + ctx.set_source_rgb(red, green, blue) + + def on_get_size(self, widget, cell_area): + """Return the size we need for this cell. + + Each cell is drawn individually and is only as wide as it needs + to be, we let the TreeViewColumn take care of making them all + line up. + """ + box_size = self.box_size(widget) + + cols = self.node[0] + for start, end, colour in self.in_lines + self.out_lines: + cols = int(max(cols, start, end)) + + (column, colour, names) = self.node + names_len = 0 + if (len(names) != 0): + for item in names: + names_len += len(item) + + width = box_size * (cols + 1 ) + names_len + height = box_size + + # FIXME I have no idea how to use cell_area properly + return (0, 0, width, height) + + def on_render(self, window, widget, bg_area, cell_area, exp_area, flags): + """Render an individual cell. + + Draws the cell contents using cairo, taking care to clip what we + do to within the background area so we don't draw over other cells. + Note that we're a bit naughty there and should really be drawing + in the cell_area (or even the exposed area), but we explicitly don't + want any gutter. + + We try and be a little clever, if the line we need to draw is going + to cross other columns we actually draw it as in the .---' style + instead of a pure diagonal ... this reduces confusion by an + incredible amount. + """ + ctx = window.cairo_create() + ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height) + ctx.clip() + + box_size = self.box_size(widget) + + ctx.set_line_width(box_size / 8) + ctx.set_line_cap(cairo.LINE_CAP_SQUARE) + + # Draw lines into the cell + for start, end, colour in self.in_lines: + ctx.move_to(cell_area.x + box_size * start + box_size / 2, + bg_area.y - bg_area.height / 2) + + if start - end > 1: + ctx.line_to(cell_area.x + box_size * start, bg_area.y) + ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y) + elif start - end < -1: + ctx.line_to(cell_area.x + box_size * start + box_size, + bg_area.y) + ctx.line_to(cell_area.x + box_size * end, bg_area.y) + + ctx.line_to(cell_area.x + box_size * end + box_size / 2, + bg_area.y + bg_area.height / 2) + + self.set_colour(ctx, colour, 0.0, 0.65) + ctx.stroke() + + # Draw lines out of the cell + for start, end, colour in self.out_lines: + ctx.move_to(cell_area.x + box_size * start + box_size / 2, + bg_area.y + bg_area.height / 2) + + if start - end > 1: + ctx.line_to(cell_area.x + box_size * start, + bg_area.y + bg_area.height) + ctx.line_to(cell_area.x + box_size * end + box_size, + bg_area.y + bg_area.height) + elif start - end < -1: + ctx.line_to(cell_area.x + box_size * start + box_size, + bg_area.y + bg_area.height) + ctx.line_to(cell_area.x + box_size * end, + bg_area.y + bg_area.height) + + ctx.line_to(cell_area.x + box_size * end + box_size / 2, + bg_area.y + bg_area.height / 2 + bg_area.height) + + self.set_colour(ctx, colour, 0.0, 0.65) + ctx.stroke() + + # Draw the revision node in the right column + (column, colour, names) = self.node + ctx.arc(cell_area.x + box_size * column + box_size / 2, + cell_area.y + cell_area.height / 2, + box_size / 4, 0, 2 * math.pi) + + + self.set_colour(ctx, colour, 0.0, 0.5) + ctx.stroke_preserve() + + self.set_colour(ctx, colour, 0.5, 1.0) + ctx.fill_preserve() + + if (len(names) != 0): + name = " " + for item in names: + name = name + item + " " + + ctx.set_font_size(13) + if (flags & 1): + self.set_colour(ctx, colour, 0.5, 1.0) + else: + self.set_colour(ctx, colour, 0.0, 0.5) + ctx.show_text(name) + +class Commit: + """ This represent a commit object obtained after parsing the git-rev-list + output """ + + children_sha1 = {} + + def __init__(self, commit_lines): + self.message = "" + self.author = "" + self.date = "" + self.committer = "" + self.commit_date = "" + self.commit_sha1 = "" + self.parent_sha1 = [ ] + self.parse_commit(commit_lines) + + + def parse_commit(self, commit_lines): + + # First line is the sha1 lines + line = string.strip(commit_lines[0]) + sha1 = re.split(" ", line) + self.commit_sha1 = sha1[0] + self.parent_sha1 = sha1[1:] + + #build the child list + for parent_id in self.parent_sha1: + try: + Commit.children_sha1[parent_id].append(self.commit_sha1) + except KeyError: + Commit.children_sha1[parent_id] = [self.commit_sha1] + + # IF we don't have parent + if (len(self.parent_sha1) == 0): + self.parent_sha1 = [0] + + for line in commit_lines[1:]: + m = re.match("^ ", line) + if (m != None): + # First line of the commit message used for short log + if self.message == "": + self.message = string.strip(line) + continue + + m = re.match("tree", line) + if (m != None): + continue + + m = re.match("parent", line) + if (m != None): + continue + + m = re_ident.match(line) + if (m != None): + date = show_date(m.group('epoch'), m.group('tz')) + if m.group(1) == "author": + self.author = m.group('ident') + self.date = date + elif m.group(1) == "committer": + self.committer = m.group('ident') + self.commit_date = date + + continue + + def get_message(self, with_diff=0): + if (with_diff == 1): + message = self.diff_tree() + else: + fp = os.popen("git cat-file commit " + self.commit_sha1) + message = fp.read() + fp.close() + + return message + + def diff_tree(self): + fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1) + diff = fp.read() + fp.close() + return diff + +class DiffWindow: + """Diff window. + This object represents and manages a single window containing the + differences between two revisions on a branch. + """ + + def __init__(self): + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.set_border_width(0) + self.window.set_title("Git repository browser diff window") + + # Use two thirds of the screen by default + screen = self.window.get_screen() + monitor = screen.get_monitor_geometry(0) + width = int(monitor.width * 0.66) + height = int(monitor.height * 0.66) + self.window.set_default_size(width, height) + + self.construct() + + def construct(self): + """Construct the window contents.""" + vbox = gtk.VBox() + self.window.add(vbox) + vbox.show() + + menu_bar = gtk.MenuBar() + save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE) + save_menu.connect("activate", self.save_menu_response, "save") + save_menu.show() + menu_bar.append(save_menu) + vbox.pack_start(menu_bar, expand=False, fill=True) + menu_bar.show() + + scrollwin = gtk.ScrolledWindow() + scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrollwin.set_shadow_type(gtk.SHADOW_IN) + vbox.pack_start(scrollwin, expand=True, fill=True) + scrollwin.show() + + if have_gtksourceview: + self.buffer = gtksourceview.SourceBuffer() + slm = gtksourceview.SourceLanguagesManager() + gsl = slm.get_language_from_mime_type("text/x-patch") + self.buffer.set_highlight(True) + self.buffer.set_language(gsl) + sourceview = gtksourceview.SourceView(self.buffer) + else: + self.buffer = gtk.TextBuffer() + sourceview = gtk.TextView(self.buffer) + + sourceview.set_editable(False) + sourceview.modify_font(pango.FontDescription("Monospace")) + scrollwin.add(sourceview) + sourceview.show() + + + def set_diff(self, commit_sha1, parent_sha1, encoding): + """Set the differences showed by this window. + Compares the two trees and populates the window with the + differences. + """ + # Diff with the first commit or the last commit shows nothing + if (commit_sha1 == 0 or parent_sha1 == 0 ): + return + + fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1) + self.buffer.set_text(unicode(fp.read(), encoding).encode('utf-8')) + fp.close() + self.window.show() + + def save_menu_response(self, widget, string): + dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_SAVE, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + response = dialog.run() + if response == gtk.RESPONSE_OK: + patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(), + self.buffer.get_end_iter()) + fp = open(dialog.get_filename(), "w") + fp.write(patch_buffer) + fp.close() + dialog.destroy() + +class GitView: + """ This is the main class + """ + version = "0.8" + + def __init__(self, with_diff=0): + self.with_diff = with_diff + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.set_border_width(0) + self.window.set_title("Git repository browser") + + self.get_encoding() + self.get_bt_sha1() + + # Use three-quarters of the screen by default + screen = self.window.get_screen() + monitor = screen.get_monitor_geometry(0) + width = int(monitor.width * 0.75) + height = int(monitor.height * 0.75) + self.window.set_default_size(width, height) + + # FIXME AndyFitz! + icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON) + self.window.set_icon(icon) + + 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.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 + respective sha1 details """ + + self.bt_sha1 = { } + ls_remote = re.compile('^(.{40})\trefs/([^^]+)(?:\\^(..))?$'); + fp = os.popen('git ls-remote "${GIT_DIR-.git}"') + while 1: + line = string.strip(fp.readline()) + if line == '': + break + m = ls_remote.match(line) + if not m: + continue + (sha1, name) = (m.group(1), m.group(2)) + if not self.bt_sha1.has_key(sha1): + self.bt_sha1[sha1] = [] + self.bt_sha1[sha1].append(name) + fp.close() + + def get_encoding(self): + fp = os.popen("git config --get i18n.commitencoding") + self.encoding=string.strip(fp.readline()) + fp.close() + if (self.encoding == ""): + self.encoding = "utf-8" + + + def construct(self): + """Construct the window contents.""" + vbox = gtk.VBox() + paned = gtk.VPaned() + paned.pack1(self.construct_top(), resize=False, shrink=True) + paned.pack2(self.construct_bottom(), resize=False, shrink=True) + menu_bar = gtk.MenuBar() + menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL) + help_menu = gtk.MenuItem("Help") + menu = gtk.Menu() + about_menu = gtk.MenuItem("About") + menu.append(about_menu) + about_menu.connect("activate", self.about_menu_response, "about") + about_menu.show() + help_menu.set_submenu(menu) + help_menu.show() + menu_bar.append(help_menu) + menu_bar.show() + vbox.pack_start(menu_bar, expand=False, fill=True) + vbox.pack_start(paned, expand=True, fill=True) + paned.show() + vbox.show() + return vbox + + + def construct_top(self): + """Construct the top-half of the window.""" + vbox = gtk.VBox(spacing=6) + vbox.set_border_width(12) + vbox.show() + + + scrollwin = gtk.ScrolledWindow() + scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrollwin.set_shadow_type(gtk.SHADOW_IN) + vbox.pack_start(scrollwin, expand=True, fill=True) + scrollwin.show() + + self.treeview = gtk.TreeView() + self.treeview.set_rules_hint(True) + self.treeview.set_search_column(4) + self.treeview.connect("cursor-changed", self._treeview_cursor_cb) + scrollwin.add(self.treeview) + self.treeview.show() + + cell = CellRendererGraph() + column = gtk.TreeViewColumn() + column.set_resizable(True) + column.pack_start(cell, expand=True) + column.add_attribute(cell, "node", 1) + column.add_attribute(cell, "in-lines", 2) + column.add_attribute(cell, "out-lines", 3) + self.treeview.append_column(column) + + cell = gtk.CellRendererText() + cell.set_property("width-chars", 65) + cell.set_property("ellipsize", pango.ELLIPSIZE_END) + column = gtk.TreeViewColumn("Message") + column.set_resizable(True) + column.pack_start(cell, expand=True) + column.add_attribute(cell, "text", 4) + self.treeview.append_column(column) + + cell = gtk.CellRendererText() + cell.set_property("width-chars", 40) + cell.set_property("ellipsize", pango.ELLIPSIZE_END) + column = gtk.TreeViewColumn("Author") + column.set_resizable(True) + column.pack_start(cell, expand=True) + column.add_attribute(cell, "text", 5) + self.treeview.append_column(column) + + cell = gtk.CellRendererText() + cell.set_property("ellipsize", pango.ELLIPSIZE_END) + column = gtk.TreeViewColumn("Date") + column.set_resizable(True) + column.pack_start(cell, expand=True) + column.add_attribute(cell, "text", 6) + self.treeview.append_column(column) + + return vbox + + def about_menu_response(self, widget, string): + dialog = gtk.AboutDialog() + dialog.set_name("Gitview") + dialog.set_version(GitView.version) + dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"]) + dialog.set_website("http://www.kernel.org/pub/software/scm/git/") + dialog.set_copyright("Use and distribute under the terms of the GNU General Public License") + dialog.set_wrap_license(True) + dialog.run() + dialog.destroy() + + + def construct_bottom(self): + """Construct the bottom half of the window.""" + vbox = gtk.VBox(False, spacing=6) + vbox.set_border_width(12) + (width, height) = self.window.get_size() + vbox.set_size_request(width, int(height / 2.5)) + vbox.show() + + self.table = gtk.Table(rows=4, columns=4) + self.table.set_row_spacings(6) + self.table.set_col_spacings(6) + vbox.pack_start(self.table, expand=False, fill=True) + self.table.show() + + align = gtk.Alignment(0.0, 0.5) + label = gtk.Label() + label.set_markup("<b>Revision:</b>") + align.add(label) + self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL) + label.show() + align.show() + + align = gtk.Alignment(0.0, 0.5) + self.revid_label = gtk.Label() + self.revid_label.set_selectable(True) + align.add(self.revid_label) + self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL) + self.revid_label.show() + align.show() + + align = gtk.Alignment(0.0, 0.5) + label = gtk.Label() + label.set_markup("<b>Committer:</b>") + align.add(label) + self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL) + label.show() + align.show() + + align = gtk.Alignment(0.0, 0.5) + self.committer_label = gtk.Label() + self.committer_label.set_selectable(True) + align.add(self.committer_label) + self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL) + self.committer_label.show() + align.show() + + align = gtk.Alignment(0.0, 0.5) + label = gtk.Label() + label.set_markup("<b>Timestamp:</b>") + align.add(label) + self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL) + label.show() + align.show() + + align = gtk.Alignment(0.0, 0.5) + self.timestamp_label = gtk.Label() + self.timestamp_label.set_selectable(True) + align.add(self.timestamp_label) + self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL) + self.timestamp_label.show() + align.show() + + align = gtk.Alignment(0.0, 0.5) + label = gtk.Label() + label.set_markup("<b>Parents:</b>") + align.add(label) + self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL) + label.show() + align.show() + self.parents_widgets = [] + + align = gtk.Alignment(0.0, 0.5) + label = gtk.Label() + label.set_markup("<b>Children:</b>") + align.add(label) + self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL) + label.show() + align.show() + self.children_widgets = [] + + scrollwin = gtk.ScrolledWindow() + scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrollwin.set_shadow_type(gtk.SHADOW_IN) + vbox.pack_start(scrollwin, expand=True, fill=True) + scrollwin.show() + + if have_gtksourceview: + self.message_buffer = gtksourceview.SourceBuffer() + slm = gtksourceview.SourceLanguagesManager() + gsl = slm.get_language_from_mime_type("text/x-patch") + self.message_buffer.set_highlight(True) + self.message_buffer.set_language(gsl) + sourceview = gtksourceview.SourceView(self.message_buffer) + else: + self.message_buffer = gtk.TextBuffer() + sourceview = gtk.TextView(self.message_buffer) + + sourceview.set_editable(False) + sourceview.modify_font(pango.FontDescription("Monospace")) + scrollwin.add(sourceview) + sourceview.show() + + return vbox + + def _treeview_cursor_cb(self, *args): + """Callback for when the treeview cursor changes.""" + (path, col) = self.treeview.get_cursor() + commit = self.model[path][0] + + if commit.committer is not None: + committer = commit.committer + timestamp = commit.commit_date + message = commit.get_message(self.with_diff) + revid_label = commit.commit_sha1 + else: + committer = "" + timestamp = "" + message = "" + revid_label = "" + + self.revid_label.set_text(revid_label) + self.committer_label.set_text(committer) + self.timestamp_label.set_text(timestamp) + self.message_buffer.set_text(unicode(message, self.encoding).encode('utf-8')) + + for widget in self.parents_widgets: + self.table.remove(widget) + + self.parents_widgets = [] + self.table.resize(4 + len(commit.parent_sha1) - 1, 4) + for idx, parent_id in enumerate(commit.parent_sha1): + self.table.set_row_spacing(idx + 3, 0) + + align = gtk.Alignment(0.0, 0.0) + self.parents_widgets.append(align) + self.table.attach(align, 1, 2, idx + 3, idx + 4, + gtk.EXPAND | gtk.FILL, gtk.FILL) + align.show() + + hbox = gtk.HBox(False, 0) + align.add(hbox) + hbox.show() + + label = gtk.Label(parent_id) + label.set_selectable(True) + hbox.pack_start(label, expand=False, fill=True) + label.show() + + image = gtk.Image() + image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU) + image.show() + + button = gtk.Button() + button.add(image) + button.set_relief(gtk.RELIEF_NONE) + button.connect("clicked", self._go_clicked_cb, parent_id) + hbox.pack_start(button, expand=False, fill=True) + button.show() + + image = gtk.Image() + image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU) + image.show() + + button = gtk.Button() + button.add(image) + button.set_relief(gtk.RELIEF_NONE) + button.set_sensitive(True) + button.connect("clicked", self._show_clicked_cb, + commit.commit_sha1, parent_id, self.encoding) + hbox.pack_start(button, expand=False, fill=True) + button.show() + + # Populate with child details + for widget in self.children_widgets: + self.table.remove(widget) + + self.children_widgets = [] + try: + child_sha1 = Commit.children_sha1[commit.commit_sha1] + except KeyError: + # We don't have child + child_sha1 = [ 0 ] + + if ( len(child_sha1) > len(commit.parent_sha1)): + self.table.resize(4 + len(child_sha1) - 1, 4) + + for idx, child_id in enumerate(child_sha1): + self.table.set_row_spacing(idx + 3, 0) + + align = gtk.Alignment(0.0, 0.0) + self.children_widgets.append(align) + self.table.attach(align, 3, 4, idx + 3, idx + 4, + gtk.EXPAND | gtk.FILL, gtk.FILL) + align.show() + + hbox = gtk.HBox(False, 0) + align.add(hbox) + hbox.show() + + label = gtk.Label(child_id) + label.set_selectable(True) + hbox.pack_start(label, expand=False, fill=True) + label.show() + + image = gtk.Image() + image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU) + image.show() + + button = gtk.Button() + button.add(image) + button.set_relief(gtk.RELIEF_NONE) + button.connect("clicked", self._go_clicked_cb, child_id) + hbox.pack_start(button, expand=False, fill=True) + button.show() + + image = gtk.Image() + image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU) + image.show() + + button = gtk.Button() + button.add(image) + button.set_relief(gtk.RELIEF_NONE) + button.set_sensitive(True) + button.connect("clicked", self._show_clicked_cb, + child_id, commit.commit_sha1, self.encoding) + hbox.pack_start(button, expand=False, fill=True) + button.show() + + def _destroy_cb(self, widget): + """Callback for when a window we manage is destroyed.""" + self.quit() + + + def quit(self): + """Stop the GTK+ main loop.""" + gtk.main_quit() + + def run(self, args): + self.set_branch(args) + self.window.connect("destroy", self._destroy_cb) + self.window.show() + gtk.main() + + def set_branch(self, args): + """Fill in different windows with info from the reposiroty""" + fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1)) + git_rev_list_cmd = fp.read() + fp.close() + fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd) + self.update_window(fp) + + def update_window(self, fp): + commit_lines = [] + + self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str) + + # used for cursor positioning + self.index = {} + + self.colours = {} + self.nodepos = {} + self.incomplete_line = {} + self.commits = [] + + index = 0 + last_colour = 0 + last_nodepos = -1 + out_line = [] + input_line = fp.readline() + while (input_line != ""): + # The commit header ends with '\0' + # This NULL is immediately followed by the sha1 of the + # next commit + if (input_line[0] != '\0'): + commit_lines.append(input_line) + input_line = fp.readline() + continue; + + commit = Commit(commit_lines) + if (commit != None ): + self.commits.append(commit) + + # Skip the '\0 + commit_lines = [] + commit_lines.append(input_line[1:]) + input_line = fp.readline() + + fp.close() + + for commit in self.commits: + (out_line, last_colour, last_nodepos) = self.draw_graph(commit, + index, out_line, + last_colour, + last_nodepos) + self.index[commit.commit_sha1] = index + index += 1 + + self.treeview.set_model(self.model) + self.treeview.show() + + def draw_graph(self, commit, index, out_line, last_colour, last_nodepos): + in_line=[] + + # | -> outline + # X + # |\ <- inline + + # Reset nodepostion + if (last_nodepos > 5): + last_nodepos = -1 + + # Add the incomplete lines of the last cell in this + try: + colour = self.colours[commit.commit_sha1] + except KeyError: + self.colours[commit.commit_sha1] = last_colour+1 + last_colour = self.colours[commit.commit_sha1] + colour = self.colours[commit.commit_sha1] + + try: + node_pos = self.nodepos[commit.commit_sha1] + except KeyError: + self.nodepos[commit.commit_sha1] = last_nodepos+1 + last_nodepos = self.nodepos[commit.commit_sha1] + node_pos = self.nodepos[commit.commit_sha1] + + #The first parent always continue on the same line + try: + # check we alreay have the value + tmp_node_pos = self.nodepos[commit.parent_sha1[0]] + except KeyError: + self.colours[commit.parent_sha1[0]] = colour + self.nodepos[commit.parent_sha1[0]] = node_pos + + for sha1 in self.incomplete_line.keys(): + if (sha1 != commit.commit_sha1): + self.draw_incomplete_line(sha1, node_pos, + out_line, in_line, index) + else: + del self.incomplete_line[sha1] + + + for parent_id in commit.parent_sha1: + try: + tmp_node_pos = self.nodepos[parent_id] + except KeyError: + self.colours[parent_id] = last_colour+1 + last_colour = self.colours[parent_id] + self.nodepos[parent_id] = last_nodepos+1 + last_nodepos = self.nodepos[parent_id] + + in_line.append((node_pos, self.nodepos[parent_id], + self.colours[parent_id])) + self.add_incomplete_line(parent_id) + + try: + branch_tag = self.bt_sha1[commit.commit_sha1] + except KeyError: + branch_tag = [ ] + + + node = (node_pos, colour, branch_tag) + + self.model.append([commit, node, out_line, in_line, + commit.message, commit.author, commit.date]) + + return (in_line, last_colour, last_nodepos) + + def add_incomplete_line(self, sha1): + try: + self.incomplete_line[sha1].append(self.nodepos[sha1]) + except KeyError: + self.incomplete_line[sha1] = [self.nodepos[sha1]] + + def draw_incomplete_line(self, sha1, node_pos, out_line, in_line, index): + for idx, pos in enumerate(self.incomplete_line[sha1]): + if(pos == node_pos): + #remove the straight line and add a slash + if ((pos, pos, self.colours[sha1]) in out_line): + out_line.remove((pos, pos, self.colours[sha1])) + out_line.append((pos, pos+0.5, self.colours[sha1])) + self.incomplete_line[sha1][idx] = pos = pos+0.5 + try: + next_commit = self.commits[index+1] + if (next_commit.commit_sha1 == sha1 and pos != int(pos)): + # join the line back to the node point + # This need to be done only if we modified it + in_line.append((pos, pos-0.5, self.colours[sha1])) + continue; + except IndexError: + pass + in_line.append((pos, pos, self.colours[sha1])) + + + def _go_clicked_cb(self, widget, revid): + """Callback for when the go button for a parent is clicked.""" + try: + self.treeview.set_cursor(self.index[revid]) + except KeyError: + 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 ): + dialog.format_secondary_text("Try running gitview without any options") + dialog.run() + dialog.destroy() + + self.treeview.grab_focus() + + def _show_clicked_cb(self, widget, commit_sha1, parent_sha1, encoding): + """Callback for when the show button for a parent is clicked.""" + window = DiffWindow() + window.set_diff(commit_sha1, parent_sha1, encoding) + self.treeview.grab_focus() + +without_diff = 0 +if __name__ == "__main__": + + if (len(sys.argv) > 1 ): + if (sys.argv[1] == "--without-diff"): + without_diff = 1 + + view = GitView( without_diff != 1) + view.run(sys.argv[without_diff:]) + + diff --git a/contrib/gitview/gitview.txt b/contrib/gitview/gitview.txt new file mode 100644 index 0000000000..77c29de305 --- /dev/null +++ b/contrib/gitview/gitview.txt @@ -0,0 +1,56 @@ +gitview(1) +========== + +NAME +---- +gitview - A GTK based repository browser for git + +SYNOPSIS +-------- +'gitview' [options] [args] + +DESCRIPTION +--------- + +Dependencies: + +* Python 2.4 +* PyGTK 2.8 or later +* PyCairo 1.0 or later + +OPTIONS +------- +--without-diff:: + + If the user doesn't want to list the commit diffs in the main window. + This may speed up the repository browsing. + +<args>:: + + All the valid option for gitlink:git-rev-list[1]. + +Key Bindings +------------ +F4:: + To maximize the window + +F5:: + To reread references. + +F11:: + Full screen + +F12:: + Leave full screen + +EXAMPLES +-------- + +gitview v2.6.12.. include/scsi drivers/scsi:: + + Show as the changes since version v2.6.12 that changed any file in the + include/scsi or drivers/scsi subdirectories + +gitview --since=2.weeks.ago:: + + Show the changes during the last two weeks diff --git a/contrib/hg-to-git/hg-to-git.py b/contrib/hg-to-git/hg-to-git.py new file mode 100755 index 0000000000..37337ff01f --- /dev/null +++ b/contrib/hg-to-git/hg-to-git.py @@ -0,0 +1,233 @@ +#! /usr/bin/python + +""" hg-to-svn.py - A Mercurial to GIT converter + + Copyright (C)2007 Stelian Pop <stelian@popies.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +import os, os.path, sys +import tempfile, popen2, pickle, getopt +import re + +# Maps hg version -> git version +hgvers = {} +# List of children for each hg revision +hgchildren = {} +# Current branch for each hg revision +hgbranch = {} + +#------------------------------------------------------------------------------ + +def usage(): + + print """\ +%s: [OPTIONS] <hgprj> + +options: + -s, --gitstate=FILE: name of the state to be saved/read + for incrementals + +required: + hgprj: name of the HG project to import (directory) +""" % sys.argv[0] + +#------------------------------------------------------------------------------ + +def getgitenv(user, date): + env = '' + elems = re.compile('(.*?)\s+<(.*)>').match(user) + if elems: + env += 'export GIT_AUTHOR_NAME="%s" ;' % elems.group(1) + env += 'export GIT_COMMITER_NAME="%s" ;' % elems.group(1) + env += 'export GIT_AUTHOR_EMAIL="%s" ;' % elems.group(2) + env += 'export GIT_COMMITER_EMAIL="%s" ;' % elems.group(2) + else: + env += 'export GIT_AUTHOR_NAME="%s" ;' % user + env += 'export GIT_COMMITER_NAME="%s" ;' % user + env += 'export GIT_AUTHOR_EMAIL= ;' + env += 'export GIT_COMMITER_EMAIL= ;' + + env += 'export GIT_AUTHOR_DATE="%s" ;' % date + env += 'export GIT_COMMITTER_DATE="%s" ;' % date + return env + +#------------------------------------------------------------------------------ + +state = '' + +try: + opts, args = getopt.getopt(sys.argv[1:], 's:t:', ['gitstate=', 'tempdir=']) + for o, a in opts: + if o in ('-s', '--gitstate'): + state = a + state = os.path.abspath(state) + + if len(args) != 1: + raise('params') +except: + usage() + sys.exit(1) + +hgprj = args[0] +os.chdir(hgprj) + +if state: + if os.path.exists(state): + print 'State does exist, reading' + f = open(state, 'r') + hgvers = pickle.load(f) + else: + print 'State does not exist, first run' + +tip = os.popen('hg tip | head -1 | cut -f 2 -d :').read().strip() +print 'tip is', tip + +# Calculate the branches +print 'analysing the branches...' +hgchildren["0"] = () +hgbranch["0"] = "master" +for cset in range(1, int(tip) + 1): + hgchildren[str(cset)] = () + prnts = os.popen('hg log -r %d | grep ^parent: | cut -f 2 -d :' % cset).readlines() + if len(prnts) > 0: + parent = prnts[0].strip() + else: + parent = str(cset - 1) + hgchildren[parent] += ( str(cset), ) + if len(prnts) > 1: + mparent = prnts[1].strip() + hgchildren[mparent] += ( str(cset), ) + else: + mparent = None + + if mparent: + # For merge changesets, take either one, preferably the 'master' branch + if hgbranch[mparent] == 'master': + hgbranch[str(cset)] = 'master' + else: + hgbranch[str(cset)] = hgbranch[parent] + else: + # Normal changesets + # For first children, take the parent branch, for the others create a new branch + if hgchildren[parent][0] == str(cset): + hgbranch[str(cset)] = hgbranch[parent] + else: + hgbranch[str(cset)] = "branch-" + str(cset) + +if not hgvers.has_key("0"): + print 'creating repository' + os.system('git-init-db') + +# loop through every hg changeset +for cset in range(int(tip) + 1): + + # incremental, already seen + if hgvers.has_key(str(cset)): + continue + + # get info + prnts = os.popen('hg log -r %d | grep ^parent: | cut -f 2 -d :' % cset).readlines() + if len(prnts) > 0: + parent = prnts[0].strip() + else: + parent = str(cset - 1) + if len(prnts) > 1: + mparent = prnts[1].strip() + else: + mparent = None + + (fdcomment, filecomment) = tempfile.mkstemp() + csetcomment = os.popen('hg log -r %d -v | grep -v ^changeset: | grep -v ^parent: | grep -v ^user: | grep -v ^date | grep -v ^files: | grep -v ^description: | grep -v ^tag:' % cset).read().strip() + os.write(fdcomment, csetcomment) + os.close(fdcomment) + + date = os.popen('hg log -r %d | grep ^date: | cut -f 2- -d :' % cset).read().strip() + + tag = os.popen('hg log -r %d | grep ^tag: | cut -f 2- -d :' % cset).read().strip() + + user = os.popen('hg log -r %d | grep ^user: | cut -f 2- -d :' % cset).read().strip() + + print '-----------------------------------------' + print 'cset:', cset + print 'branch:', hgbranch[str(cset)] + print 'user:', user + print 'date:', date + print 'comment:', csetcomment + print 'parent:', parent + if mparent: + print 'mparent:', mparent + if tag: + print 'tag:', tag + print '-----------------------------------------' + + # checkout the parent if necessary + if cset != 0: + if hgbranch[str(cset)] == "branch-" + str(cset): + print 'creating new branch', hgbranch[str(cset)] + os.system('git-checkout -b %s %s' % (hgbranch[str(cset)], hgvers[parent])) + else: + print 'checking out branch', hgbranch[str(cset)] + os.system('git-checkout %s' % hgbranch[str(cset)]) + + # merge + if mparent: + if hgbranch[parent] == hgbranch[str(cset)]: + otherbranch = hgbranch[mparent] + else: + otherbranch = hgbranch[parent] + print 'merging', otherbranch, 'into', hgbranch[str(cset)] + os.system(getgitenv(user, date) + 'git-merge --no-commit -s ours "" %s %s' % (hgbranch[str(cset)], otherbranch)) + + # remove everything except .git and .hg directories + os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf') + + # repopulate with checkouted files + os.system('hg update -C %d' % cset) + + # add new files + os.system('git-ls-files -x .hg --others | git-update-index --add --stdin') + # delete removed files + os.system('git-ls-files -x .hg --deleted | git-update-index --remove --stdin') + + # commit + os.system(getgitenv(user, date) + 'git-commit -a -F %s' % filecomment) + os.unlink(filecomment) + + # tag + if tag and tag != 'tip': + os.system(getgitenv(user, date) + 'git-tag %s' % tag) + + # delete branch if not used anymore... + if mparent and len(hgchildren[str(cset)]): + print "Deleting unused branch:", otherbranch + os.system('git-branch -d %s' % otherbranch) + + # retrieve and record the version + vvv = os.popen('git-show | head -1').read() + vvv = vvv[vvv.index(' ') + 1 : ].strip() + print 'record', cset, '->', vvv + hgvers[str(cset)] = vvv + +os.system('git-repack -a -d') + +# write the state for incrementals +if state: + print 'Writing state' + f = open(state, 'w') + pickle.dump(hgvers, f) + +# vim: et ts=8 sw=4 sts=4 diff --git a/contrib/hg-to-git/hg-to-git.txt b/contrib/hg-to-git/hg-to-git.txt new file mode 100644 index 0000000000..91f8fe6410 --- /dev/null +++ b/contrib/hg-to-git/hg-to-git.txt @@ -0,0 +1,21 @@ +hg-to-git.py is able to convert a Mercurial repository into a git one, +and preserves the branches in the process (unlike tailor) + +hg-to-git.py can probably be greatly improved (it's a rather crude +combination of shell and python) but it does already work quite well for +me. Features: + - supports incremental conversion + (for keeping a git repo in sync with a hg one) + - supports hg branches + - converts hg tags + +Note that the git repository will be created 'in place' (at the same +location as the source hg repo). You will have to manually remove the +'.hg' directory after the conversion. + +Also note that the incremental conversion uses 'simple' hg changesets +identifiers (ordinals, as opposed to SHA-1 ids), and since these ids +are not stable across different repositories the hg-to-git.py state file +is forever tied to one hg repository. + +Stelian Pop <stelian@popies.net> diff --git a/contrib/remotes2config.sh b/contrib/remotes2config.sh new file mode 100644 index 0000000000..dc09eae972 --- /dev/null +++ b/contrib/remotes2config.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +# Use this tool to rewrite your .git/remotes/ files into the config. + +. git-sh-setup + +if [ -d "$GIT_DIR"/remotes ]; then + echo "Rewriting $GIT_DIR/remotes" >&2 + error=0 + # rewrite into config + { + cd "$GIT_DIR"/remotes + ls | while read f; do + name=$(printf "$f" | tr -c "A-Za-z0-9" ".") + sed -n \ + -e "s/^URL: \(.*\)$/remote.$name.url \1 ./p" \ + -e "s/^Pull: \(.*\)$/remote.$name.fetch \1 ^$ /p" \ + -e "s/^Push: \(.*\)$/remote.$name.push \1 ^$ /p" \ + < "$f" + done + echo done + } | while read key value regex; do + case $key in + done) + if [ $error = 0 ]; then + mv "$GIT_DIR"/remotes "$GIT_DIR"/remotes.old + fi ;; + *) + echo "git-config $key "$value" $regex" + git-config $key "$value" $regex || error=1 ;; + esac + done +fi + + diff --git a/contrib/vim/README b/contrib/vim/README new file mode 100644 index 0000000000..9e7881fea9 --- /dev/null +++ b/contrib/vim/README @@ -0,0 +1,8 @@ +To syntax highlight git's commit messages, you need to: + 1. Copy syntax/gitcommit.vim to vim's syntax directory: + $ mkdir -p $HOME/.vim/syntax + $ cp syntax/gitcommit.vim $HOME/.vim/syntax + 2. Auto-detect the editing of git commit files: + $ cat >>$HOME/.vimrc <<'EOF' + autocmd BufNewFile,BufRead COMMIT_EDITMSG set filetype=gitcommit + EOF diff --git a/contrib/vim/syntax/gitcommit.vim b/contrib/vim/syntax/gitcommit.vim new file mode 100644 index 0000000000..332121b40e --- /dev/null +++ b/contrib/vim/syntax/gitcommit.vim @@ -0,0 +1,18 @@ +syn region gitLine start=/^#/ end=/$/ +syn region gitCommit start=/^# Changes to be committed:$/ end=/^#$/ contains=gitHead,gitCommitFile +syn region gitHead contained start=/^# (.*)/ end=/^#$/ +syn region gitChanged start=/^# Changed but not updated:/ end=/^#$/ contains=gitHead,gitChangedFile +syn region gitUntracked start=/^# Untracked files:/ end=/^#$/ contains=gitHead,gitUntrackedFile + +syn match gitCommitFile contained /^#\t.*/hs=s+2 +syn match gitChangedFile contained /^#\t.*/hs=s+2 +syn match gitUntrackedFile contained /^#\t.*/hs=s+2 + +hi def link gitLine Comment +hi def link gitCommit Comment +hi def link gitChanged Comment +hi def link gitHead Comment +hi def link gitUntracked Comment +hi def link gitCommitFile Type +hi def link gitChangedFile Constant +hi def link gitUntrackedFile Constant diff --git a/convert-objects.c b/convert-objects.c new file mode 100644 index 0000000000..a630132985 --- /dev/null +++ b/convert-objects.c @@ -0,0 +1,329 @@ +#include "cache.h" +#include "blob.h" +#include "commit.h" +#include "tree.h" + +struct entry { + unsigned char old_sha1[20]; + unsigned char new_sha1[20]; + int converted; +}; + +#define MAXOBJECTS (1000000) + +static struct entry *convert[MAXOBJECTS]; +static int nr_convert; + +static struct entry * convert_entry(unsigned char *sha1); + +static struct entry *insert_new(unsigned char *sha1, int pos) +{ + struct entry *new = xcalloc(1, sizeof(struct entry)); + hashcpy(new->old_sha1, sha1); + memmove(convert + pos + 1, convert + pos, (nr_convert - pos) * sizeof(struct entry *)); + convert[pos] = new; + nr_convert++; + if (nr_convert == MAXOBJECTS) + die("you're kidding me - hit maximum object limit"); + return new; +} + +static struct entry *lookup_entry(unsigned char *sha1) +{ + int low = 0, high = nr_convert; + + while (low < high) { + int next = (low + high) / 2; + struct entry *n = convert[next]; + int cmp = hashcmp(sha1, n->old_sha1); + if (!cmp) + return n; + if (cmp < 0) { + high = next; + continue; + } + low = next+1; + } + return insert_new(sha1, low); +} + +static void convert_binary_sha1(void *buffer) +{ + struct entry *entry = convert_entry(buffer); + hashcpy(buffer, entry->new_sha1); +} + +static void convert_ascii_sha1(void *buffer) +{ + unsigned char sha1[20]; + struct entry *entry; + + if (get_sha1_hex(buffer, sha1)) + die("expected sha1, got '%s'", (char*) buffer); + entry = convert_entry(sha1); + memcpy(buffer, sha1_to_hex(entry->new_sha1), 40); +} + +static unsigned int convert_mode(unsigned int mode) +{ + unsigned int newmode; + + newmode = mode & S_IFMT; + if (S_ISREG(mode)) + newmode |= (mode & 0100) ? 0755 : 0644; + return newmode; +} + +static int write_subdirectory(void *buffer, unsigned long size, const char *base, int baselen, unsigned char *result_sha1) +{ + char *new = xmalloc(size); + unsigned long newlen = 0; + unsigned long used; + + used = 0; + while (size) { + int len = 21 + strlen(buffer); + char *path = strchr(buffer, ' '); + unsigned char *sha1; + unsigned int mode; + char *slash, *origpath; + + if (!path || sscanf(buffer, "%o", &mode) != 1) + die("bad tree conversion"); + mode = convert_mode(mode); + path++; + if (memcmp(path, base, baselen)) + break; + origpath = path; + path += baselen; + slash = strchr(path, '/'); + if (!slash) { + newlen += sprintf(new + newlen, "%o %s", mode, path); + new[newlen++] = '\0'; + hashcpy((unsigned char*)new + newlen, (unsigned char *) buffer + len - 20); + newlen += 20; + + used += len; + size -= len; + buffer = (char *) buffer + len; + continue; + } + + newlen += sprintf(new + newlen, "%o %.*s", S_IFDIR, (int)(slash - path), path); + new[newlen++] = 0; + sha1 = (unsigned char *)(new + newlen); + newlen += 20; + + len = write_subdirectory(buffer, size, origpath, slash-origpath+1, sha1); + + used += len; + size -= len; + buffer = (char *) buffer + len; + } + + write_sha1_file(new, newlen, tree_type, result_sha1); + free(new); + return used; +} + +static void convert_tree(void *buffer, unsigned long size, unsigned char *result_sha1) +{ + void *orig_buffer = buffer; + unsigned long orig_size = size; + + while (size) { + int len = 1+strlen(buffer); + + convert_binary_sha1((char *) buffer + len); + + len += 20; + if (len > size) + die("corrupt tree object"); + size -= len; + buffer = (char *) buffer + len; + } + + write_subdirectory(orig_buffer, orig_size, "", 0, result_sha1); +} + +static unsigned long parse_oldstyle_date(const char *buf) +{ + char c, *p; + char buffer[100]; + struct tm tm; + const char *formats[] = { + "%c", + "%a %b %d %T", + "%Z", + "%Y", + " %Y", + NULL + }; + /* We only ever did two timezones in the bad old format .. */ + const char *timezones[] = { + "PDT", "PST", "CEST", NULL + }; + const char **fmt = formats; + + p = buffer; + while (isspace(c = *buf)) + buf++; + while ((c = *buf++) != '\n') + *p++ = c; + *p++ = 0; + buf = buffer; + memset(&tm, 0, sizeof(tm)); + do { + const char *next = strptime(buf, *fmt, &tm); + if (next) { + if (!*next) + return mktime(&tm); + buf = next; + } else { + const char **p = timezones; + while (isspace(*buf)) + buf++; + while (*p) { + if (!memcmp(buf, *p, strlen(*p))) { + buf += strlen(*p); + break; + } + p++; + } + } + fmt++; + } while (*buf && *fmt); + printf("left: %s\n", buf); + return mktime(&tm); +} + +static int convert_date_line(char *dst, void **buf, unsigned long *sp) +{ + unsigned long size = *sp; + char *line = *buf; + char *next = strchr(line, '\n'); + char *date = strchr(line, '>'); + int len; + + if (!next || !date) + die("missing or bad author/committer line %s", line); + next++; date += 2; + + *buf = next; + *sp = size - (next - line); + + len = date - line; + memcpy(dst, line, len); + dst += len; + + /* Is it already in new format? */ + if (isdigit(*date)) { + int datelen = next - date; + memcpy(dst, date, datelen); + return len + datelen; + } + + /* + * Hacky hacky: one of the sparse old-style commits does not have + * any date at all, but we can fake it by using the committer date. + */ + if (*date == '\n' && strchr(next, '>')) + date = strchr(next, '>')+2; + + return len + sprintf(dst, "%lu -0700\n", parse_oldstyle_date(date)); +} + +static void convert_date(void *buffer, unsigned long size, unsigned char *result_sha1) +{ + char *new = xmalloc(size + 100); + unsigned long newlen = 0; + + /* "tree <sha1>\n" */ + memcpy(new + newlen, buffer, 46); + newlen += 46; + buffer = (char *) buffer + 46; + size -= 46; + + /* "parent <sha1>\n" */ + while (!memcmp(buffer, "parent ", 7)) { + memcpy(new + newlen, buffer, 48); + newlen += 48; + buffer = (char *) buffer + 48; + size -= 48; + } + + /* "author xyz <xyz> date" */ + newlen += convert_date_line(new + newlen, &buffer, &size); + /* "committer xyz <xyz> date" */ + newlen += convert_date_line(new + newlen, &buffer, &size); + + /* Rest */ + memcpy(new + newlen, buffer, size); + newlen += size; + + write_sha1_file(new, newlen, commit_type, result_sha1); + free(new); +} + +static void convert_commit(void *buffer, unsigned long size, unsigned char *result_sha1) +{ + void *orig_buffer = buffer; + unsigned long orig_size = size; + + if (memcmp(buffer, "tree ", 5)) + die("Bad commit '%s'", (char*) buffer); + convert_ascii_sha1((char *) buffer + 5); + buffer = (char *) buffer + 46; /* "tree " + "hex sha1" + "\n" */ + while (!memcmp(buffer, "parent ", 7)) { + convert_ascii_sha1((char *) buffer + 7); + buffer = (char *) buffer + 48; + } + convert_date(orig_buffer, orig_size, result_sha1); +} + +static struct entry * convert_entry(unsigned char *sha1) +{ + struct entry *entry = lookup_entry(sha1); + char type[20]; + void *buffer, *data; + unsigned long size; + + if (entry->converted) + return entry; + data = read_sha1_file(sha1, type, &size); + if (!data) + die("unable to read object %s", sha1_to_hex(sha1)); + + buffer = xmalloc(size); + memcpy(buffer, data, size); + + if (!strcmp(type, blob_type)) { + write_sha1_file(buffer, size, blob_type, entry->new_sha1); + } else if (!strcmp(type, tree_type)) + convert_tree(buffer, size, entry->new_sha1); + else if (!strcmp(type, commit_type)) + convert_commit(buffer, size, entry->new_sha1); + else + die("unknown object type '%s' in %s", type, sha1_to_hex(sha1)); + entry->converted = 1; + free(buffer); + free(data); + return entry; +} + +int main(int argc, char **argv) +{ + unsigned char sha1[20]; + struct entry *entry; + + setup_git_directory(); + + if (argc != 2) + usage("git-convert-objects <sha1>"); + if (get_sha1(argv[1], sha1)) + die("Not a valid object name %s", argv[1]); + + entry = convert_entry(sha1); + printf("new sha1: %s\n", sha1_to_hex(entry->new_sha1)); + return 0; +} diff --git a/copy.c b/copy.c new file mode 100644 index 0000000000..08a3d388a4 --- /dev/null +++ b/copy.c @@ -0,0 +1,38 @@ +#include "cache.h" + +int copy_fd(int ifd, int ofd) +{ + while (1) { + int len; + char buffer[8192]; + char *buf = buffer; + len = xread(ifd, buffer, sizeof(buffer)); + if (!len) + break; + if (len < 0) { + int read_error; + read_error = errno; + close(ifd); + return error("copy-fd: read returned %s", + strerror(read_error)); + } + while (len) { + int written = xwrite(ofd, buf, len); + if (written > 0) { + buf += written; + len -= written; + } + else if (!written) { + close(ifd); + return error("copy-fd: write returned 0"); + } else { + close(ifd); + return error("copy-fd: write returned %s", + strerror(errno)); + } + } + } + close(ifd); + return 0; +} + diff --git a/csum-file.c b/csum-file.c new file mode 100644 index 0000000000..b7174c6c05 --- /dev/null +++ b/csum-file.c @@ -0,0 +1,146 @@ +/* + * csum-file.c + * + * Copyright (C) 2005 Linus Torvalds + * + * Simple file write infrastructure for writing SHA1-summed + * files. Useful when you write a file that you want to be + * able to verify hasn't been messed with afterwards. + */ +#include "cache.h" +#include "csum-file.h" + +static void sha1flush(struct sha1file *f, unsigned int count) +{ + void *buf = f->buffer; + + for (;;) { + int ret = xwrite(f->fd, buf, count); + if (ret > 0) { + buf = (char *) buf + ret; + count -= ret; + if (count) + continue; + return; + } + if (!ret) + die("sha1 file '%s' write error. Out of diskspace", f->name); + die("sha1 file '%s' write error (%s)", f->name, strerror(errno)); + } +} + +int sha1close(struct sha1file *f, unsigned char *result, int update) +{ + unsigned offset = f->offset; + if (offset) { + SHA1_Update(&f->ctx, f->buffer, offset); + sha1flush(f, offset); + } + SHA1_Final(f->buffer, &f->ctx); + if (result) + hashcpy(result, f->buffer); + if (update) + sha1flush(f, 20); + if (close(f->fd)) + die("%s: sha1 file error on close (%s)", f->name, strerror(errno)); + free(f); + return 0; +} + +int sha1write(struct sha1file *f, void *buf, unsigned int count) +{ + while (count) { + unsigned offset = f->offset; + unsigned left = sizeof(f->buffer) - offset; + unsigned nr = count > left ? left : count; + + memcpy(f->buffer + offset, buf, nr); + count -= nr; + offset += nr; + buf = (char *) buf + nr; + left -= nr; + if (!left) { + SHA1_Update(&f->ctx, f->buffer, offset); + sha1flush(f, offset); + offset = 0; + } + f->offset = offset; + } + return 0; +} + +struct sha1file *sha1create(const char *fmt, ...) +{ + struct sha1file *f; + unsigned len; + va_list arg; + int fd; + + f = xmalloc(sizeof(*f)); + + va_start(arg, fmt); + len = vsnprintf(f->name, sizeof(f->name), fmt, arg); + va_end(arg); + if (len >= PATH_MAX) + die("you wascally wabbit, you"); + f->namelen = len; + + fd = open(f->name, O_CREAT | O_EXCL | O_WRONLY, 0666); + if (fd < 0) + die("unable to open %s (%s)", f->name, strerror(errno)); + f->fd = fd; + f->error = 0; + f->offset = 0; + SHA1_Init(&f->ctx); + return f; +} + +struct sha1file *sha1fd(int fd, const char *name) +{ + struct sha1file *f; + unsigned len; + + f = xmalloc(sizeof(*f)); + + len = strlen(name); + if (len >= PATH_MAX) + die("you wascally wabbit, you"); + f->namelen = len; + memcpy(f->name, name, len+1); + + f->fd = fd; + f->error = 0; + f->offset = 0; + SHA1_Init(&f->ctx); + return f; +} + +int sha1write_compressed(struct sha1file *f, void *in, unsigned int size) +{ + z_stream stream; + unsigned long maxsize; + void *out; + + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, zlib_compression_level); + maxsize = deflateBound(&stream, size); + out = xmalloc(maxsize); + + /* Compress it */ + stream.next_in = in; + stream.avail_in = size; + + stream.next_out = out; + stream.avail_out = maxsize; + + while (deflate(&stream, Z_FINISH) == Z_OK) + /* nothing */; + deflateEnd(&stream); + + size = stream.total_out; + sha1write(f, out, size); + free(out); + return size; +} + + diff --git a/csum-file.h b/csum-file.h new file mode 100644 index 0000000000..3ad1a992a7 --- /dev/null +++ b/csum-file.h @@ -0,0 +1,19 @@ +#ifndef CSUM_FILE_H +#define CSUM_FILE_H + +/* A SHA1-protected file */ +struct sha1file { + int fd, error; + unsigned int offset, namelen; + SHA_CTX ctx; + char name[PATH_MAX]; + unsigned char buffer[8192]; +}; + +extern struct sha1file *sha1fd(int fd, const char *name); +extern struct sha1file *sha1create(const char *fmt, ...) __attribute__((format (printf, 1, 2))); +extern int sha1close(struct sha1file *, unsigned char *, int); +extern int sha1write(struct sha1file *, void *, unsigned int); +extern int sha1write_compressed(struct sha1file *, void *, unsigned int); + +#endif diff --git a/ctype.c b/ctype.c new file mode 100644 index 0000000000..56bdffa636 --- /dev/null +++ b/ctype.c @@ -0,0 +1,23 @@ +/* + * Sane locale-independent, ASCII ctype. + * + * No surprises, and works with signed and unsigned chars. + */ +#include "cache.h" + +#define SS GIT_SPACE +#define AA GIT_ALPHA +#define DD GIT_DIGIT + +unsigned char sane_ctype[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, SS, SS, 0, 0, SS, 0, 0, /* 0-15 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16-15 */ + SS, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 32-15 */ + DD, DD, DD, DD, DD, DD, DD, DD, DD, DD, 0, 0, 0, 0, 0, 0, /* 48-15 */ + 0, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, /* 64-15 */ + AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, 0, 0, 0, 0, 0, /* 80-15 */ + 0, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, /* 96-15 */ + AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, AA, 0, 0, 0, 0, 0, /* 112-15 */ + /* Nothing in the 128.. range */ +}; + diff --git a/daemon.c b/daemon.c new file mode 100644 index 0000000000..2a20ca55cb --- /dev/null +++ b/daemon.c @@ -0,0 +1,1171 @@ +#include "cache.h" +#include "pkt-line.h" +#include "exec_cmd.h" +#include "interpolate.h" + +#include <syslog.h> + +#ifndef HOST_NAME_MAX +#define HOST_NAME_MAX 256 +#endif + +static int log_syslog; +static int verbose; +static int reuseaddr; + +static const char daemon_usage[] = +"git-daemon [--verbose] [--syslog] [--export-all]\n" +" [--timeout=n] [--init-timeout=n] [--strict-paths]\n" +" [--base-path=path] [--user-path | --user-path=path]\n" +" [--interpolated-path=path]\n" +" [--reuseaddr] [--detach] [--pid-file=file]\n" +" [--[enable|disable|allow-override|forbid-override]=service]\n" +" [--inetd | [--listen=host_or_ipaddr] [--port=n]\n" +" [--user=user [--group=group]]\n" +" [directory...]"; + +/* List of acceptable pathname prefixes */ +static char **ok_paths; +static int strict_paths; + +/* If this is set, git-daemon-export-ok is not required */ +static int export_all_trees; + +/* Take all paths relative to this one if non-NULL */ +static char *base_path; +static char *interpolated_path; + +/* Flag indicating client sent extra args. */ +static int saw_extended_args; + +/* If defined, ~user notation is allowed and the string is inserted + * after ~user/. E.g. a request to git://host/~alice/frotz would + * go to /home/alice/pub_git/frotz with --user-path=pub_git. + */ +static const char *user_path; + +/* Timeout, and initial timeout */ +static unsigned int timeout; +static unsigned int init_timeout; + +/* + * Static table for now. Ugh. + * Feel free to make dynamic as needed. + */ +#define INTERP_SLOT_HOST (0) +#define INTERP_SLOT_CANON_HOST (1) +#define INTERP_SLOT_IP (2) +#define INTERP_SLOT_PORT (3) +#define INTERP_SLOT_DIR (4) +#define INTERP_SLOT_PERCENT (5) + +static struct interp interp_table[] = { + { "%H", 0}, + { "%CH", 0}, + { "%IP", 0}, + { "%P", 0}, + { "%D", 0}, + { "%%", 0}, +}; + + +static void logreport(int priority, const char *err, va_list params) +{ + /* We should do a single write so that it is atomic and output + * of several processes do not get intermingled. */ + char buf[1024]; + int buflen; + int maxlen, msglen; + + /* sizeof(buf) should be big enough for "[pid] \n" */ + buflen = snprintf(buf, sizeof(buf), "[%ld] ", (long) getpid()); + + maxlen = sizeof(buf) - buflen - 1; /* -1 for our own LF */ + msglen = vsnprintf(buf + buflen, maxlen, err, params); + + if (log_syslog) { + syslog(priority, "%s", buf); + return; + } + + /* maxlen counted our own LF but also counts space given to + * vsnprintf for the terminating NUL. We want to make sure that + * we have space for our own LF and NUL after the "meat" of the + * message, so truncate it at maxlen - 1. + */ + if (msglen > maxlen - 1) + msglen = maxlen - 1; + else if (msglen < 0) + msglen = 0; /* Protect against weird return values. */ + buflen += msglen; + + buf[buflen++] = '\n'; + buf[buflen] = '\0'; + + write_in_full(2, buf, buflen); +} + +static void logerror(const char *err, ...) +{ + va_list params; + va_start(params, err); + logreport(LOG_ERR, err, params); + va_end(params); +} + +static void loginfo(const char *err, ...) +{ + va_list params; + if (!verbose) + return; + va_start(params, err); + logreport(LOG_INFO, err, params); + va_end(params); +} + +static void NORETURN daemon_die(const char *err, va_list params) +{ + logreport(LOG_ERR, err, params); + exit(1); +} + +static int avoid_alias(char *p) +{ + int sl, ndot; + + /* + * This resurrects the belts and suspenders paranoia check by HPA + * done in <435560F7.4080006@zytor.com> thread, now enter_repo() + * does not do getcwd() based path canonicalizations. + * + * sl becomes true immediately after seeing '/' and continues to + * be true as long as dots continue after that without intervening + * non-dot character. + */ + if (!p || (*p != '/' && *p != '~')) + return -1; + sl = 1; ndot = 0; + p++; + + while (1) { + char ch = *p++; + if (sl) { + if (ch == '.') + ndot++; + else if (ch == '/') { + if (ndot < 3) + /* reject //, /./ and /../ */ + return -1; + ndot = 0; + } + else if (ch == 0) { + if (0 < ndot && ndot < 3) + /* reject /.$ and /..$ */ + return -1; + return 0; + } + else + sl = ndot = 0; + } + else if (ch == 0) + return 0; + else if (ch == '/') { + sl = 1; + ndot = 0; + } + } +} + +static char *path_ok(struct interp *itable) +{ + static char rpath[PATH_MAX]; + static char interp_path[PATH_MAX]; + char *path; + char *dir; + + dir = itable[INTERP_SLOT_DIR].value; + + if (avoid_alias(dir)) { + logerror("'%s': aliased", dir); + return NULL; + } + + if (*dir == '~') { + if (!user_path) { + logerror("'%s': User-path not allowed", dir); + return NULL; + } + if (*user_path) { + /* Got either "~alice" or "~alice/foo"; + * rewrite them to "~alice/%s" or + * "~alice/%s/foo". + */ + int namlen, restlen = strlen(dir); + char *slash = strchr(dir, '/'); + if (!slash) + slash = dir + restlen; + namlen = slash - dir; + restlen -= namlen; + loginfo("userpath <%s>, request <%s>, namlen %d, restlen %d, slash <%s>", user_path, dir, namlen, restlen, slash); + snprintf(rpath, PATH_MAX, "%.*s/%s%.*s", + namlen, dir, user_path, restlen, slash); + dir = rpath; + } + } + else if (interpolated_path && saw_extended_args) { + if (*dir != '/') { + /* Allow only absolute */ + logerror("'%s': Non-absolute path denied (interpolated-path active)", dir); + return NULL; + } + + interpolate(interp_path, PATH_MAX, interpolated_path, + interp_table, ARRAY_SIZE(interp_table)); + loginfo("Interpolated dir '%s'", interp_path); + + dir = interp_path; + } + else if (base_path) { + if (*dir != '/') { + /* Allow only absolute */ + logerror("'%s': Non-absolute path denied (base-path active)", dir); + return NULL; + } + snprintf(rpath, PATH_MAX, "%s%s", base_path, dir); + dir = rpath; + } + + path = enter_repo(dir, strict_paths); + + if (!path) { + logerror("'%s': unable to chdir or not a git archive", dir); + return NULL; + } + + if ( ok_paths && *ok_paths ) { + char **pp; + int pathlen = strlen(path); + + /* The validation is done on the paths after enter_repo + * appends optional {.git,.git/.git} and friends, but + * it does not use getcwd(). So if your /pub is + * a symlink to /mnt/pub, you can whitelist /pub and + * do not have to say /mnt/pub. + * Do not say /pub/. + */ + for ( pp = ok_paths ; *pp ; pp++ ) { + int len = strlen(*pp); + if (len <= pathlen && + !memcmp(*pp, path, len) && + (path[len] == '\0' || + (!strict_paths && path[len] == '/'))) + return path; + } + } + else { + /* be backwards compatible */ + if (!strict_paths) + return path; + } + + logerror("'%s': not in whitelist", path); + return NULL; /* Fallthrough. Deny by default */ +} + +typedef int (*daemon_service_fn)(void); +struct daemon_service { + const char *name; + const char *config_name; + daemon_service_fn fn; + int enabled; + int overridable; +}; + +static struct daemon_service *service_looking_at; +static int service_enabled; + +static int git_daemon_config(const char *var, const char *value) +{ + if (!strncmp(var, "daemon.", 7) && + !strcmp(var + 7, service_looking_at->config_name)) { + service_enabled = git_config_bool(var, value); + return 0; + } + + /* we are not interested in parsing any other configuration here */ + return 0; +} + +static int run_service(struct interp *itable, struct daemon_service *service) +{ + const char *path; + int enabled = service->enabled; + + loginfo("Request %s for '%s'", + service->name, + itable[INTERP_SLOT_DIR].value); + + if (!enabled && !service->overridable) { + logerror("'%s': service not enabled.", service->name); + errno = EACCES; + return -1; + } + + if (!(path = path_ok(itable))) + return -1; + + /* + * Security on the cheap. + * + * We want a readable HEAD, usable "objects" directory, and + * a "git-daemon-export-ok" flag that says that the other side + * is ok with us doing this. + * + * path_ok() uses enter_repo() and does whitelist checking. + * We only need to make sure the repository is exported. + */ + + if (!export_all_trees && access("git-daemon-export-ok", F_OK)) { + logerror("'%s': repository not exported.", path); + errno = EACCES; + return -1; + } + + if (service->overridable) { + service_looking_at = service; + service_enabled = -1; + git_config(git_daemon_config); + if (0 <= service_enabled) + enabled = service_enabled; + } + if (!enabled) { + logerror("'%s': service not enabled for '%s'", + service->name, path); + errno = EACCES; + return -1; + } + + /* + * We'll ignore SIGTERM from now on, we have a + * good client. + */ + signal(SIGTERM, SIG_IGN); + + return service->fn(); +} + +static int upload_pack(void) +{ + /* Timeout as string */ + char timeout_buf[64]; + + snprintf(timeout_buf, sizeof timeout_buf, "--timeout=%u", timeout); + + /* git-upload-pack only ever reads stuff, so this is safe */ + execl_git_cmd("upload-pack", "--strict", timeout_buf, ".", NULL); + return -1; +} + +static int upload_archive(void) +{ + execl_git_cmd("upload-archive", ".", NULL); + return -1; +} + +static int receive_pack(void) +{ + execl_git_cmd("receive-pack", ".", NULL); + return -1; +} + +static struct daemon_service daemon_service[] = { + { "upload-archive", "uploadarch", upload_archive, 0, 1 }, + { "upload-pack", "uploadpack", upload_pack, 1, 1 }, + { "receive-pack", "receivepack", receive_pack, 0, 1 }, +}; + +static void enable_service(const char *name, int ena) { + int i; + for (i = 0; i < ARRAY_SIZE(daemon_service); i++) { + if (!strcmp(daemon_service[i].name, name)) { + daemon_service[i].enabled = ena; + return; + } + } + die("No such service %s", name); +} + +static void make_service_overridable(const char *name, int ena) { + int i; + for (i = 0; i < ARRAY_SIZE(daemon_service); i++) { + if (!strcmp(daemon_service[i].name, name)) { + daemon_service[i].overridable = ena; + return; + } + } + die("No such service %s", name); +} + +/* + * Separate the "extra args" information as supplied by the client connection. + * Any resulting data is squirreled away in the given interpolation table. + */ +static void parse_extra_args(struct interp *table, char *extra_args, int buflen) +{ + char *val; + int vallen; + char *end = extra_args + buflen; + + while (extra_args < end && *extra_args) { + saw_extended_args = 1; + if (strncasecmp("host=", extra_args, 5) == 0) { + val = extra_args + 5; + vallen = strlen(val) + 1; + if (*val) { + /* Split <host>:<port> at colon. */ + char *host = val; + char *port = strrchr(host, ':'); + if (port) { + *port = 0; + port++; + interp_set_entry(table, INTERP_SLOT_PORT, port); + } + interp_set_entry(table, INTERP_SLOT_HOST, host); + } + + /* On to the next one */ + extra_args = val + vallen; + } + } +} + +void fill_in_extra_table_entries(struct interp *itable) +{ + char *hp; + + /* + * Replace literal host with lowercase-ized hostname. + */ + hp = interp_table[INTERP_SLOT_HOST].value; + if (!hp) + return; + for ( ; *hp; hp++) + *hp = tolower(*hp); + + /* + * Locate canonical hostname and its IP address. + */ +#ifndef NO_IPV6 + { + struct addrinfo hints; + struct addrinfo *ai, *ai0; + int gai; + static char addrbuf[HOST_NAME_MAX + 1]; + + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_CANONNAME; + + gai = getaddrinfo(interp_table[INTERP_SLOT_HOST].value, 0, &hints, &ai0); + if (!gai) { + for (ai = ai0; ai; ai = ai->ai_next) { + struct sockaddr_in *sin_addr = (void *)ai->ai_addr; + + inet_ntop(AF_INET, &sin_addr->sin_addr, + addrbuf, sizeof(addrbuf)); + interp_set_entry(interp_table, + INTERP_SLOT_CANON_HOST, ai->ai_canonname); + interp_set_entry(interp_table, + INTERP_SLOT_IP, addrbuf); + break; + } + freeaddrinfo(ai0); + } + } +#else + { + struct hostent *hent; + struct sockaddr_in sa; + char **ap; + static char addrbuf[HOST_NAME_MAX + 1]; + + hent = gethostbyname(interp_table[INTERP_SLOT_HOST].value); + + ap = hent->h_addr_list; + memset(&sa, 0, sizeof sa); + sa.sin_family = hent->h_addrtype; + sa.sin_port = htons(0); + memcpy(&sa.sin_addr, *ap, hent->h_length); + + inet_ntop(hent->h_addrtype, &sa.sin_addr, + addrbuf, sizeof(addrbuf)); + + interp_set_entry(interp_table, INTERP_SLOT_CANON_HOST, hent->h_name); + interp_set_entry(interp_table, INTERP_SLOT_IP, addrbuf); + } +#endif +} + + +static int execute(struct sockaddr *addr) +{ + static char line[1000]; + int pktlen, len, i; + + if (addr) { + char addrbuf[256] = ""; + int port = -1; + + if (addr->sa_family == AF_INET) { + struct sockaddr_in *sin_addr = (void *) addr; + inet_ntop(addr->sa_family, &sin_addr->sin_addr, addrbuf, sizeof(addrbuf)); + port = sin_addr->sin_port; +#ifndef NO_IPV6 + } else if (addr && addr->sa_family == AF_INET6) { + struct sockaddr_in6 *sin6_addr = (void *) addr; + + char *buf = addrbuf; + *buf++ = '['; *buf = '\0'; /* stpcpy() is cool */ + inet_ntop(AF_INET6, &sin6_addr->sin6_addr, buf, sizeof(addrbuf) - 1); + strcat(buf, "]"); + + port = sin6_addr->sin6_port; +#endif + } + loginfo("Connection from %s:%d", addrbuf, port); + } + + alarm(init_timeout ? init_timeout : timeout); + 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; + pktlen--; + } + + /* + * Initialize the path interpolation table for this connection. + */ + interp_clear_table(interp_table, ARRAY_SIZE(interp_table)); + interp_set_entry(interp_table, INTERP_SLOT_PERCENT, "%"); + + if (len != pktlen) { + parse_extra_args(interp_table, line + len + 1, pktlen - len - 1); + fill_in_extra_table_entries(interp_table); + } + + for (i = 0; i < ARRAY_SIZE(daemon_service); i++) { + struct daemon_service *s = &(daemon_service[i]); + int namelen = strlen(s->name); + if (!strncmp("git-", line, 4) && + !strncmp(s->name, line + 4, namelen) && + line[namelen + 4] == ' ') { + /* + * Note: The directory here is probably context sensitive, + * and might depend on the actual service being performed. + */ + interp_set_entry(interp_table, + INTERP_SLOT_DIR, line + namelen + 5); + return run_service(interp_table, s); + } + } + + logerror("Protocol error: '%s'", line); + return -1; +} + + +/* + * We count spawned/reaped separately, just to avoid any + * races when updating them from signals. The SIGCHLD handler + * will only update children_reaped, and the fork logic will + * only update children_spawned. + * + * MAX_CHILDREN should be a power-of-two to make the modulus + * operation cheap. It should also be at least twice + * the maximum number of connections we will ever allow. + */ +#define MAX_CHILDREN 128 + +static int max_connections = 25; + +/* These are updated by the signal handler */ +static volatile unsigned int children_reaped; +static pid_t dead_child[MAX_CHILDREN]; + +/* These are updated by the main loop */ +static unsigned int children_spawned; +static unsigned int children_deleted; + +static struct child { + pid_t pid; + int addrlen; + struct sockaddr_storage address; +} live_child[MAX_CHILDREN]; + +static void add_child(int idx, pid_t pid, struct sockaddr *addr, int addrlen) +{ + live_child[idx].pid = pid; + live_child[idx].addrlen = addrlen; + memcpy(&live_child[idx].address, addr, addrlen); +} + +/* + * Walk from "deleted" to "spawned", and remove child "pid". + * + * We move everything up by one, since the new "deleted" will + * be one higher. + */ +static void remove_child(pid_t pid, unsigned deleted, unsigned spawned) +{ + struct child n; + + deleted %= MAX_CHILDREN; + spawned %= MAX_CHILDREN; + if (live_child[deleted].pid == pid) { + live_child[deleted].pid = -1; + return; + } + n = live_child[deleted]; + for (;;) { + struct child m; + deleted = (deleted + 1) % MAX_CHILDREN; + if (deleted == spawned) + die("could not find dead child %d\n", pid); + m = live_child[deleted]; + live_child[deleted] = n; + if (m.pid == pid) + return; + n = m; + } +} + +/* + * This gets called if the number of connections grows + * past "max_connections". + * + * We _should_ start off by searching for connections + * from the same IP, and if there is some address wth + * multiple connections, we should kill that first. + * + * As it is, we just "randomly" kill 25% of the connections, + * and our pseudo-random generator sucks too. I have no + * shame. + * + * Really, this is just a place-holder for a _real_ algorithm. + */ +static void kill_some_children(int signo, unsigned start, unsigned stop) +{ + start %= MAX_CHILDREN; + stop %= MAX_CHILDREN; + while (start != stop) { + if (!(start & 3)) + kill(live_child[start].pid, signo); + start = (start + 1) % MAX_CHILDREN; + } +} + +static void check_max_connections(void) +{ + for (;;) { + int active; + unsigned spawned, reaped, deleted; + + spawned = children_spawned; + reaped = children_reaped; + deleted = children_deleted; + + while (deleted < reaped) { + pid_t pid = dead_child[deleted % MAX_CHILDREN]; + remove_child(pid, deleted, spawned); + deleted++; + } + children_deleted = deleted; + + active = spawned - deleted; + if (active <= max_connections) + break; + + /* Kill some unstarted connections with SIGTERM */ + kill_some_children(SIGTERM, deleted, spawned); + if (active <= max_connections << 1) + break; + + /* If the SIGTERM thing isn't helping use SIGKILL */ + kill_some_children(SIGKILL, deleted, spawned); + sleep(1); + } +} + +static void handle(int incoming, struct sockaddr *addr, int addrlen) +{ + pid_t pid = fork(); + + if (pid) { + unsigned idx; + + close(incoming); + if (pid < 0) + return; + + idx = children_spawned % MAX_CHILDREN; + children_spawned++; + add_child(idx, pid, addr, addrlen); + + check_max_connections(); + return; + } + + dup2(incoming, 0); + dup2(incoming, 1); + close(incoming); + + exit(execute(addr)); +} + +static void child_handler(int signo) +{ + for (;;) { + int status; + pid_t pid = waitpid(-1, &status, WNOHANG); + + if (pid > 0) { + unsigned reaped = children_reaped; + dead_child[reaped % MAX_CHILDREN] = pid; + children_reaped = reaped + 1; + /* XXX: Custom logging, since we don't wanna getpid() */ + if (verbose) { + const char *dead = ""; + if (!WIFEXITED(status) || WEXITSTATUS(status) > 0) + dead = " (with error)"; + if (log_syslog) + syslog(LOG_INFO, "[%d] Disconnected%s", pid, dead); + else + fprintf(stderr, "[%d] Disconnected%s\n", pid, dead); + } + continue; + } + break; + } +} + +static int set_reuse_addr(int sockfd) +{ + int on = 1; + + if (!reuseaddr) + return 0; + return setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, + &on, sizeof(on)); +} + +#ifndef NO_IPV6 + +static int socksetup(char *listen_addr, int listen_port, int **socklist_p) +{ + int socknum = 0, *socklist = NULL; + int maxfd = -1; + char pbuf[NI_MAXSERV]; + struct addrinfo hints, *ai0, *ai; + int gai; + + sprintf(pbuf, "%d", listen_port); + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + + gai = getaddrinfo(listen_addr, pbuf, &hints, &ai0); + if (gai) + die("getaddrinfo() failed: %s\n", gai_strerror(gai)); + + for (ai = ai0; ai; ai = ai->ai_next) { + int sockfd; + + sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sockfd < 0) + continue; + if (sockfd >= FD_SETSIZE) { + error("too large socket descriptor."); + close(sockfd); + continue; + } + +#ifdef IPV6_V6ONLY + if (ai->ai_family == AF_INET6) { + int on = 1; + setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, + &on, sizeof(on)); + /* Note: error is not fatal */ + } +#endif + + if (set_reuse_addr(sockfd)) { + close(sockfd); + continue; + } + + if (bind(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) { + close(sockfd); + continue; /* not fatal */ + } + if (listen(sockfd, 5) < 0) { + close(sockfd); + continue; /* not fatal */ + } + + socklist = xrealloc(socklist, sizeof(int) * (socknum + 1)); + socklist[socknum++] = sockfd; + + if (maxfd < sockfd) + maxfd = sockfd; + } + + freeaddrinfo(ai0); + + *socklist_p = socklist; + return socknum; +} + +#else /* NO_IPV6 */ + +static int socksetup(char *listen_addr, int listen_port, int **socklist_p) +{ + struct sockaddr_in sin; + int sockfd; + + memset(&sin, 0, sizeof sin); + sin.sin_family = AF_INET; + sin.sin_port = htons(listen_port); + + if (listen_addr) { + /* Well, host better be an IP address here. */ + if (inet_pton(AF_INET, listen_addr, &sin.sin_addr.s_addr) <= 0) + return 0; + } else { + sin.sin_addr.s_addr = htonl(INADDR_ANY); + } + + sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd < 0) + return 0; + + if (set_reuse_addr(sockfd)) { + close(sockfd); + return 0; + } + + if ( bind(sockfd, (struct sockaddr *)&sin, sizeof sin) < 0 ) { + close(sockfd); + return 0; + } + + if (listen(sockfd, 5) < 0) { + close(sockfd); + return 0; + } + + *socklist_p = xmalloc(sizeof(int)); + **socklist_p = sockfd; + return 1; +} + +#endif + +static int service_loop(int socknum, int *socklist) +{ + struct pollfd *pfd; + int i; + + pfd = xcalloc(socknum, sizeof(struct pollfd)); + + for (i = 0; i < socknum; i++) { + pfd[i].fd = socklist[i]; + pfd[i].events = POLLIN; + } + + signal(SIGCHLD, child_handler); + + for (;;) { + int i; + + if (poll(pfd, socknum, -1) < 0) { + if (errno != EINTR) { + error("poll failed, resuming: %s", + strerror(errno)); + sleep(1); + } + continue; + } + + for (i = 0; i < socknum; i++) { + if (pfd[i].revents & POLLIN) { + struct sockaddr_storage ss; + unsigned int sslen = sizeof(ss); + int incoming = accept(pfd[i].fd, (struct sockaddr *)&ss, &sslen); + if (incoming < 0) { + switch (errno) { + case EAGAIN: + case EINTR: + case ECONNABORTED: + continue; + default: + die("accept returned %s", strerror(errno)); + } + } + handle(incoming, (struct sockaddr *)&ss, sslen); + } + } + } +} + +/* if any standard file descriptor is missing open it to /dev/null */ +static void sanitize_stdfds(void) +{ + int fd = open("/dev/null", O_RDWR, 0); + while (fd != -1 && fd < 2) + fd = dup(fd); + if (fd == -1) + die("open /dev/null or dup failed: %s", strerror(errno)); + if (fd > 2) + close(fd); +} + +static void daemonize(void) +{ + switch (fork()) { + case 0: + break; + case -1: + die("fork failed: %s", strerror(errno)); + default: + exit(0); + } + if (setsid() == -1) + die("setsid failed: %s", strerror(errno)); + close(0); + close(1); + close(2); + sanitize_stdfds(); +} + +static void store_pid(const char *path) +{ + FILE *f = fopen(path, "w"); + if (!f) + die("cannot open pid file %s: %s", path, strerror(errno)); + fprintf(f, "%d\n", getpid()); + fclose(f); +} + +static int serve(char *listen_addr, int listen_port, struct passwd *pass, gid_t gid) +{ + int socknum, *socklist; + + socknum = socksetup(listen_addr, listen_port, &socklist); + if (socknum == 0) + die("unable to allocate any listen sockets on host %s port %u", + listen_addr, listen_port); + + if (pass && gid && + (initgroups(pass->pw_name, gid) || setgid (gid) || + setuid(pass->pw_uid))) + die("cannot drop privileges"); + + return service_loop(socknum, socklist); +} + +int main(int argc, char **argv) +{ + int listen_port = 0; + char *listen_addr = NULL; + int inetd_mode = 0; + const char *pid_file = NULL, *user_name = NULL, *group_name = NULL; + int detach = 0; + struct passwd *pass = NULL; + struct group *group; + gid_t gid = 0; + int i; + + /* Without this we cannot rely on waitpid() to tell + * what happened to our children. + */ + signal(SIGCHLD, SIG_DFL); + + for (i = 1; i < argc; i++) { + char *arg = argv[i]; + + if (!strncmp(arg, "--listen=", 9)) { + char *p = arg + 9; + char *ph = listen_addr = xmalloc(strlen(arg + 9) + 1); + while (*p) + *ph++ = tolower(*p++); + *ph = 0; + continue; + } + if (!strncmp(arg, "--port=", 7)) { + char *end; + unsigned long n; + n = strtoul(arg+7, &end, 0); + if (arg[7] && !*end) { + listen_port = n; + continue; + } + } + if (!strcmp(arg, "--inetd")) { + inetd_mode = 1; + log_syslog = 1; + continue; + } + if (!strcmp(arg, "--verbose")) { + verbose = 1; + continue; + } + if (!strcmp(arg, "--syslog")) { + log_syslog = 1; + continue; + } + if (!strcmp(arg, "--export-all")) { + export_all_trees = 1; + continue; + } + if (!strncmp(arg, "--timeout=", 10)) { + timeout = atoi(arg+10); + continue; + } + if (!strncmp(arg, "--init-timeout=", 15)) { + init_timeout = atoi(arg+15); + continue; + } + if (!strcmp(arg, "--strict-paths")) { + strict_paths = 1; + continue; + } + if (!strncmp(arg, "--base-path=", 12)) { + base_path = arg+12; + continue; + } + if (!strncmp(arg, "--interpolated-path=", 20)) { + interpolated_path = arg+20; + continue; + } + if (!strcmp(arg, "--reuseaddr")) { + reuseaddr = 1; + continue; + } + if (!strcmp(arg, "--user-path")) { + user_path = ""; + continue; + } + if (!strncmp(arg, "--user-path=", 12)) { + user_path = arg + 12; + continue; + } + if (!strncmp(arg, "--pid-file=", 11)) { + pid_file = arg + 11; + continue; + } + if (!strcmp(arg, "--detach")) { + detach = 1; + log_syslog = 1; + continue; + } + if (!strncmp(arg, "--user=", 7)) { + user_name = arg + 7; + continue; + } + if (!strncmp(arg, "--group=", 8)) { + group_name = arg + 8; + continue; + } + if (!strncmp(arg, "--enable=", 9)) { + enable_service(arg + 9, 1); + continue; + } + if (!strncmp(arg, "--disable=", 10)) { + enable_service(arg + 10, 0); + continue; + } + if (!strncmp(arg, "--allow-override=", 17)) { + make_service_overridable(arg + 17, 1); + continue; + } + if (!strncmp(arg, "--forbid-override=", 18)) { + make_service_overridable(arg + 18, 0); + continue; + } + if (!strcmp(arg, "--")) { + ok_paths = &argv[i+1]; + break; + } else if (arg[0] != '-') { + ok_paths = &argv[i]; + break; + } + + usage(daemon_usage); + } + + if (inetd_mode && (group_name || user_name)) + die("--user and --group are incompatible with --inetd"); + + if (inetd_mode && (listen_port || listen_addr)) + die("--listen= and --port= are incompatible with --inetd"); + else if (listen_port == 0) + listen_port = DEFAULT_GIT_PORT; + + if (group_name && !user_name) + die("--group supplied without --user"); + + if (user_name) { + pass = getpwnam(user_name); + if (!pass) + die("user not found - %s", user_name); + + if (!group_name) + gid = pass->pw_gid; + else { + group = getgrnam(group_name); + if (!group) + die("group not found - %s", group_name); + + gid = group->gr_gid; + } + } + + if (log_syslog) { + openlog("git-daemon", 0, LOG_DAEMON); + set_die_routine(daemon_die); + } + + if (strict_paths && (!ok_paths || !*ok_paths)) + die("option --strict-paths requires a whitelist"); + + if (inetd_mode) { + struct sockaddr_storage ss; + struct sockaddr *peer = (struct sockaddr *)&ss; + socklen_t slen = sizeof(ss); + + freopen("/dev/null", "w", stderr); + + if (getpeername(0, peer, &slen)) + peer = NULL; + + return execute(peer); + } + + if (detach) + daemonize(); + else + sanitize_stdfds(); + + if (pid_file) + store_pid(pid_file); + + return serve(listen_addr, listen_port, pass, gid); +} diff --git a/date.c b/date.c new file mode 100644 index 0000000000..542c004c2e --- /dev/null +++ b/date.c @@ -0,0 +1,796 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ + +#include "cache.h" + +static time_t my_mktime(struct tm *tm) +{ + static const int mdays[] = { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 + }; + int year = tm->tm_year - 70; + int month = tm->tm_mon; + int day = tm->tm_mday; + + if (year < 0 || year > 129) /* algo only works for 1970-2099 */ + return -1; + if (month < 0 || month > 11) /* array bounds */ + return -1; + if (month < 2 || (year + 2) % 4) + day--; + return (year * 365 + (year + 1) / 4 + mdays[month] + day) * 24*60*60UL + + tm->tm_hour * 60*60 + tm->tm_min * 60 + tm->tm_sec; +} + +static const char *month_names[] = { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +}; + +static const char *weekday_names[] = { + "Sundays", "Mondays", "Tuesdays", "Wednesdays", "Thursdays", "Fridays", "Saturdays" +}; + +static time_t gm_time_t(unsigned long time, int tz) +{ + int minutes; + + minutes = tz < 0 ? -tz : tz; + minutes = (minutes / 100)*60 + (minutes % 100); + minutes = tz < 0 ? -minutes : minutes; + return time + minutes * 60; +} + +/* + * The "tz" thing is passed in as this strange "decimal parse of tz" + * thing, which means that tz -0100 is passed in as the integer -100, + * even though it means "sixty minutes off" + */ +static struct tm *time_to_tm(unsigned long time, int tz) +{ + time_t t = gm_time_t(time, tz); + return gmtime(&t); +} + +const char *show_date(unsigned long time, int tz, int relative) +{ + struct tm *tm; + static char timebuf[200]; + + if (relative) { + unsigned long diff; + struct timeval now; + gettimeofday(&now, NULL); + if (now.tv_sec < time) + return "in the future"; + diff = now.tv_sec - time; + if (diff < 90) { + snprintf(timebuf, sizeof(timebuf), "%lu seconds ago", diff); + return timebuf; + } + /* Turn it into minutes */ + diff = (diff + 30) / 60; + if (diff < 90) { + snprintf(timebuf, sizeof(timebuf), "%lu minutes ago", diff); + return timebuf; + } + /* Turn it into hours */ + diff = (diff + 30) / 60; + if (diff < 36) { + snprintf(timebuf, sizeof(timebuf), "%lu hours ago", diff); + return timebuf; + } + /* We deal with number of days from here on */ + diff = (diff + 12) / 24; + if (diff < 14) { + snprintf(timebuf, sizeof(timebuf), "%lu days ago", diff); + return timebuf; + } + /* Say weeks for the past 10 weeks or so */ + if (diff < 70) { + snprintf(timebuf, sizeof(timebuf), "%lu weeks ago", (diff + 3) / 7); + return timebuf; + } + /* Say months for the past 12 months or so */ + if (diff < 360) { + snprintf(timebuf, sizeof(timebuf), "%lu months ago", (diff + 15) / 30); + return timebuf; + } + /* Else fall back on absolute format.. */ + } + + tm = time_to_tm(time, tz); + if (!tm) + return NULL; + sprintf(timebuf, "%.3s %.3s %d %02d:%02d:%02d %d %+05d", + weekday_names[tm->tm_wday], + month_names[tm->tm_mon], + tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec, + tm->tm_year + 1900, tz); + return timebuf; +} + +const char *show_rfc2822_date(unsigned long time, int tz) +{ + struct tm *tm; + static char timebuf[200]; + + tm = time_to_tm(time, tz); + if (!tm) + return NULL; + sprintf(timebuf, "%.3s, %d %.3s %d %02d:%02d:%02d %+05d", + weekday_names[tm->tm_wday], tm->tm_mday, + month_names[tm->tm_mon], tm->tm_year + 1900, + tm->tm_hour, tm->tm_min, tm->tm_sec, tz); + return timebuf; +} + +/* + * Check these. And note how it doesn't do the summer-time conversion. + * + * In my world, it's always summer, and things are probably a bit off + * in other ways too. + */ +static const struct { + const char *name; + int offset; + int dst; +} timezone_names[] = { + { "IDLW", -12, 0, }, /* International Date Line West */ + { "NT", -11, 0, }, /* Nome */ + { "CAT", -10, 0, }, /* Central Alaska */ + { "HST", -10, 0, }, /* Hawaii Standard */ + { "HDT", -10, 1, }, /* Hawaii Daylight */ + { "YST", -9, 0, }, /* Yukon Standard */ + { "YDT", -9, 1, }, /* Yukon Daylight */ + { "PST", -8, 0, }, /* Pacific Standard */ + { "PDT", -8, 1, }, /* Pacific Daylight */ + { "MST", -7, 0, }, /* Mountain Standard */ + { "MDT", -7, 1, }, /* Mountain Daylight */ + { "CST", -6, 0, }, /* Central Standard */ + { "CDT", -6, 1, }, /* Central Daylight */ + { "EST", -5, 0, }, /* Eastern Standard */ + { "EDT", -5, 1, }, /* Eastern Daylight */ + { "AST", -3, 0, }, /* Atlantic Standard */ + { "ADT", -3, 1, }, /* Atlantic Daylight */ + { "WAT", -1, 0, }, /* West Africa */ + + { "GMT", 0, 0, }, /* Greenwich Mean */ + { "UTC", 0, 0, }, /* Universal (Coordinated) */ + + { "WET", 0, 0, }, /* Western European */ + { "BST", 0, 1, }, /* British Summer */ + { "CET", +1, 0, }, /* Central European */ + { "MET", +1, 0, }, /* Middle European */ + { "MEWT", +1, 0, }, /* Middle European Winter */ + { "MEST", +1, 1, }, /* Middle European Summer */ + { "CEST", +1, 1, }, /* Central European Summer */ + { "MESZ", +1, 1, }, /* Middle European Summer */ + { "FWT", +1, 0, }, /* French Winter */ + { "FST", +1, 1, }, /* French Summer */ + { "EET", +2, 0, }, /* Eastern Europe, USSR Zone 1 */ + { "EEST", +2, 1, }, /* Eastern European Daylight */ + { "WAST", +7, 0, }, /* West Australian Standard */ + { "WADT", +7, 1, }, /* West Australian Daylight */ + { "CCT", +8, 0, }, /* China Coast, USSR Zone 7 */ + { "JST", +9, 0, }, /* Japan Standard, USSR Zone 8 */ + { "EAST", +10, 0, }, /* Eastern Australian Standard */ + { "EADT", +10, 1, }, /* Eastern Australian Daylight */ + { "GST", +10, 0, }, /* Guam Standard, USSR Zone 9 */ + { "NZT", +11, 0, }, /* New Zealand */ + { "NZST", +11, 0, }, /* New Zealand Standard */ + { "NZDT", +11, 1, }, /* New Zealand Daylight */ + { "IDLE", +12, 0, }, /* International Date Line East */ +}; + +static int match_string(const char *date, const char *str) +{ + int i = 0; + + for (i = 0; *date; date++, str++, i++) { + if (*date == *str) + continue; + if (toupper(*date) == toupper(*str)) + continue; + if (!isalnum(*date)) + break; + return 0; + } + return i; +} + +static int skip_alpha(const char *date) +{ + int i = 0; + do { + i++; + } while (isalpha(date[i])); + return i; +} + +/* +* Parse month, weekday, or timezone name +*/ +static int match_alpha(const char *date, struct tm *tm, int *offset) +{ + int i; + + for (i = 0; i < 12; i++) { + int match = match_string(date, month_names[i]); + if (match >= 3) { + tm->tm_mon = i; + return match; + } + } + + for (i = 0; i < 7; i++) { + int match = match_string(date, weekday_names[i]); + if (match >= 3) { + tm->tm_wday = i; + return match; + } + } + + for (i = 0; i < ARRAY_SIZE(timezone_names); i++) { + int match = match_string(date, timezone_names[i].name); + if (match >= 3) { + int off = timezone_names[i].offset; + + /* This is bogus, but we like summer */ + off += timezone_names[i].dst; + + /* Only use the tz name offset if we don't have anything better */ + if (*offset == -1) + *offset = 60*off; + + return match; + } + } + + if (match_string(date, "PM") == 2) { + tm->tm_hour = (tm->tm_hour % 12) + 12; + return 2; + } + + if (match_string(date, "AM") == 2) { + tm->tm_hour = (tm->tm_hour % 12) + 0; + return 2; + } + + /* BAD CRAP */ + return skip_alpha(date); +} + +static int is_date(int year, int month, int day, struct tm *now_tm, time_t now, struct tm *tm) +{ + if (month > 0 && month < 13 && day > 0 && day < 32) { + struct tm check = *tm; + struct tm *r = (now_tm ? &check : tm); + time_t specified; + + r->tm_mon = month - 1; + r->tm_mday = day; + if (year == -1) { + if (!now_tm) + return 1; + r->tm_year = now_tm->tm_year; + } + else if (year >= 1970 && year < 2100) + r->tm_year = year - 1900; + else if (year > 70 && year < 100) + r->tm_year = year; + else if (year < 38) + r->tm_year = year + 100; + else + return 0; + if (!now_tm) + return 1; + + specified = my_mktime(r); + + /* Be it commit time or author time, it does not make + * sense to specify timestamp way into the future. Make + * sure it is not later than ten days from now... + */ + if (now + 10*24*3600 < specified) + return 0; + tm->tm_mon = r->tm_mon; + tm->tm_mday = r->tm_mday; + if (year != -1) + tm->tm_year = r->tm_year; + return 1; + } + return 0; +} + +static int match_multi_number(unsigned long num, char c, const char *date, char *end, struct tm *tm) +{ + time_t now; + struct tm now_tm; + struct tm *refuse_future; + long num2, num3; + + num2 = strtol(end+1, &end, 10); + num3 = -1; + if (*end == c && isdigit(end[1])) + num3 = strtol(end+1, &end, 10); + + /* Time? Date? */ + switch (c) { + case ':': + if (num3 < 0) + num3 = 0; + if (num < 25 && num2 >= 0 && num2 < 60 && num3 >= 0 && num3 <= 60) { + tm->tm_hour = num; + tm->tm_min = num2; + tm->tm_sec = num3; + break; + } + return 0; + + case '-': + case '/': + case '.': + now = time(NULL); + refuse_future = NULL; + if (gmtime_r(&now, &now_tm)) + refuse_future = &now_tm; + + if (num > 70) { + /* yyyy-mm-dd? */ + if (is_date(num, num2, num3, refuse_future, now, tm)) + break; + /* yyyy-dd-mm? */ + if (is_date(num, num3, num2, refuse_future, now, tm)) + break; + } + /* Our eastern European friends say dd.mm.yy[yy] + * is the norm there, so giving precedence to + * mm/dd/yy[yy] form only when separator is not '.' + */ + if (c != '.' && + is_date(num3, num, num2, refuse_future, now, tm)) + break; + /* European dd.mm.yy[yy] or funny US dd/mm/yy[yy] */ + if (is_date(num3, num2, num, refuse_future, now, tm)) + break; + /* Funny European mm.dd.yy */ + if (c == '.' && + is_date(num3, num, num2, refuse_future, now, tm)) + break; + return 0; + } + return end - date; +} + +/* + * We've seen a digit. Time? Year? Date? + */ +static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt) +{ + int n; + char *end; + unsigned long num; + + num = strtoul(date, &end, 10); + + /* + * Seconds since 1970? We trigger on that for anything after Jan 1, 2000 + */ + if (num > 946684800) { + time_t time = num; + if (gmtime_r(&time, tm)) { + *tm_gmt = 1; + return end - date; + } + } + + /* + * Check for special formats: num[-.:/]num[same]num + */ + switch (*end) { + case ':': + case '.': + case '/': + case '-': + if (isdigit(end[1])) { + int match = match_multi_number(num, *end, date, end, tm); + if (match) + return match; + } + } + + /* + * None of the special formats? Try to guess what + * the number meant. We use the number of digits + * to make a more educated guess.. + */ + n = 0; + do { + n++; + } while (isdigit(date[n])); + + /* Four-digit year or a timezone? */ + if (n == 4) { + if (num <= 1400 && *offset == -1) { + unsigned int minutes = num % 100; + unsigned int hours = num / 100; + *offset = hours*60 + minutes; + } else if (num > 1900 && num < 2100) + tm->tm_year = num - 1900; + return n; + } + + /* + * NOTE! We will give precedence to day-of-month over month or + * year numbers in the 1-12 range. So 05 is always "mday 5", + * unless we already have a mday.. + * + * IOW, 01 Apr 05 parses as "April 1st, 2005". + */ + if (num > 0 && num < 32 && tm->tm_mday < 0) { + tm->tm_mday = num; + return n; + } + + /* Two-digit year? */ + if (n == 2 && tm->tm_year < 0) { + if (num < 10 && tm->tm_mday >= 0) { + tm->tm_year = num + 100; + return n; + } + if (num >= 70) { + tm->tm_year = num; + return n; + } + } + + if (num > 0 && num < 32) { + tm->tm_mday = num; + } else if (num > 1900) { + tm->tm_year = num - 1900; + } else if (num > 70) { + tm->tm_year = num; + } else if (num > 0 && num < 13) { + tm->tm_mon = num-1; + } + + return n; +} + +static int match_tz(const char *date, int *offp) +{ + char *end; + int offset = strtoul(date+1, &end, 10); + int min, hour; + int n = end - date - 1; + + min = offset % 100; + hour = offset / 100; + + /* + * Don't accept any random crap.. At least 3 digits, and + * a valid minute. We might want to check that the minutes + * are divisible by 30 or something too. + */ + if (min < 60 && n > 2) { + offset = hour*60+min; + if (*date == '-') + offset = -offset; + + *offp = offset; + } + return end - date; +} + +static int date_string(unsigned long date, int offset, char *buf, int len) +{ + int sign = '+'; + + if (offset < 0) { + offset = -offset; + sign = '-'; + } + return snprintf(buf, len, "%lu %c%02d%02d", date, sign, offset / 60, offset % 60); +} + +/* Gr. strptime is crap for this; it doesn't have a way to require RFC2822 + (i.e. English) day/month names, and it doesn't work correctly with %z. */ +int parse_date(const char *date, char *result, int maxlen) +{ + struct tm tm; + int offset, tm_gmt; + time_t then; + + memset(&tm, 0, sizeof(tm)); + tm.tm_year = -1; + tm.tm_mon = -1; + tm.tm_mday = -1; + tm.tm_isdst = -1; + offset = -1; + tm_gmt = 0; + + for (;;) { + int match = 0; + unsigned char c = *date; + + /* Stop at end of string or newline */ + if (!c || c == '\n') + break; + + if (isalpha(c)) + match = match_alpha(date, &tm, &offset); + else if (isdigit(c)) + match = match_digit(date, &tm, &offset, &tm_gmt); + else if ((c == '-' || c == '+') && isdigit(date[1])) + match = match_tz(date, &offset); + + if (!match) { + /* BAD CRAP */ + match = 1; + } + + date += match; + } + + /* mktime uses local timezone */ + then = my_mktime(&tm); + if (offset == -1) + offset = (then - mktime(&tm)) / 60; + + if (then == -1) + return -1; + + if (!tm_gmt) + then -= offset * 60; + return date_string(then, offset, result, maxlen); +} + +void datestamp(char *buf, int bufsize) +{ + time_t now; + int offset; + + time(&now); + + offset = my_mktime(localtime(&now)) - now; + offset /= 60; + + date_string(now, offset, buf, bufsize); +} + +static void update_tm(struct tm *tm, unsigned long sec) +{ + time_t n = mktime(tm) - sec; + localtime_r(&n, tm); +} + +static void date_yesterday(struct tm *tm, int *num) +{ + update_tm(tm, 24*60*60); +} + +static void date_time(struct tm *tm, int hour) +{ + if (tm->tm_hour < hour) + date_yesterday(tm, NULL); + tm->tm_hour = hour; + tm->tm_min = 0; + tm->tm_sec = 0; +} + +static void date_midnight(struct tm *tm, int *num) +{ + date_time(tm, 0); +} + +static void date_noon(struct tm *tm, int *num) +{ + date_time(tm, 12); +} + +static void date_tea(struct tm *tm, int *num) +{ + date_time(tm, 17); +} + +static void date_pm(struct tm *tm, int *num) +{ + int hour, n = *num; + *num = 0; + + hour = tm->tm_hour; + if (n) { + hour = n; + tm->tm_min = 0; + tm->tm_sec = 0; + } + tm->tm_hour = (hour % 12) + 12; +} + +static void date_am(struct tm *tm, int *num) +{ + int hour, n = *num; + *num = 0; + + hour = tm->tm_hour; + if (n) { + hour = n; + tm->tm_min = 0; + tm->tm_sec = 0; + } + tm->tm_hour = (hour % 12); +} + +static const struct special { + const char *name; + void (*fn)(struct tm *, int *); +} special[] = { + { "yesterday", date_yesterday }, + { "noon", date_noon }, + { "midnight", date_midnight }, + { "tea", date_tea }, + { "PM", date_pm }, + { "AM", date_am }, + { NULL } +}; + +static const char *number_name[] = { + "zero", "one", "two", "three", "four", + "five", "six", "seven", "eight", "nine", "ten", +}; + +static const struct typelen { + const char *type; + int length; +} typelen[] = { + { "seconds", 1 }, + { "minutes", 60 }, + { "hours", 60*60 }, + { "days", 24*60*60 }, + { "weeks", 7*24*60*60 }, + { NULL } +}; + +static const char *approxidate_alpha(const char *date, struct tm *tm, int *num) +{ + const struct typelen *tl; + const struct special *s; + const char *end = date; + int i; + + while (isalpha(*++end)); + ; + + for (i = 0; i < 12; i++) { + int match = match_string(date, month_names[i]); + if (match >= 3) { + tm->tm_mon = i; + return end; + } + } + + for (s = special; s->name; s++) { + int len = strlen(s->name); + if (match_string(date, s->name) == len) { + s->fn(tm, num); + return end; + } + } + + if (!*num) { + for (i = 1; i < 11; i++) { + int len = strlen(number_name[i]); + if (match_string(date, number_name[i]) == len) { + *num = i; + return end; + } + } + if (match_string(date, "last") == 4) + *num = 1; + return end; + } + + tl = typelen; + while (tl->type) { + int len = strlen(tl->type); + if (match_string(date, tl->type) >= len-1) { + update_tm(tm, tl->length * *num); + *num = 0; + return end; + } + tl++; + } + + for (i = 0; i < 7; i++) { + int match = match_string(date, weekday_names[i]); + if (match >= 3) { + int diff, n = *num -1; + *num = 0; + + diff = tm->tm_wday - i; + if (diff <= 0) + n++; + diff += 7*n; + + update_tm(tm, diff * 24 * 60 * 60); + return end; + } + } + + if (match_string(date, "months") >= 5) { + int n = tm->tm_mon - *num; + *num = 0; + while (n < 0) { + n += 12; + tm->tm_year--; + } + tm->tm_mon = n; + return end; + } + + if (match_string(date, "years") >= 4) { + tm->tm_year -= *num; + *num = 0; + return end; + } + + return end; +} + +static const char *approxidate_digit(const char *date, struct tm *tm, int *num) +{ + char *end; + unsigned long number = strtoul(date, &end, 10); + + switch (*end) { + case ':': + case '.': + case '/': + case '-': + if (isdigit(end[1])) { + int match = match_multi_number(number, *end, date, end, tm); + if (match) + return date + match; + } + } + + *num = number; + return end; +} + +unsigned long approxidate(const char *date) +{ + int number = 0; + struct tm tm, now; + struct timeval tv; + char buffer[50]; + + if (parse_date(date, buffer, sizeof(buffer)) > 0) + return strtoul(buffer, NULL, 10); + + gettimeofday(&tv, NULL); + localtime_r(&tv.tv_sec, &tm); + now = tm; + for (;;) { + unsigned char c = *date; + if (!c) + break; + date++; + if (isdigit(c)) { + date = approxidate_digit(date-1, &tm, &number); + continue; + } + if (isalpha(c)) + date = approxidate_alpha(date-1, &tm, &number); + } + if (number > 0 && number < 32) + tm.tm_mday = number; + if (tm.tm_mon > now.tm_mon && tm.tm_year == now.tm_year) + tm.tm_year--; + return mktime(&tm); +} diff --git a/delta.h b/delta.h new file mode 100644 index 0000000000..7b3f86d85f --- /dev/null +++ b/delta.h @@ -0,0 +1,98 @@ +#ifndef DELTA_H +#define DELTA_H + +/* opaque object for delta index */ +struct delta_index; + +/* + * create_delta_index: compute index data from given buffer + * + * This returns a pointer to a struct delta_index that should be passed to + * subsequent create_delta() calls, or to free_delta_index(). A NULL pointer + * is returned on failure. The given buffer must not be freed nor altered + * before free_delta_index() is called. The returned pointer must be freed + * using free_delta_index(). + */ +extern struct delta_index * +create_delta_index(const void *buf, unsigned long bufsize); + +/* + * free_delta_index: free the index created by create_delta_index() + * + * Given pointer must be what create_delta_index() returned, or NULL. + */ +extern void free_delta_index(struct delta_index *index); + +/* + * create_delta: create a delta from given index for the given buffer + * + * This function may be called multiple times with different buffers using + * the same delta_index pointer. If max_delta_size is non-zero and the + * resulting delta is to be larger than max_delta_size then NULL is returned. + * On success, a non-NULL pointer to the buffer with the delta data is + * returned and *delta_size is updated with its size. The returned buffer + * must be freed by the caller. + */ +extern void * +create_delta(const struct delta_index *index, + const void *buf, unsigned long bufsize, + unsigned long *delta_size, unsigned long max_delta_size); + +/* + * diff_delta: create a delta from source buffer to target buffer + * + * If max_delta_size is non-zero and the resulting delta is to be larger + * than max_delta_size then NULL is returned. On success, a non-NULL + * pointer to the buffer with the delta data is returned and *delta_size is + * updated with its size. The returned buffer must be freed by the caller. + */ +static inline void * +diff_delta(const void *src_buf, unsigned long src_bufsize, + const void *trg_buf, unsigned long trg_bufsize, + unsigned long *delta_size, unsigned long max_delta_size) +{ + struct delta_index *index = create_delta_index(src_buf, src_bufsize); + if (index) { + void *delta = create_delta(index, trg_buf, trg_bufsize, + delta_size, max_delta_size); + free_delta_index(index); + return delta; + } + return NULL; +} + +/* + * patch_delta: recreate target buffer given source buffer and delta data + * + * On success, a non-NULL pointer to the target buffer is returned and + * *trg_bufsize is updated with its size. On failure a NULL pointer is + * returned. The returned buffer must be freed by the caller. + */ +extern void *patch_delta(const void *src_buf, unsigned long src_size, + const void *delta_buf, unsigned long delta_size, + unsigned long *dst_size); + +/* the smallest possible delta size is 4 bytes */ +#define DELTA_SIZE_MIN 4 + +/* + * This must be called twice on the delta data buffer, first to get the + * expected source buffer size, and again to get the target buffer size. + */ +static inline unsigned long get_delta_hdr_size(const unsigned char **datap, + const unsigned char *top) +{ + const unsigned char *data = *datap; + unsigned char cmd; + unsigned long size = 0; + int i = 0; + do { + cmd = *data++; + size |= (cmd & ~0x80) << i; + i += 7; + } while (cmd & 0x80 && data < top); + *datap = data; + return size; +} + +#endif diff --git a/diff-delta.c b/diff-delta.c new file mode 100644 index 0000000000..9f998d0a73 --- /dev/null +++ b/diff-delta.c @@ -0,0 +1,410 @@ +/* + * diff-delta.c: generate a delta between two buffers + * + * Many parts of this file have been lifted from LibXDiff version 0.10. + * http://www.xmailserver.org/xdiff-lib.html + * + * LibXDiff was written by Davide Libenzi <davidel@xmailserver.org> + * Copyright (C) 2003 Davide Libenzi + * + * Many mods for GIT usage by Nicolas Pitre <nico@cam.org>, (C) 2005. + * + * This file is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * Use of this within git automatically means that the LGPL + * licensing gets turned into GPLv2 within this project. + */ + +#include "git-compat-util.h" +#include "delta.h" + +/* maximum hash entry list for the same hash bucket */ +#define HASH_LIMIT 64 + +#define RABIN_SHIFT 23 +#define RABIN_WINDOW 16 + +static const unsigned int T[256] = { + 0x00000000, 0xab59b4d1, 0x56b369a2, 0xfdeadd73, 0x063f6795, 0xad66d344, + 0x508c0e37, 0xfbd5bae6, 0x0c7ecf2a, 0xa7277bfb, 0x5acda688, 0xf1941259, + 0x0a41a8bf, 0xa1181c6e, 0x5cf2c11d, 0xf7ab75cc, 0x18fd9e54, 0xb3a42a85, + 0x4e4ef7f6, 0xe5174327, 0x1ec2f9c1, 0xb59b4d10, 0x48719063, 0xe32824b2, + 0x1483517e, 0xbfdae5af, 0x423038dc, 0xe9698c0d, 0x12bc36eb, 0xb9e5823a, + 0x440f5f49, 0xef56eb98, 0x31fb3ca8, 0x9aa28879, 0x6748550a, 0xcc11e1db, + 0x37c45b3d, 0x9c9defec, 0x6177329f, 0xca2e864e, 0x3d85f382, 0x96dc4753, + 0x6b369a20, 0xc06f2ef1, 0x3bba9417, 0x90e320c6, 0x6d09fdb5, 0xc6504964, + 0x2906a2fc, 0x825f162d, 0x7fb5cb5e, 0xd4ec7f8f, 0x2f39c569, 0x846071b8, + 0x798aaccb, 0xd2d3181a, 0x25786dd6, 0x8e21d907, 0x73cb0474, 0xd892b0a5, + 0x23470a43, 0x881ebe92, 0x75f463e1, 0xdeadd730, 0x63f67950, 0xc8afcd81, + 0x354510f2, 0x9e1ca423, 0x65c91ec5, 0xce90aa14, 0x337a7767, 0x9823c3b6, + 0x6f88b67a, 0xc4d102ab, 0x393bdfd8, 0x92626b09, 0x69b7d1ef, 0xc2ee653e, + 0x3f04b84d, 0x945d0c9c, 0x7b0be704, 0xd05253d5, 0x2db88ea6, 0x86e13a77, + 0x7d348091, 0xd66d3440, 0x2b87e933, 0x80de5de2, 0x7775282e, 0xdc2c9cff, + 0x21c6418c, 0x8a9ff55d, 0x714a4fbb, 0xda13fb6a, 0x27f92619, 0x8ca092c8, + 0x520d45f8, 0xf954f129, 0x04be2c5a, 0xafe7988b, 0x5432226d, 0xff6b96bc, + 0x02814bcf, 0xa9d8ff1e, 0x5e738ad2, 0xf52a3e03, 0x08c0e370, 0xa39957a1, + 0x584ced47, 0xf3155996, 0x0eff84e5, 0xa5a63034, 0x4af0dbac, 0xe1a96f7d, + 0x1c43b20e, 0xb71a06df, 0x4ccfbc39, 0xe79608e8, 0x1a7cd59b, 0xb125614a, + 0x468e1486, 0xedd7a057, 0x103d7d24, 0xbb64c9f5, 0x40b17313, 0xebe8c7c2, + 0x16021ab1, 0xbd5bae60, 0x6cb54671, 0xc7ecf2a0, 0x3a062fd3, 0x915f9b02, + 0x6a8a21e4, 0xc1d39535, 0x3c394846, 0x9760fc97, 0x60cb895b, 0xcb923d8a, + 0x3678e0f9, 0x9d215428, 0x66f4eece, 0xcdad5a1f, 0x3047876c, 0x9b1e33bd, + 0x7448d825, 0xdf116cf4, 0x22fbb187, 0x89a20556, 0x7277bfb0, 0xd92e0b61, + 0x24c4d612, 0x8f9d62c3, 0x7836170f, 0xd36fa3de, 0x2e857ead, 0x85dcca7c, + 0x7e09709a, 0xd550c44b, 0x28ba1938, 0x83e3ade9, 0x5d4e7ad9, 0xf617ce08, + 0x0bfd137b, 0xa0a4a7aa, 0x5b711d4c, 0xf028a99d, 0x0dc274ee, 0xa69bc03f, + 0x5130b5f3, 0xfa690122, 0x0783dc51, 0xacda6880, 0x570fd266, 0xfc5666b7, + 0x01bcbbc4, 0xaae50f15, 0x45b3e48d, 0xeeea505c, 0x13008d2f, 0xb85939fe, + 0x438c8318, 0xe8d537c9, 0x153feaba, 0xbe665e6b, 0x49cd2ba7, 0xe2949f76, + 0x1f7e4205, 0xb427f6d4, 0x4ff24c32, 0xe4abf8e3, 0x19412590, 0xb2189141, + 0x0f433f21, 0xa41a8bf0, 0x59f05683, 0xf2a9e252, 0x097c58b4, 0xa225ec65, + 0x5fcf3116, 0xf49685c7, 0x033df00b, 0xa86444da, 0x558e99a9, 0xfed72d78, + 0x0502979e, 0xae5b234f, 0x53b1fe3c, 0xf8e84aed, 0x17bea175, 0xbce715a4, + 0x410dc8d7, 0xea547c06, 0x1181c6e0, 0xbad87231, 0x4732af42, 0xec6b1b93, + 0x1bc06e5f, 0xb099da8e, 0x4d7307fd, 0xe62ab32c, 0x1dff09ca, 0xb6a6bd1b, + 0x4b4c6068, 0xe015d4b9, 0x3eb80389, 0x95e1b758, 0x680b6a2b, 0xc352defa, + 0x3887641c, 0x93ded0cd, 0x6e340dbe, 0xc56db96f, 0x32c6cca3, 0x999f7872, + 0x6475a501, 0xcf2c11d0, 0x34f9ab36, 0x9fa01fe7, 0x624ac294, 0xc9137645, + 0x26459ddd, 0x8d1c290c, 0x70f6f47f, 0xdbaf40ae, 0x207afa48, 0x8b234e99, + 0x76c993ea, 0xdd90273b, 0x2a3b52f7, 0x8162e626, 0x7c883b55, 0xd7d18f84, + 0x2c043562, 0x875d81b3, 0x7ab75cc0, 0xd1eee811 +}; + +static const unsigned int U[256] = { + 0x00000000, 0x7eb5200d, 0x5633f4cb, 0x2886d4c6, 0x073e5d47, 0x798b7d4a, + 0x510da98c, 0x2fb88981, 0x0e7cba8e, 0x70c99a83, 0x584f4e45, 0x26fa6e48, + 0x0942e7c9, 0x77f7c7c4, 0x5f711302, 0x21c4330f, 0x1cf9751c, 0x624c5511, + 0x4aca81d7, 0x347fa1da, 0x1bc7285b, 0x65720856, 0x4df4dc90, 0x3341fc9d, + 0x1285cf92, 0x6c30ef9f, 0x44b63b59, 0x3a031b54, 0x15bb92d5, 0x6b0eb2d8, + 0x4388661e, 0x3d3d4613, 0x39f2ea38, 0x4747ca35, 0x6fc11ef3, 0x11743efe, + 0x3eccb77f, 0x40799772, 0x68ff43b4, 0x164a63b9, 0x378e50b6, 0x493b70bb, + 0x61bda47d, 0x1f088470, 0x30b00df1, 0x4e052dfc, 0x6683f93a, 0x1836d937, + 0x250b9f24, 0x5bbebf29, 0x73386bef, 0x0d8d4be2, 0x2235c263, 0x5c80e26e, + 0x740636a8, 0x0ab316a5, 0x2b7725aa, 0x55c205a7, 0x7d44d161, 0x03f1f16c, + 0x2c4978ed, 0x52fc58e0, 0x7a7a8c26, 0x04cfac2b, 0x73e5d470, 0x0d50f47d, + 0x25d620bb, 0x5b6300b6, 0x74db8937, 0x0a6ea93a, 0x22e87dfc, 0x5c5d5df1, + 0x7d996efe, 0x032c4ef3, 0x2baa9a35, 0x551fba38, 0x7aa733b9, 0x041213b4, + 0x2c94c772, 0x5221e77f, 0x6f1ca16c, 0x11a98161, 0x392f55a7, 0x479a75aa, + 0x6822fc2b, 0x1697dc26, 0x3e1108e0, 0x40a428ed, 0x61601be2, 0x1fd53bef, + 0x3753ef29, 0x49e6cf24, 0x665e46a5, 0x18eb66a8, 0x306db26e, 0x4ed89263, + 0x4a173e48, 0x34a21e45, 0x1c24ca83, 0x6291ea8e, 0x4d29630f, 0x339c4302, + 0x1b1a97c4, 0x65afb7c9, 0x446b84c6, 0x3adea4cb, 0x1258700d, 0x6ced5000, + 0x4355d981, 0x3de0f98c, 0x15662d4a, 0x6bd30d47, 0x56ee4b54, 0x285b6b59, + 0x00ddbf9f, 0x7e689f92, 0x51d01613, 0x2f65361e, 0x07e3e2d8, 0x7956c2d5, + 0x5892f1da, 0x2627d1d7, 0x0ea10511, 0x7014251c, 0x5facac9d, 0x21198c90, + 0x099f5856, 0x772a785b, 0x4c921c31, 0x32273c3c, 0x1aa1e8fa, 0x6414c8f7, + 0x4bac4176, 0x3519617b, 0x1d9fb5bd, 0x632a95b0, 0x42eea6bf, 0x3c5b86b2, + 0x14dd5274, 0x6a687279, 0x45d0fbf8, 0x3b65dbf5, 0x13e30f33, 0x6d562f3e, + 0x506b692d, 0x2ede4920, 0x06589de6, 0x78edbdeb, 0x5755346a, 0x29e01467, + 0x0166c0a1, 0x7fd3e0ac, 0x5e17d3a3, 0x20a2f3ae, 0x08242768, 0x76910765, + 0x59298ee4, 0x279caee9, 0x0f1a7a2f, 0x71af5a22, 0x7560f609, 0x0bd5d604, + 0x235302c2, 0x5de622cf, 0x725eab4e, 0x0ceb8b43, 0x246d5f85, 0x5ad87f88, + 0x7b1c4c87, 0x05a96c8a, 0x2d2fb84c, 0x539a9841, 0x7c2211c0, 0x029731cd, + 0x2a11e50b, 0x54a4c506, 0x69998315, 0x172ca318, 0x3faa77de, 0x411f57d3, + 0x6ea7de52, 0x1012fe5f, 0x38942a99, 0x46210a94, 0x67e5399b, 0x19501996, + 0x31d6cd50, 0x4f63ed5d, 0x60db64dc, 0x1e6e44d1, 0x36e89017, 0x485db01a, + 0x3f77c841, 0x41c2e84c, 0x69443c8a, 0x17f11c87, 0x38499506, 0x46fcb50b, + 0x6e7a61cd, 0x10cf41c0, 0x310b72cf, 0x4fbe52c2, 0x67388604, 0x198da609, + 0x36352f88, 0x48800f85, 0x6006db43, 0x1eb3fb4e, 0x238ebd5d, 0x5d3b9d50, + 0x75bd4996, 0x0b08699b, 0x24b0e01a, 0x5a05c017, 0x728314d1, 0x0c3634dc, + 0x2df207d3, 0x534727de, 0x7bc1f318, 0x0574d315, 0x2acc5a94, 0x54797a99, + 0x7cffae5f, 0x024a8e52, 0x06852279, 0x78300274, 0x50b6d6b2, 0x2e03f6bf, + 0x01bb7f3e, 0x7f0e5f33, 0x57888bf5, 0x293dabf8, 0x08f998f7, 0x764cb8fa, + 0x5eca6c3c, 0x207f4c31, 0x0fc7c5b0, 0x7172e5bd, 0x59f4317b, 0x27411176, + 0x1a7c5765, 0x64c97768, 0x4c4fa3ae, 0x32fa83a3, 0x1d420a22, 0x63f72a2f, + 0x4b71fee9, 0x35c4dee4, 0x1400edeb, 0x6ab5cde6, 0x42331920, 0x3c86392d, + 0x133eb0ac, 0x6d8b90a1, 0x450d4467, 0x3bb8646a +}; + +struct index_entry { + const unsigned char *ptr; + unsigned int val; + struct index_entry *next; +}; + +struct delta_index { + const void *src_buf; + unsigned long src_size; + unsigned int hash_mask; + struct index_entry *hash[FLEX_ARRAY]; +}; + +struct delta_index * create_delta_index(const void *buf, unsigned long bufsize) +{ + unsigned int i, hsize, hmask, entries, prev_val, *hash_count; + const unsigned char *data, *buffer = buf; + struct delta_index *index; + struct index_entry *entry, **hash; + void *mem; + unsigned long memsize; + + if (!buf || !bufsize) + return NULL; + + /* Determine index hash size. Note that indexing skips the + first byte to allow for optimizing the Rabin's polynomial + initialization in create_delta(). */ + entries = (bufsize - 1) / RABIN_WINDOW; + hsize = entries / 4; + for (i = 4; (1u << i) < hsize && i < 31; i++); + hsize = 1 << i; + hmask = hsize - 1; + + /* allocate lookup index */ + memsize = sizeof(*index) + + sizeof(*hash) * hsize + + sizeof(*entry) * entries; + mem = malloc(memsize); + if (!mem) + return NULL; + index = mem; + mem = index + 1; + hash = mem; + mem = hash + hsize; + entry = mem; + + index->src_buf = buf; + index->src_size = bufsize; + index->hash_mask = hmask; + memset(hash, 0, hsize * sizeof(*hash)); + + /* allocate an array to count hash entries */ + hash_count = calloc(hsize, sizeof(*hash_count)); + if (!hash_count) { + free(index); + return NULL; + } + + /* then populate the index */ + prev_val = ~0; + for (data = buffer + entries * RABIN_WINDOW - RABIN_WINDOW; + data >= buffer; + data -= RABIN_WINDOW) { + unsigned int val = 0; + for (i = 1; i <= RABIN_WINDOW; i++) + val = ((val << 8) | data[i]) ^ T[val >> RABIN_SHIFT]; + if (val == prev_val) { + /* keep the lowest of consecutive identical blocks */ + entry[-1].ptr = data + RABIN_WINDOW; + } else { + prev_val = val; + i = val & hmask; + entry->ptr = data + RABIN_WINDOW; + entry->val = val; + entry->next = hash[i]; + hash[i] = entry++; + hash_count[i]++; + } + } + + /* + * Determine a limit on the number of entries in the same hash + * bucket. This guards us against pathological data sets causing + * really bad hash distribution with most entries in the same hash + * bucket that would bring us to O(m*n) computing costs (m and n + * corresponding to reference and target buffer sizes). + * + * Make sure none of the hash buckets has more entries than + * we're willing to test. Otherwise we cull the entry list + * uniformly to still preserve a good repartition across + * the reference buffer. + */ + for (i = 0; i < hsize; i++) { + if (hash_count[i] < HASH_LIMIT) + continue; + entry = hash[i]; + do { + struct index_entry *keep = entry; + int skip = hash_count[i] / HASH_LIMIT / 2; + do { + entry = entry->next; + } while(--skip && entry); + keep->next = entry; + } while(entry); + } + free(hash_count); + + return index; +} + +void free_delta_index(struct delta_index *index) +{ + free(index); +} + +/* + * The maximum size for any opcode sequence, including the initial header + * plus Rabin window plus biggest copy. + */ +#define MAX_OP_SIZE (5 + 5 + 1 + RABIN_WINDOW + 7) + +void * +create_delta(const struct delta_index *index, + const void *trg_buf, unsigned long trg_size, + unsigned long *delta_size, unsigned long max_size) +{ + unsigned int i, outpos, outsize, val; + int inscnt; + const unsigned char *ref_data, *ref_top, *data, *top; + unsigned char *out; + + if (!trg_buf || !trg_size) + return NULL; + + outpos = 0; + outsize = 8192; + if (max_size && outsize >= max_size) + outsize = max_size + MAX_OP_SIZE + 1; + out = malloc(outsize); + if (!out) + return NULL; + + /* store reference buffer size */ + i = index->src_size; + while (i >= 0x80) { + out[outpos++] = i | 0x80; + i >>= 7; + } + out[outpos++] = i; + + /* store target buffer size */ + i = trg_size; + while (i >= 0x80) { + out[outpos++] = i | 0x80; + i >>= 7; + } + out[outpos++] = i; + + ref_data = index->src_buf; + ref_top = ref_data + index->src_size; + data = trg_buf; + top = (const unsigned char *) trg_buf + trg_size; + + outpos++; + val = 0; + for (i = 0; i < RABIN_WINDOW && data < top; i++, data++) { + out[outpos++] = *data; + val = ((val << 8) | *data) ^ T[val >> RABIN_SHIFT]; + } + inscnt = i; + + while (data < top) { + unsigned int moff = 0, msize = 0; + struct index_entry *entry; + val ^= U[data[-RABIN_WINDOW]]; + val = ((val << 8) | *data) ^ T[val >> RABIN_SHIFT]; + i = val & index->hash_mask; + for (entry = index->hash[i]; entry; entry = entry->next) { + const unsigned char *ref = entry->ptr; + const unsigned char *src = data; + unsigned int ref_size = ref_top - ref; + if (entry->val != val) + continue; + if (ref_size > top - src) + ref_size = top - src; + if (ref_size > 0x10000) + ref_size = 0x10000; + if (ref_size <= msize) + break; + while (ref_size-- && *src++ == *ref) + ref++; + if (msize < ref - entry->ptr) { + /* this is our best match so far */ + msize = ref - entry->ptr; + moff = entry->ptr - ref_data; + } + } + + if (msize < 4) { + if (!inscnt) + outpos++; + out[outpos++] = *data++; + inscnt++; + if (inscnt == 0x7f) { + out[outpos - inscnt - 1] = inscnt; + inscnt = 0; + } + } else { + unsigned char *op; + + if (msize >= RABIN_WINDOW) { + const unsigned char *sk; + sk = data + msize - RABIN_WINDOW; + val = 0; + for (i = 0; i < RABIN_WINDOW; i++) + val = ((val << 8) | *sk++) ^ T[val >> RABIN_SHIFT]; + } else { + const unsigned char *sk = data + 1; + for (i = 1; i < msize; i++) { + val ^= U[sk[-RABIN_WINDOW]]; + val = ((val << 8) | *sk++) ^ T[val >> RABIN_SHIFT]; + } + } + + if (inscnt) { + while (moff && ref_data[moff-1] == data[-1]) { + if (msize == 0x10000) + break; + /* we can match one byte back */ + msize++; + moff--; + data--; + outpos--; + if (--inscnt) + continue; + outpos--; /* remove count slot */ + inscnt--; /* make it -1 */ + break; + } + out[outpos - inscnt - 1] = inscnt; + inscnt = 0; + } + + data += msize; + op = out + outpos++; + i = 0x80; + + if (moff & 0xff) { out[outpos++] = moff; i |= 0x01; } + moff >>= 8; + if (moff & 0xff) { out[outpos++] = moff; i |= 0x02; } + moff >>= 8; + if (moff & 0xff) { out[outpos++] = moff; i |= 0x04; } + moff >>= 8; + if (moff & 0xff) { out[outpos++] = moff; i |= 0x08; } + + if (msize & 0xff) { out[outpos++] = msize; i |= 0x10; } + msize >>= 8; + if (msize & 0xff) { out[outpos++] = msize; i |= 0x20; } + + *op = i; + } + + if (outpos >= outsize - MAX_OP_SIZE) { + void *tmp = out; + outsize = outsize * 3 / 2; + if (max_size && outsize >= max_size) + outsize = max_size + MAX_OP_SIZE + 1; + if (max_size && outpos > max_size) + break; + out = xrealloc(out, outsize); + if (!out) { + free(tmp); + return NULL; + } + } + } + + if (inscnt) + out[outpos - inscnt - 1] = inscnt; + + if (max_size && outpos > max_size) { + free(out); + return NULL; + } + + *delta_size = outpos; + return out; +} diff --git a/diff-lib.c b/diff-lib.c new file mode 100644 index 0000000000..91cd87742f --- /dev/null +++ b/diff-lib.c @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "quote.h" +#include "commit.h" +#include "diff.h" +#include "diffcore.h" +#include "revision.h" +#include "cache-tree.h" + +/* + * diff-files + */ + +int run_diff_files(struct rev_info *revs, int silent_on_removed) +{ + int entries, i; + int diff_unmerged_stage = revs->max_count; + + if (diff_unmerged_stage < 0) + diff_unmerged_stage = 2; + entries = read_cache(); + if (entries < 0) { + perror("read_cache"); + return -1; + } + for (i = 0; i < entries; i++) { + struct stat st; + unsigned int oldmode, newmode; + struct cache_entry *ce = active_cache[i]; + int changed; + + if (!ce_path_match(ce, revs->prune_data)) + continue; + + if (ce_stage(ce)) { + struct combine_diff_path *dpath; + int num_compare_stages = 0; + size_t path_len; + + path_len = ce_namelen(ce); + + dpath = xmalloc (combine_diff_path_size (5, path_len)); + dpath->path = (char *) &(dpath->parent[5]); + + dpath->next = NULL; + dpath->len = path_len; + memcpy(dpath->path, ce->name, path_len); + dpath->path[path_len] = '\0'; + dpath->mode = 0; + hashclr(dpath->sha1); + memset(&(dpath->parent[0]), 0, + sizeof(struct combine_diff_parent)*5); + + while (i < entries) { + struct cache_entry *nce = active_cache[i]; + int stage; + + if (strcmp(ce->name, nce->name)) + break; + + /* Stage #2 (ours) is the first parent, + * stage #3 (theirs) is the second. + */ + stage = ce_stage(nce); + if (2 <= stage) { + int mode = ntohl(nce->ce_mode); + num_compare_stages++; + hashcpy(dpath->parent[stage-2].sha1, nce->sha1); + dpath->parent[stage-2].mode = + canon_mode(mode); + dpath->parent[stage-2].status = + DIFF_STATUS_MODIFIED; + } + + /* diff against the proper unmerged stage */ + if (stage == diff_unmerged_stage) + ce = nce; + i++; + } + /* + * Compensate for loop update + */ + i--; + + if (revs->combine_merges && num_compare_stages == 2) { + show_combined_diff(dpath, 2, + revs->dense_combined_merges, + revs); + free(dpath); + continue; + } + free(dpath); + dpath = NULL; + + /* + * Show the diff for the 'ce' if we found the one + * from the desired stage. + */ + diff_unmerge(&revs->diffopt, ce->name, 0, null_sha1); + if (ce_stage(ce) != diff_unmerged_stage) + continue; + } + + if (lstat(ce->name, &st) < 0) { + if (errno != ENOENT && errno != ENOTDIR) { + perror(ce->name); + continue; + } + if (silent_on_removed) + continue; + diff_addremove(&revs->diffopt, '-', ntohl(ce->ce_mode), + ce->sha1, ce->name, NULL); + continue; + } + changed = ce_match_stat(ce, &st, 0); + if (!changed && !revs->diffopt.find_copies_harder) + continue; + oldmode = ntohl(ce->ce_mode); + + newmode = canon_mode(st.st_mode); + if (!trust_executable_bit && + S_ISREG(newmode) && S_ISREG(oldmode) && + ((newmode ^ oldmode) == 0111)) + newmode = oldmode; + diff_change(&revs->diffopt, oldmode, newmode, + ce->sha1, (changed ? null_sha1 : ce->sha1), + ce->name, NULL); + + } + diffcore_std(&revs->diffopt); + diff_flush(&revs->diffopt); + return 0; +} + +/* + * diff-index + */ + +/* A file entry went away or appeared */ +static void diff_index_show_file(struct rev_info *revs, + const char *prefix, + struct cache_entry *ce, + unsigned char *sha1, unsigned int mode) +{ + diff_addremove(&revs->diffopt, prefix[0], ntohl(mode), + sha1, ce->name, NULL); +} + +static int get_stat_data(struct cache_entry *ce, + unsigned char **sha1p, + unsigned int *modep, + int cached, int match_missing) +{ + unsigned char *sha1 = ce->sha1; + unsigned int mode = ce->ce_mode; + + if (!cached) { + static unsigned char no_sha1[20]; + int changed; + struct stat st; + if (lstat(ce->name, &st) < 0) { + if (errno == ENOENT && match_missing) { + *sha1p = sha1; + *modep = mode; + return 0; + } + return -1; + } + changed = ce_match_stat(ce, &st, 0); + if (changed) { + mode = create_ce_mode(st.st_mode); + if (!trust_executable_bit && S_ISREG(st.st_mode)) + mode = ce->ce_mode; + sha1 = no_sha1; + } + } + + *sha1p = sha1; + *modep = mode; + return 0; +} + +static void show_new_file(struct rev_info *revs, + struct cache_entry *new, + int cached, int match_missing) +{ + unsigned char *sha1; + unsigned int mode; + + /* New file in the index: it might actually be different in + * the working copy. + */ + if (get_stat_data(new, &sha1, &mode, cached, match_missing) < 0) + return; + + diff_index_show_file(revs, "+", new, sha1, mode); +} + +static int show_modified(struct rev_info *revs, + struct cache_entry *old, + struct cache_entry *new, + int report_missing, + int cached, int match_missing) +{ + unsigned int mode, oldmode; + unsigned char *sha1; + + if (get_stat_data(new, &sha1, &mode, cached, match_missing) < 0) { + if (report_missing) + diff_index_show_file(revs, "-", old, + old->sha1, old->ce_mode); + return -1; + } + + if (revs->combine_merges && !cached && + (hashcmp(sha1, old->sha1) || hashcmp(old->sha1, new->sha1))) { + struct combine_diff_path *p; + int pathlen = ce_namelen(new); + + p = xmalloc(combine_diff_path_size(2, pathlen)); + p->path = (char *) &p->parent[2]; + p->next = NULL; + p->len = pathlen; + memcpy(p->path, new->name, pathlen); + p->path[pathlen] = 0; + p->mode = ntohl(mode); + hashclr(p->sha1); + memset(p->parent, 0, 2 * sizeof(struct combine_diff_parent)); + p->parent[0].status = DIFF_STATUS_MODIFIED; + p->parent[0].mode = ntohl(new->ce_mode); + hashcpy(p->parent[0].sha1, new->sha1); + p->parent[1].status = DIFF_STATUS_MODIFIED; + p->parent[1].mode = ntohl(old->ce_mode); + hashcpy(p->parent[1].sha1, old->sha1); + show_combined_diff(p, 2, revs->dense_combined_merges, revs); + free(p); + return 0; + } + + oldmode = old->ce_mode; + if (mode == oldmode && !hashcmp(sha1, old->sha1) && + !revs->diffopt.find_copies_harder) + return 0; + + mode = ntohl(mode); + oldmode = ntohl(oldmode); + + diff_change(&revs->diffopt, oldmode, mode, + old->sha1, sha1, old->name, NULL); + return 0; +} + +static int diff_cache(struct rev_info *revs, + struct cache_entry **ac, int entries, + const char **pathspec, + int cached, int match_missing) +{ + while (entries) { + struct cache_entry *ce = *ac; + int same = (entries > 1) && ce_same_name(ce, ac[1]); + + if (!ce_path_match(ce, pathspec)) + goto skip_entry; + + switch (ce_stage(ce)) { + case 0: + /* No stage 1 entry? That means it's a new file */ + if (!same) { + show_new_file(revs, ce, cached, match_missing); + break; + } + /* Show difference between old and new */ + show_modified(revs, ac[1], ce, 1, + cached, match_missing); + break; + case 1: + /* No stage 3 (merge) entry? + * That means it's been deleted. + */ + if (!same) { + diff_index_show_file(revs, "-", ce, + ce->sha1, ce->ce_mode); + break; + } + /* We come here with ce pointing at stage 1 + * (original tree) and ac[1] pointing at stage + * 3 (unmerged). show-modified with + * report-missing set to false does not say the + * file is deleted but reports true if work + * tree does not have it, in which case we + * fall through to report the unmerged state. + * Otherwise, we show the differences between + * the original tree and the work tree. + */ + if (!cached && + !show_modified(revs, ce, ac[1], 0, + cached, match_missing)) + break; + diff_unmerge(&revs->diffopt, ce->name, + ntohl(ce->ce_mode), ce->sha1); + break; + case 3: + diff_unmerge(&revs->diffopt, ce->name, + 0, null_sha1); + break; + + default: + die("impossible cache entry stage"); + } + +skip_entry: + /* + * Ignore all the different stages for this file, + * we've handled the relevant cases now. + */ + do { + ac++; + entries--; + } while (entries && ce_same_name(ce, ac[0])); + } + return 0; +} + +/* + * This turns all merge entries into "stage 3". That guarantees that + * when we read in the new tree (into "stage 1"), we won't lose sight + * of the fact that we had unmerged entries. + */ +static void mark_merge_entries(void) +{ + int i; + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (!ce_stage(ce)) + continue; + ce->ce_flags |= htons(CE_STAGEMASK); + } +} + +int run_diff_index(struct rev_info *revs, int cached) +{ + int ret; + struct object *ent; + struct tree *tree; + const char *tree_name; + int match_missing = 0; + + /* + * Backward compatibility wart - "diff-index -m" does + * not mean "do not ignore merges", but totally different. + */ + if (!revs->ignore_merges) + match_missing = 1; + + if (read_cache() < 0) { + perror("read_cache"); + return -1; + } + mark_merge_entries(); + + ent = revs->pending.objects[0].item; + tree_name = revs->pending.objects[0].name; + tree = parse_tree_indirect(ent->sha1); + if (!tree) + return error("bad tree object %s", tree_name); + if (read_tree(tree, 1, revs->prune_data)) + return error("unable to read tree object %s", tree_name); + ret = diff_cache(revs, active_cache, active_nr, revs->prune_data, + cached, match_missing); + diffcore_std(&revs->diffopt); + diff_flush(&revs->diffopt); + return ret; +} + +int do_diff_cache(const unsigned char *tree_sha1, struct diff_options *opt) +{ + struct tree *tree; + struct rev_info revs; + int i; + struct cache_entry **dst; + struct cache_entry *last = NULL; + + /* + * This is used by git-blame to run diff-cache internally; + * it potentially needs to repeatedly run this, so we will + * start by removing the higher order entries the last round + * left behind. + */ + dst = active_cache; + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (ce_stage(ce)) { + if (last && !strcmp(ce->name, last->name)) + continue; + cache_tree_invalidate_path(active_cache_tree, + ce->name); + last = ce; + ce->ce_mode = 0; + ce->ce_flags &= ~htons(CE_STAGEMASK); + } + *dst++ = ce; + } + active_nr = dst - active_cache; + + init_revisions(&revs, NULL); + revs.prune_data = opt->paths; + tree = parse_tree_indirect(tree_sha1); + if (!tree) + die("bad tree object %s", sha1_to_hex(tree_sha1)); + if (read_tree(tree, 1, opt->paths)) + return error("unable to read tree %s", sha1_to_hex(tree_sha1)); + return diff_cache(&revs, active_cache, active_nr, revs.prune_data, + 1, 0); +} diff --git a/diff.c b/diff.c new file mode 100644 index 0000000000..aaab3095a5 --- /dev/null +++ b/diff.c @@ -0,0 +1,2888 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "quote.h" +#include "diff.h" +#include "diffcore.h" +#include "delta.h" +#include "xdiff-interface.h" +#include "color.h" + +#ifdef NO_FAST_WORKING_DIRECTORY +#define FAST_WORKING_DIRECTORY 0 +#else +#define FAST_WORKING_DIRECTORY 1 +#endif + +static int use_size_cache; + +static int diff_detect_rename_default; +static int diff_rename_limit_default = -1; +static int diff_use_color_default; + +static char diff_colors[][COLOR_MAXLEN] = { + "\033[m", /* reset */ + "", /* PLAIN (normal) */ + "\033[1m", /* METAINFO (bold) */ + "\033[36m", /* FRAGINFO (cyan) */ + "\033[31m", /* OLD (red) */ + "\033[32m", /* NEW (green) */ + "\033[33m", /* COMMIT (yellow) */ + "\033[41m", /* WHITESPACE (red background) */ +}; + +static int parse_diff_color_slot(const char *var, int ofs) +{ + if (!strcasecmp(var+ofs, "plain")) + return DIFF_PLAIN; + if (!strcasecmp(var+ofs, "meta")) + return DIFF_METAINFO; + if (!strcasecmp(var+ofs, "frag")) + return DIFF_FRAGINFO; + if (!strcasecmp(var+ofs, "old")) + return DIFF_FILE_OLD; + if (!strcasecmp(var+ofs, "new")) + return DIFF_FILE_NEW; + if (!strcasecmp(var+ofs, "commit")) + return DIFF_COMMIT; + if (!strcasecmp(var+ofs, "whitespace")) + return DIFF_WHITESPACE; + die("bad config variable '%s'", var); +} + +/* + * These are to give UI layer defaults. + * The core-level commands such as git-diff-files should + * never be affected by the setting of diff.renames + * the user happens to have in the configuration file. + */ +int git_diff_ui_config(const char *var, const char *value) +{ + if (!strcmp(var, "diff.renamelimit")) { + diff_rename_limit_default = git_config_int(var, value); + return 0; + } + if (!strcmp(var, "diff.color") || !strcmp(var, "color.diff")) { + diff_use_color_default = git_config_colorbool(var, value); + return 0; + } + if (!strcmp(var, "diff.renames")) { + if (!value) + diff_detect_rename_default = DIFF_DETECT_RENAME; + else if (!strcasecmp(value, "copies") || + !strcasecmp(value, "copy")) + diff_detect_rename_default = DIFF_DETECT_COPY; + else if (git_config_bool(var,value)) + diff_detect_rename_default = DIFF_DETECT_RENAME; + return 0; + } + if (!strncmp(var, "diff.color.", 11) || !strncmp(var, "color.diff.", 11)) { + int slot = parse_diff_color_slot(var, 11); + color_parse(value, var, diff_colors[slot]); + return 0; + } + return git_default_config(var, value); +} + +static char *quote_one(const char *str) +{ + int needlen; + char *xp; + + if (!str) + return NULL; + needlen = quote_c_style(str, NULL, NULL, 0); + if (!needlen) + return xstrdup(str); + xp = xmalloc(needlen + 1); + quote_c_style(str, xp, NULL, 0); + return xp; +} + +static char *quote_two(const char *one, const char *two) +{ + int need_one = quote_c_style(one, NULL, NULL, 1); + int need_two = quote_c_style(two, NULL, NULL, 1); + char *xp; + + if (need_one + need_two) { + if (!need_one) need_one = strlen(one); + if (!need_two) need_one = strlen(two); + + xp = xmalloc(need_one + need_two + 3); + xp[0] = '"'; + quote_c_style(one, xp + 1, NULL, 1); + quote_c_style(two, xp + need_one + 1, NULL, 1); + strcpy(xp + need_one + need_two + 1, "\""); + return xp; + } + need_one = strlen(one); + need_two = strlen(two); + xp = xmalloc(need_one + need_two + 1); + strcpy(xp, one); + strcpy(xp + need_one, two); + return xp; +} + +static const char *external_diff(void) +{ + static const char *external_diff_cmd = NULL; + static int done_preparing = 0; + + if (done_preparing) + return external_diff_cmd; + external_diff_cmd = getenv("GIT_EXTERNAL_DIFF"); + done_preparing = 1; + return external_diff_cmd; +} + +#define TEMPFILE_PATH_LEN 50 + +static struct diff_tempfile { + const char *name; /* filename external diff should read from */ + char hex[41]; + char mode[10]; + char tmp_path[TEMPFILE_PATH_LEN]; +} diff_temp[2]; + +static int count_lines(const char *data, int size) +{ + int count, ch, completely_empty = 1, nl_just_seen = 0; + count = 0; + while (0 < size--) { + ch = *data++; + if (ch == '\n') { + count++; + nl_just_seen = 1; + completely_empty = 0; + } + else { + nl_just_seen = 0; + completely_empty = 0; + } + } + if (completely_empty) + return 0; + if (!nl_just_seen) + count++; /* no trailing newline */ + return count; +} + +static void print_line_count(int count) +{ + switch (count) { + case 0: + printf("0,0"); + break; + case 1: + printf("1"); + break; + default: + printf("1,%d", count); + break; + } +} + +static void copy_file(int prefix, const char *data, int size) +{ + int ch, nl_just_seen = 1; + while (0 < size--) { + ch = *data++; + if (nl_just_seen) + putchar(prefix); + putchar(ch); + if (ch == '\n') + nl_just_seen = 1; + else + nl_just_seen = 0; + } + if (!nl_just_seen) + printf("\n\\ No newline at end of file\n"); +} + +static void emit_rewrite_diff(const char *name_a, + const char *name_b, + struct diff_filespec *one, + struct diff_filespec *two) +{ + int lc_a, lc_b; + diff_populate_filespec(one, 0); + diff_populate_filespec(two, 0); + lc_a = count_lines(one->data, one->size); + lc_b = count_lines(two->data, two->size); + printf("--- a/%s\n+++ b/%s\n@@ -", name_a, name_b); + print_line_count(lc_a); + printf(" +"); + print_line_count(lc_b); + printf(" @@\n"); + if (lc_a) + copy_file('-', one->data, one->size); + if (lc_b) + copy_file('+', two->data, two->size); +} + +static int fill_mmfile(mmfile_t *mf, struct diff_filespec *one) +{ + if (!DIFF_FILE_VALID(one)) { + mf->ptr = (char *)""; /* does not matter */ + mf->size = 0; + return 0; + } + else if (diff_populate_filespec(one, 0)) + return -1; + mf->ptr = one->data; + mf->size = one->size; + return 0; +} + +struct diff_words_buffer { + mmfile_t text; + long alloc; + long current; /* output pointer */ + int suppressed_newline; +}; + +static void diff_words_append(char *line, unsigned long len, + struct diff_words_buffer *buffer) +{ + if (buffer->text.size + len > buffer->alloc) { + buffer->alloc = (buffer->text.size + len) * 3 / 2; + buffer->text.ptr = xrealloc(buffer->text.ptr, buffer->alloc); + } + line++; + len--; + memcpy(buffer->text.ptr + buffer->text.size, line, len); + buffer->text.size += len; +} + +struct diff_words_data { + struct xdiff_emit_state xm; + struct diff_words_buffer minus, plus; +}; + +static void print_word(struct diff_words_buffer *buffer, int len, int color, + int suppress_newline) +{ + const char *ptr; + int eol = 0; + + if (len == 0) + return; + + ptr = buffer->text.ptr + buffer->current; + buffer->current += len; + + if (ptr[len - 1] == '\n') { + eol = 1; + len--; + } + + fputs(diff_get_color(1, color), stdout); + fwrite(ptr, len, 1, stdout); + fputs(diff_get_color(1, DIFF_RESET), stdout); + + if (eol) { + if (suppress_newline) + buffer->suppressed_newline = 1; + else + putchar('\n'); + } +} + +static void fn_out_diff_words_aux(void *priv, char *line, unsigned long len) +{ + struct diff_words_data *diff_words = priv; + + if (diff_words->minus.suppressed_newline) { + if (line[0] != '+') + putchar('\n'); + diff_words->minus.suppressed_newline = 0; + } + + len--; + switch (line[0]) { + case '-': + print_word(&diff_words->minus, len, DIFF_FILE_OLD, 1); + break; + case '+': + print_word(&diff_words->plus, len, DIFF_FILE_NEW, 0); + break; + case ' ': + print_word(&diff_words->plus, len, DIFF_PLAIN, 0); + diff_words->minus.current += len; + break; + } +} + +/* this executes the word diff on the accumulated buffers */ +static void diff_words_show(struct diff_words_data *diff_words) +{ + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + mmfile_t minus, plus; + int i; + + minus.size = diff_words->minus.text.size; + minus.ptr = xmalloc(minus.size); + memcpy(minus.ptr, diff_words->minus.text.ptr, minus.size); + for (i = 0; i < minus.size; i++) + if (isspace(minus.ptr[i])) + minus.ptr[i] = '\n'; + diff_words->minus.current = 0; + + plus.size = diff_words->plus.text.size; + plus.ptr = xmalloc(plus.size); + memcpy(plus.ptr, diff_words->plus.text.ptr, plus.size); + for (i = 0; i < plus.size; i++) + if (isspace(plus.ptr[i])) + plus.ptr[i] = '\n'; + diff_words->plus.current = 0; + + xpp.flags = XDF_NEED_MINIMAL; + xecfg.ctxlen = diff_words->minus.alloc + diff_words->plus.alloc; + xecfg.flags = 0; + ecb.outf = xdiff_outf; + ecb.priv = diff_words; + diff_words->xm.consume = fn_out_diff_words_aux; + xdl_diff(&minus, &plus, &xpp, &xecfg, &ecb); + + free(minus.ptr); + free(plus.ptr); + diff_words->minus.text.size = diff_words->plus.text.size = 0; + + if (diff_words->minus.suppressed_newline) { + putchar('\n'); + diff_words->minus.suppressed_newline = 0; + } +} + +struct emit_callback { + struct xdiff_emit_state xm; + int nparents, color_diff; + const char **label_path; + struct diff_words_data *diff_words; +}; + +static void free_diff_words_data(struct emit_callback *ecbdata) +{ + if (ecbdata->diff_words) { + /* flush buffers */ + if (ecbdata->diff_words->minus.text.size || + ecbdata->diff_words->plus.text.size) + diff_words_show(ecbdata->diff_words); + + if (ecbdata->diff_words->minus.text.ptr) + free (ecbdata->diff_words->minus.text.ptr); + if (ecbdata->diff_words->plus.text.ptr) + free (ecbdata->diff_words->plus.text.ptr); + free(ecbdata->diff_words); + ecbdata->diff_words = NULL; + } +} + +const char *diff_get_color(int diff_use_color, enum color_diff ix) +{ + if (diff_use_color) + return diff_colors[ix]; + return ""; +} + +static void emit_line(const char *set, const char *reset, const char *line, int len) +{ + if (len > 0 && line[len-1] == '\n') + len--; + fputs(set, stdout); + fwrite(line, len, 1, stdout); + puts(reset); +} + +static void emit_add_line(const char *reset, struct emit_callback *ecbdata, const char *line, int len) +{ + int col0 = ecbdata->nparents; + int last_tab_in_indent = -1; + int last_space_in_indent = -1; + int i; + int tail = len; + int need_highlight_leading_space = 0; + const char *ws = diff_get_color(ecbdata->color_diff, DIFF_WHITESPACE); + const char *set = diff_get_color(ecbdata->color_diff, DIFF_FILE_NEW); + + if (!*ws) { + emit_line(set, reset, line, len); + return; + } + + /* The line is a newly added line. Does it have funny leading + * whitespaces? In indent, SP should never precede a TAB. + */ + for (i = col0; i < len; i++) { + if (line[i] == '\t') { + last_tab_in_indent = i; + if (0 <= last_space_in_indent) + need_highlight_leading_space = 1; + } + else if (line[i] == ' ') + last_space_in_indent = i; + else + break; + } + fputs(set, stdout); + fwrite(line, col0, 1, stdout); + fputs(reset, stdout); + if (((i == len) || line[i] == '\n') && i != col0) { + /* The whole line was indent */ + emit_line(ws, reset, line + col0, len - col0); + return; + } + i = col0; + if (need_highlight_leading_space) { + while (i < last_tab_in_indent) { + if (line[i] == ' ') { + fputs(ws, stdout); + putchar(' '); + fputs(reset, stdout); + } + else + putchar(line[i]); + i++; + } + } + tail = len - 1; + if (line[tail] == '\n' && i < tail) + tail--; + while (i < tail) { + if (!isspace(line[tail])) + break; + tail--; + } + if ((i < tail && line[tail + 1] != '\n')) { + /* This has whitespace between tail+1..len */ + fputs(set, stdout); + fwrite(line + i, tail - i + 1, 1, stdout); + fputs(reset, stdout); + emit_line(ws, reset, line + tail + 1, len - tail - 1); + } + else + emit_line(set, reset, line + i, len - i); +} + +static void fn_out_consume(void *priv, char *line, unsigned long len) +{ + int i; + int color; + struct emit_callback *ecbdata = priv; + const char *set = diff_get_color(ecbdata->color_diff, DIFF_METAINFO); + const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET); + + if (ecbdata->label_path[0]) { + printf("%s--- %s%s\n", set, ecbdata->label_path[0], reset); + printf("%s+++ %s%s\n", set, ecbdata->label_path[1], reset); + ecbdata->label_path[0] = ecbdata->label_path[1] = NULL; + } + + /* This is not really necessary for now because + * this codepath only deals with two-way diffs. + */ + for (i = 0; i < len && line[i] == '@'; i++) + ; + if (2 <= i && i < len && line[i] == ' ') { + ecbdata->nparents = i - 1; + emit_line(diff_get_color(ecbdata->color_diff, DIFF_FRAGINFO), + reset, line, len); + return; + } + + if (len < ecbdata->nparents) { + set = reset; + emit_line(reset, reset, line, len); + return; + } + + color = DIFF_PLAIN; + if (ecbdata->diff_words && ecbdata->nparents != 1) + /* fall back to normal diff */ + free_diff_words_data(ecbdata); + if (ecbdata->diff_words) { + if (line[0] == '-') { + diff_words_append(line, len, + &ecbdata->diff_words->minus); + return; + } else if (line[0] == '+') { + diff_words_append(line, len, + &ecbdata->diff_words->plus); + return; + } + if (ecbdata->diff_words->minus.text.size || + ecbdata->diff_words->plus.text.size) + diff_words_show(ecbdata->diff_words); + line++; + len--; + emit_line(set, reset, line, len); + return; + } + for (i = 0; i < ecbdata->nparents && len; i++) { + if (line[i] == '-') + color = DIFF_FILE_OLD; + else if (line[i] == '+') + color = DIFF_FILE_NEW; + } + + if (color != DIFF_FILE_NEW) { + emit_line(diff_get_color(ecbdata->color_diff, color), + reset, line, len); + return; + } + emit_add_line(reset, ecbdata, line, len); +} + +static char *pprint_rename(const char *a, const char *b) +{ + const char *old = a; + const char *new = b; + char *name = NULL; + int pfx_length, sfx_length; + int len_a = strlen(a); + int len_b = strlen(b); + int qlen_a = quote_c_style(a, NULL, NULL, 0); + int qlen_b = quote_c_style(b, NULL, NULL, 0); + + if (qlen_a || qlen_b) { + if (qlen_a) len_a = qlen_a; + if (qlen_b) len_b = qlen_b; + name = xmalloc( len_a + len_b + 5 ); + if (qlen_a) + quote_c_style(a, name, NULL, 0); + else + memcpy(name, a, len_a); + memcpy(name + len_a, " => ", 4); + if (qlen_b) + quote_c_style(b, name + len_a + 4, NULL, 0); + else + memcpy(name + len_a + 4, b, len_b + 1); + return name; + } + + /* Find common prefix */ + pfx_length = 0; + while (*old && *new && *old == *new) { + if (*old == '/') + pfx_length = old - a + 1; + old++; + new++; + } + + /* Find common suffix */ + old = a + len_a; + new = b + len_b; + sfx_length = 0; + while (a <= old && b <= new && *old == *new) { + if (*old == '/') + sfx_length = len_a - (old - a); + old--; + new--; + } + + /* + * pfx{mid-a => mid-b}sfx + * {pfx-a => pfx-b}sfx + * pfx{sfx-a => sfx-b} + * name-a => name-b + */ + if (pfx_length + sfx_length) { + int a_midlen = len_a - pfx_length - sfx_length; + int b_midlen = len_b - pfx_length - sfx_length; + if (a_midlen < 0) a_midlen = 0; + if (b_midlen < 0) b_midlen = 0; + + name = xmalloc(pfx_length + a_midlen + b_midlen + sfx_length + 7); + sprintf(name, "%.*s{%.*s => %.*s}%s", + pfx_length, a, + a_midlen, a + pfx_length, + b_midlen, b + pfx_length, + a + len_a - sfx_length); + } + else { + name = xmalloc(len_a + len_b + 5); + sprintf(name, "%s => %s", a, b); + } + return name; +} + +struct diffstat_t { + struct xdiff_emit_state xm; + + int nr; + int alloc; + struct diffstat_file { + char *name; + unsigned is_unmerged:1; + unsigned is_binary:1; + unsigned is_renamed:1; + unsigned int added, deleted; + } **files; +}; + +static struct diffstat_file *diffstat_add(struct diffstat_t *diffstat, + const char *name_a, + const char *name_b) +{ + struct diffstat_file *x; + x = xcalloc(sizeof (*x), 1); + if (diffstat->nr == diffstat->alloc) { + diffstat->alloc = alloc_nr(diffstat->alloc); + diffstat->files = xrealloc(diffstat->files, + diffstat->alloc * sizeof(x)); + } + diffstat->files[diffstat->nr++] = x; + if (name_b) { + x->name = pprint_rename(name_a, name_b); + x->is_renamed = 1; + } + else + x->name = xstrdup(name_a); + return x; +} + +static void diffstat_consume(void *priv, char *line, unsigned long len) +{ + struct diffstat_t *diffstat = priv; + struct diffstat_file *x = diffstat->files[diffstat->nr - 1]; + + if (line[0] == '+') + x->added++; + else if (line[0] == '-') + x->deleted++; +} + +const char mime_boundary_leader[] = "------------"; + +static int scale_linear(int it, int width, int max_change) +{ + /* + * make sure that at least one '-' is printed if there were deletions, + * and likewise for '+'. + */ + if (max_change < 2) + return it; + return ((it - 1) * (width - 1) + max_change - 1) / (max_change - 1); +} + +static void show_name(const char *prefix, const char *name, int len, + const char *reset, const char *set) +{ + printf(" %s%s%-*s%s |", set, prefix, len, name, reset); +} + +static void show_graph(char ch, int cnt, const char *set, const char *reset) +{ + if (cnt <= 0) + return; + printf("%s", set); + while (cnt--) + putchar(ch); + printf("%s", reset); +} + +static void show_stats(struct diffstat_t* data, struct diff_options *options) +{ + int i, len, add, del, total, adds = 0, dels = 0; + int max_change = 0, max_len = 0; + int total_files = data->nr; + int width, name_width; + const char *reset, *set, *add_c, *del_c; + + if (data->nr == 0) + return; + + width = options->stat_width ? options->stat_width : 80; + name_width = options->stat_name_width ? options->stat_name_width : 50; + + /* Sanity: give at least 5 columns to the graph, + * but leave at least 10 columns for the name. + */ + if (width < name_width + 15) { + if (name_width <= 25) + width = name_width + 15; + else + name_width = width - 15; + } + + /* Find the longest filename and max number of changes */ + reset = diff_get_color(options->color_diff, DIFF_RESET); + set = diff_get_color(options->color_diff, DIFF_PLAIN); + add_c = diff_get_color(options->color_diff, DIFF_FILE_NEW); + del_c = diff_get_color(options->color_diff, DIFF_FILE_OLD); + + for (i = 0; i < data->nr; i++) { + struct diffstat_file *file = data->files[i]; + int change = file->added + file->deleted; + + if (!file->is_renamed) { /* renames are already quoted by pprint_rename */ + len = quote_c_style(file->name, NULL, NULL, 0); + if (len) { + char *qname = xmalloc(len + 1); + quote_c_style(file->name, qname, NULL, 0); + free(file->name); + file->name = qname; + } + } + + len = strlen(file->name); + if (max_len < len) + max_len = len; + + if (file->is_binary || file->is_unmerged) + continue; + if (max_change < change) + max_change = change; + } + + /* Compute the width of the graph part; + * 10 is for one blank at the beginning of the line plus + * " | count " between the name and the graph. + * + * From here on, name_width is the width of the name area, + * and width is the width of the graph area. + */ + name_width = (name_width < max_len) ? name_width : max_len; + if (width < (name_width + 10) + max_change) + width = width - (name_width + 10); + else + width = max_change; + + for (i = 0; i < data->nr; i++) { + const char *prefix = ""; + char *name = data->files[i]->name; + int added = data->files[i]->added; + int deleted = data->files[i]->deleted; + int name_len; + + /* + * "scale" the filename + */ + len = name_width; + name_len = strlen(name); + if (name_width < name_len) { + char *slash; + prefix = "..."; + len -= 3; + name += name_len - len; + slash = strchr(name, '/'); + if (slash) + name = slash; + } + + if (data->files[i]->is_binary) { + show_name(prefix, name, len, reset, set); + printf(" Bin\n"); + goto free_diffstat_file; + } + else if (data->files[i]->is_unmerged) { + show_name(prefix, name, len, reset, set); + printf(" Unmerged\n"); + goto free_diffstat_file; + } + else if (!data->files[i]->is_renamed && + (added + deleted == 0)) { + total_files--; + goto free_diffstat_file; + } + + /* + * scale the add/delete + */ + add = added; + del = deleted; + total = add + del; + adds += add; + dels += del; + + if (width <= max_change) { + add = scale_linear(add, width, max_change); + del = scale_linear(del, width, max_change); + total = add + del; + } + show_name(prefix, name, len, reset, set); + printf("%5d ", added + deleted); + show_graph('+', add, add_c, reset); + show_graph('-', del, del_c, reset); + putchar('\n'); + free_diffstat_file: + free(data->files[i]->name); + free(data->files[i]); + } + free(data->files); + printf("%s %d files changed, %d insertions(+), %d deletions(-)%s\n", + set, total_files, adds, dels, reset); +} + +static void show_shortstats(struct diffstat_t* data) +{ + int i, adds = 0, dels = 0, total_files = data->nr; + + if (data->nr == 0) + return; + + for (i = 0; i < data->nr; i++) { + if (!data->files[i]->is_binary && + !data->files[i]->is_unmerged) { + int added = data->files[i]->added; + int deleted= data->files[i]->deleted; + if (!data->files[i]->is_renamed && + (added + deleted == 0)) { + total_files--; + } else { + adds += added; + dels += deleted; + } + } + free(data->files[i]->name); + free(data->files[i]); + } + free(data->files); + + printf(" %d files changed, %d insertions(+), %d deletions(-)\n", + total_files, adds, dels); +} + +static void show_numstat(struct diffstat_t* data, struct diff_options *options) +{ + int i; + + for (i = 0; i < data->nr; i++) { + struct diffstat_file *file = data->files[i]; + + if (file->is_binary) + printf("-\t-\t"); + else + printf("%d\t%d\t", file->added, file->deleted); + if (options->line_termination && !file->is_renamed && + quote_c_style(file->name, NULL, NULL, 0)) + quote_c_style(file->name, NULL, stdout, 0); + else + fputs(file->name, stdout); + putchar(options->line_termination); + } +} + +struct checkdiff_t { + struct xdiff_emit_state xm; + const char *filename; + int lineno; +}; + +static void checkdiff_consume(void *priv, char *line, unsigned long len) +{ + struct checkdiff_t *data = priv; + + if (line[0] == '+') { + int i, spaces = 0; + + /* check space before tab */ + for (i = 1; i < len && (line[i] == ' ' || line[i] == '\t'); i++) + if (line[i] == ' ') + spaces++; + if (line[i - 1] == '\t' && spaces) + printf("%s:%d: space before tab:%.*s\n", + data->filename, data->lineno, (int)len, line); + + /* check white space at line end */ + if (line[len - 1] == '\n') + len--; + if (isspace(line[len - 1])) + printf("%s:%d: white space at end: %.*s\n", + data->filename, data->lineno, (int)len, line); + + data->lineno++; + } else if (line[0] == ' ') + data->lineno++; + else if (line[0] == '@') { + char *plus = strchr(line, '+'); + if (plus) + data->lineno = strtol(plus, NULL, 10); + else + die("invalid diff"); + } +} + +static unsigned char *deflate_it(char *data, + unsigned long size, + unsigned long *result_size) +{ + int bound; + unsigned char *deflated; + z_stream stream; + + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, zlib_compression_level); + bound = deflateBound(&stream, size); + deflated = xmalloc(bound); + stream.next_out = deflated; + stream.avail_out = bound; + + stream.next_in = (unsigned char *)data; + stream.avail_in = size; + while (deflate(&stream, Z_FINISH) == Z_OK) + ; /* nothing */ + deflateEnd(&stream); + *result_size = stream.total_out; + return deflated; +} + +static void emit_binary_diff_body(mmfile_t *one, mmfile_t *two) +{ + void *cp; + void *delta; + void *deflated; + void *data; + unsigned long orig_size; + unsigned long delta_size; + unsigned long deflate_size; + unsigned long data_size; + + /* We could do deflated delta, or we could do just deflated two, + * whichever is smaller. + */ + delta = NULL; + deflated = deflate_it(two->ptr, two->size, &deflate_size); + if (one->size && two->size) { + delta = diff_delta(one->ptr, one->size, + two->ptr, two->size, + &delta_size, deflate_size); + if (delta) { + void *to_free = delta; + orig_size = delta_size; + delta = deflate_it(delta, delta_size, &delta_size); + free(to_free); + } + } + + if (delta && delta_size < deflate_size) { + printf("delta %lu\n", orig_size); + free(deflated); + data = delta; + data_size = delta_size; + } + else { + printf("literal %lu\n", two->size); + free(delta); + data = deflated; + data_size = deflate_size; + } + + /* emit data encoded in base85 */ + cp = data; + while (data_size) { + int bytes = (52 < data_size) ? 52 : data_size; + char line[70]; + data_size -= bytes; + if (bytes <= 26) + line[0] = bytes + 'A' - 1; + else + line[0] = bytes - 26 + 'a' - 1; + encode_85(line + 1, cp, bytes); + cp = (char *) cp + bytes; + puts(line); + } + printf("\n"); + free(data); +} + +static void emit_binary_diff(mmfile_t *one, mmfile_t *two) +{ + printf("GIT binary patch\n"); + emit_binary_diff_body(one, two); + emit_binary_diff_body(two, one); +} + +#define FIRST_FEW_BYTES 8000 +static int mmfile_is_binary(mmfile_t *mf) +{ + long sz = mf->size; + if (FIRST_FEW_BYTES < sz) + sz = FIRST_FEW_BYTES; + return !!memchr(mf->ptr, 0, sz); +} + +static void builtin_diff(const char *name_a, + const char *name_b, + struct diff_filespec *one, + struct diff_filespec *two, + const char *xfrm_msg, + struct diff_options *o, + int complete_rewrite) +{ + mmfile_t mf1, mf2; + const char *lbl[2]; + char *a_one, *b_two; + const char *set = diff_get_color(o->color_diff, DIFF_METAINFO); + const char *reset = diff_get_color(o->color_diff, DIFF_RESET); + + a_one = quote_two("a/", name_a); + b_two = quote_two("b/", name_b); + lbl[0] = DIFF_FILE_VALID(one) ? a_one : "/dev/null"; + lbl[1] = DIFF_FILE_VALID(two) ? b_two : "/dev/null"; + printf("%sdiff --git %s %s%s\n", set, a_one, b_two, reset); + if (lbl[0][0] == '/') { + /* /dev/null */ + printf("%snew file mode %06o%s\n", set, two->mode, reset); + if (xfrm_msg && xfrm_msg[0]) + printf("%s%s%s\n", set, xfrm_msg, reset); + } + else if (lbl[1][0] == '/') { + printf("%sdeleted file mode %06o%s\n", set, one->mode, reset); + if (xfrm_msg && xfrm_msg[0]) + printf("%s%s%s\n", set, xfrm_msg, reset); + } + else { + if (one->mode != two->mode) { + printf("%sold mode %06o%s\n", set, one->mode, reset); + printf("%snew mode %06o%s\n", set, two->mode, reset); + } + if (xfrm_msg && xfrm_msg[0]) + printf("%s%s%s\n", set, xfrm_msg, reset); + /* + * we do not run diff between different kind + * of objects. + */ + if ((one->mode ^ two->mode) & S_IFMT) + goto free_ab_and_return; + if (complete_rewrite) { + emit_rewrite_diff(name_a, name_b, one, two); + goto free_ab_and_return; + } + } + + if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0) + die("unable to read files to diff"); + + if (!o->text && (mmfile_is_binary(&mf1) || mmfile_is_binary(&mf2))) { + /* Quite common confusing case */ + if (mf1.size == mf2.size && + !memcmp(mf1.ptr, mf2.ptr, mf1.size)) + goto free_ab_and_return; + if (o->binary) + emit_binary_diff(&mf1, &mf2); + else + printf("Binary files %s and %s differ\n", + lbl[0], lbl[1]); + } + else { + /* Crazy xdl interfaces.. */ + const char *diffopts = getenv("GIT_DIFF_OPTS"); + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + struct emit_callback ecbdata; + + memset(&ecbdata, 0, sizeof(ecbdata)); + ecbdata.label_path = lbl; + ecbdata.color_diff = o->color_diff; + xpp.flags = XDF_NEED_MINIMAL | o->xdl_opts; + xecfg.ctxlen = o->context; + xecfg.flags = XDL_EMIT_FUNCNAMES; + if (!diffopts) + ; + else if (!strncmp(diffopts, "--unified=", 10)) + xecfg.ctxlen = strtoul(diffopts + 10, NULL, 10); + else if (!strncmp(diffopts, "-u", 2)) + xecfg.ctxlen = strtoul(diffopts + 2, NULL, 10); + ecb.outf = xdiff_outf; + ecb.priv = &ecbdata; + ecbdata.xm.consume = fn_out_consume; + if (o->color_diff_words) + ecbdata.diff_words = + xcalloc(1, sizeof(struct diff_words_data)); + xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb); + if (o->color_diff_words) + free_diff_words_data(&ecbdata); + } + + free_ab_and_return: + free(a_one); + free(b_two); + return; +} + +static void builtin_diffstat(const char *name_a, const char *name_b, + struct diff_filespec *one, + struct diff_filespec *two, + struct diffstat_t *diffstat, + struct diff_options *o, + int complete_rewrite) +{ + mmfile_t mf1, mf2; + struct diffstat_file *data; + + data = diffstat_add(diffstat, name_a, name_b); + + if (!one || !two) { + data->is_unmerged = 1; + return; + } + if (complete_rewrite) { + diff_populate_filespec(one, 0); + diff_populate_filespec(two, 0); + data->deleted = count_lines(one->data, one->size); + data->added = count_lines(two->data, two->size); + return; + } + if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0) + die("unable to read files to diff"); + + if (mmfile_is_binary(&mf1) || mmfile_is_binary(&mf2)) + data->is_binary = 1; + else { + /* Crazy xdl interfaces.. */ + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + + xpp.flags = XDF_NEED_MINIMAL | o->xdl_opts; + xecfg.ctxlen = 0; + xecfg.flags = 0; + ecb.outf = xdiff_outf; + ecb.priv = diffstat; + xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb); + } +} + +static void builtin_checkdiff(const char *name_a, const char *name_b, + struct diff_filespec *one, + struct diff_filespec *two) +{ + mmfile_t mf1, mf2; + struct checkdiff_t data; + + if (!two) + return; + + memset(&data, 0, sizeof(data)); + data.xm.consume = checkdiff_consume; + data.filename = name_b ? name_b : name_a; + data.lineno = 0; + + if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0) + die("unable to read files to diff"); + + if (mmfile_is_binary(&mf2)) + return; + else { + /* Crazy xdl interfaces.. */ + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + + xpp.flags = XDF_NEED_MINIMAL; + xecfg.ctxlen = 0; + xecfg.flags = 0; + ecb.outf = xdiff_outf; + ecb.priv = &data; + xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb); + } +} + +struct diff_filespec *alloc_filespec(const char *path) +{ + int namelen = strlen(path); + struct diff_filespec *spec = xmalloc(sizeof(*spec) + namelen + 1); + + memset(spec, 0, sizeof(*spec)); + spec->path = (char *)(spec + 1); + memcpy(spec->path, path, namelen+1); + return spec; +} + +void fill_filespec(struct diff_filespec *spec, const unsigned char *sha1, + unsigned short mode) +{ + if (mode) { + spec->mode = canon_mode(mode); + hashcpy(spec->sha1, sha1); + spec->sha1_valid = !is_null_sha1(sha1); + } +} + +/* + * Given a name and sha1 pair, if the dircache tells us the file in + * the work tree has that object contents, return true, so that + * prepare_temp_file() does not have to inflate and extract. + */ +static int reuse_worktree_file(const char *name, const unsigned char *sha1, int want_file) +{ + struct cache_entry *ce; + struct stat st; + int pos, len; + + /* We do not read the cache ourselves here, because the + * benchmark with my previous version that always reads cache + * shows that it makes things worse for diff-tree comparing + * two linux-2.6 kernel trees in an already checked out work + * tree. This is because most diff-tree comparisons deal with + * only a small number of files, while reading the cache is + * expensive for a large project, and its cost outweighs the + * savings we get by not inflating the object to a temporary + * file. Practically, this code only helps when we are used + * by diff-cache --cached, which does read the cache before + * calling us. + */ + if (!active_cache) + return 0; + + /* We want to avoid the working directory if our caller + * doesn't need the data in a normal file, this system + * is rather slow with its stat/open/mmap/close syscalls, + * and the object is contained in a pack file. The pack + * is probably already open and will be faster to obtain + * the data through than the working directory. Loose + * objects however would tend to be slower as they need + * to be individually opened and inflated. + */ + if (!FAST_WORKING_DIRECTORY && !want_file && has_sha1_pack(sha1, NULL)) + return 0; + + len = strlen(name); + pos = cache_name_pos(name, len); + if (pos < 0) + return 0; + ce = active_cache[pos]; + if ((lstat(name, &st) < 0) || + !S_ISREG(st.st_mode) || /* careful! */ + ce_match_stat(ce, &st, 0) || + hashcmp(sha1, ce->sha1)) + return 0; + /* we return 1 only when we can stat, it is a regular file, + * stat information matches, and sha1 recorded in the cache + * matches. I.e. we know the file in the work tree really is + * the same as the <name, sha1> pair. + */ + return 1; +} + +static struct sha1_size_cache { + unsigned char sha1[20]; + unsigned long size; +} **sha1_size_cache; +static int sha1_size_cache_nr, sha1_size_cache_alloc; + +static struct sha1_size_cache *locate_size_cache(unsigned char *sha1, + int find_only, + unsigned long size) +{ + int first, last; + struct sha1_size_cache *e; + + first = 0; + last = sha1_size_cache_nr; + while (last > first) { + int cmp, next = (last + first) >> 1; + e = sha1_size_cache[next]; + cmp = hashcmp(e->sha1, sha1); + if (!cmp) + return e; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + /* not found */ + if (find_only) + return NULL; + /* insert to make it at "first" */ + if (sha1_size_cache_alloc <= sha1_size_cache_nr) { + sha1_size_cache_alloc = alloc_nr(sha1_size_cache_alloc); + sha1_size_cache = xrealloc(sha1_size_cache, + sha1_size_cache_alloc * + sizeof(*sha1_size_cache)); + } + sha1_size_cache_nr++; + if (first < sha1_size_cache_nr) + memmove(sha1_size_cache + first + 1, sha1_size_cache + first, + (sha1_size_cache_nr - first - 1) * + sizeof(*sha1_size_cache)); + e = xmalloc(sizeof(struct sha1_size_cache)); + sha1_size_cache[first] = e; + hashcpy(e->sha1, sha1); + e->size = size; + return e; +} + +/* + * While doing rename detection and pickaxe operation, we may need to + * grab the data for the blob (or file) for our own in-core comparison. + * diff_filespec has data and size fields for this purpose. + */ +int diff_populate_filespec(struct diff_filespec *s, int size_only) +{ + int err = 0; + if (!DIFF_FILE_VALID(s)) + die("internal error: asking to populate invalid file."); + if (S_ISDIR(s->mode)) + return -1; + + if (!use_size_cache) + size_only = 0; + + if (s->data) + return err; + if (!s->sha1_valid || + reuse_worktree_file(s->path, s->sha1, 0)) { + struct stat st; + int fd; + if (lstat(s->path, &st) < 0) { + if (errno == ENOENT) { + err_empty: + err = -1; + empty: + s->data = (char *)""; + s->size = 0; + return err; + } + } + s->size = st.st_size; + if (!s->size) + goto empty; + if (size_only) + return 0; + if (S_ISLNK(st.st_mode)) { + int ret; + s->data = xmalloc(s->size); + s->should_free = 1; + ret = readlink(s->path, s->data, s->size); + if (ret < 0) { + free(s->data); + goto err_empty; + } + return 0; + } + fd = open(s->path, O_RDONLY); + if (fd < 0) + goto err_empty; + s->data = xmmap(NULL, s->size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + s->should_munmap = 1; + } + else { + char type[20]; + struct sha1_size_cache *e; + + if (size_only) { + e = locate_size_cache(s->sha1, 1, 0); + if (e) { + s->size = e->size; + return 0; + } + if (!sha1_object_info(s->sha1, type, &s->size)) + locate_size_cache(s->sha1, 0, s->size); + } + else { + s->data = read_sha1_file(s->sha1, type, &s->size); + s->should_free = 1; + } + } + return 0; +} + +void diff_free_filespec_data(struct diff_filespec *s) +{ + if (s->should_free) + free(s->data); + else if (s->should_munmap) + munmap(s->data, s->size); + s->should_free = s->should_munmap = 0; + s->data = NULL; + free(s->cnt_data); + s->cnt_data = NULL; +} + +static void prep_temp_blob(struct diff_tempfile *temp, + void *blob, + unsigned long size, + const unsigned char *sha1, + int mode) +{ + int fd; + + fd = git_mkstemp(temp->tmp_path, TEMPFILE_PATH_LEN, ".diff_XXXXXX"); + if (fd < 0) + die("unable to create temp-file"); + if (write_in_full(fd, blob, size) != size) + die("unable to write temp-file"); + close(fd); + temp->name = temp->tmp_path; + strcpy(temp->hex, sha1_to_hex(sha1)); + temp->hex[40] = 0; + sprintf(temp->mode, "%06o", mode); +} + +static void prepare_temp_file(const char *name, + struct diff_tempfile *temp, + struct diff_filespec *one) +{ + if (!DIFF_FILE_VALID(one)) { + not_a_valid_file: + /* A '-' entry produces this for file-2, and + * a '+' entry produces this for file-1. + */ + temp->name = "/dev/null"; + strcpy(temp->hex, "."); + strcpy(temp->mode, "."); + return; + } + + if (!one->sha1_valid || + reuse_worktree_file(name, one->sha1, 1)) { + struct stat st; + if (lstat(name, &st) < 0) { + if (errno == ENOENT) + goto not_a_valid_file; + die("stat(%s): %s", name, strerror(errno)); + } + if (S_ISLNK(st.st_mode)) { + int ret; + char buf[PATH_MAX + 1]; /* ought to be SYMLINK_MAX */ + if (sizeof(buf) <= st.st_size) + die("symlink too long: %s", name); + ret = readlink(name, buf, st.st_size); + if (ret < 0) + die("readlink(%s)", name); + prep_temp_blob(temp, buf, st.st_size, + (one->sha1_valid ? + one->sha1 : null_sha1), + (one->sha1_valid ? + one->mode : S_IFLNK)); + } + else { + /* we can borrow from the file in the work tree */ + temp->name = name; + if (!one->sha1_valid) + strcpy(temp->hex, sha1_to_hex(null_sha1)); + else + strcpy(temp->hex, sha1_to_hex(one->sha1)); + /* Even though we may sometimes borrow the + * contents from the work tree, we always want + * one->mode. mode is trustworthy even when + * !(one->sha1_valid), as long as + * DIFF_FILE_VALID(one). + */ + sprintf(temp->mode, "%06o", one->mode); + } + return; + } + else { + if (diff_populate_filespec(one, 0)) + die("cannot read data blob for %s", one->path); + prep_temp_blob(temp, one->data, one->size, + one->sha1, one->mode); + } +} + +static void remove_tempfile(void) +{ + int i; + + for (i = 0; i < 2; i++) + if (diff_temp[i].name == diff_temp[i].tmp_path) { + unlink(diff_temp[i].name); + diff_temp[i].name = NULL; + } +} + +static void remove_tempfile_on_signal(int signo) +{ + remove_tempfile(); + signal(SIGINT, SIG_DFL); + raise(signo); +} + +static int spawn_prog(const char *pgm, const char **arg) +{ + pid_t pid; + int status; + + fflush(NULL); + pid = fork(); + if (pid < 0) + die("unable to fork"); + if (!pid) { + execvp(pgm, (char *const*) arg); + exit(255); + } + + while (waitpid(pid, &status, 0) < 0) { + if (errno == EINTR) + continue; + return -1; + } + + /* Earlier we did not check the exit status because + * diff exits non-zero if files are different, and + * we are not interested in knowing that. It was a + * mistake which made it harder to quit a diff-* + * session that uses the git-apply-patch-script as + * the GIT_EXTERNAL_DIFF. A custom GIT_EXTERNAL_DIFF + * should also exit non-zero only when it wants to + * abort the entire diff-* session. + */ + if (WIFEXITED(status) && !WEXITSTATUS(status)) + return 0; + return -1; +} + +/* An external diff command takes: + * + * diff-cmd name infile1 infile1-sha1 infile1-mode \ + * infile2 infile2-sha1 infile2-mode [ rename-to ] + * + */ +static void run_external_diff(const char *pgm, + const char *name, + const char *other, + struct diff_filespec *one, + struct diff_filespec *two, + const char *xfrm_msg, + int complete_rewrite) +{ + const char *spawn_arg[10]; + struct diff_tempfile *temp = diff_temp; + int retval; + static int atexit_asked = 0; + const char *othername; + const char **arg = &spawn_arg[0]; + + othername = (other? other : name); + if (one && two) { + prepare_temp_file(name, &temp[0], one); + prepare_temp_file(othername, &temp[1], two); + if (! atexit_asked && + (temp[0].name == temp[0].tmp_path || + temp[1].name == temp[1].tmp_path)) { + atexit_asked = 1; + atexit(remove_tempfile); + } + signal(SIGINT, remove_tempfile_on_signal); + } + + if (one && two) { + *arg++ = pgm; + *arg++ = name; + *arg++ = temp[0].name; + *arg++ = temp[0].hex; + *arg++ = temp[0].mode; + *arg++ = temp[1].name; + *arg++ = temp[1].hex; + *arg++ = temp[1].mode; + if (other) { + *arg++ = other; + *arg++ = xfrm_msg; + } + } else { + *arg++ = pgm; + *arg++ = name; + } + *arg = NULL; + retval = spawn_prog(pgm, spawn_arg); + remove_tempfile(); + if (retval) { + fprintf(stderr, "external diff died, stopping at %s.\n", name); + exit(1); + } +} + +static void run_diff_cmd(const char *pgm, + const char *name, + const char *other, + struct diff_filespec *one, + struct diff_filespec *two, + const char *xfrm_msg, + struct diff_options *o, + int complete_rewrite) +{ + if (pgm) { + run_external_diff(pgm, name, other, one, two, xfrm_msg, + complete_rewrite); + return; + } + if (one && two) + builtin_diff(name, other ? other : name, + one, two, xfrm_msg, o, complete_rewrite); + else + printf("* Unmerged path %s\n", name); +} + +static void diff_fill_sha1_info(struct diff_filespec *one) +{ + if (DIFF_FILE_VALID(one)) { + if (!one->sha1_valid) { + struct stat st; + if (lstat(one->path, &st) < 0) + die("stat %s", one->path); + if (index_path(one->sha1, one->path, &st, 0)) + die("cannot hash %s\n", one->path); + } + } + else + hashclr(one->sha1); +} + +static void run_diff(struct diff_filepair *p, struct diff_options *o) +{ + const char *pgm = external_diff(); + char msg[PATH_MAX*2+300], *xfrm_msg; + struct diff_filespec *one; + struct diff_filespec *two; + const char *name; + const char *other; + char *name_munged, *other_munged; + int complete_rewrite = 0; + int len; + + if (DIFF_PAIR_UNMERGED(p)) { + /* unmerged */ + run_diff_cmd(pgm, p->one->path, NULL, NULL, NULL, NULL, o, 0); + return; + } + + name = p->one->path; + other = (strcmp(name, p->two->path) ? p->two->path : NULL); + name_munged = quote_one(name); + other_munged = quote_one(other); + one = p->one; two = p->two; + + diff_fill_sha1_info(one); + diff_fill_sha1_info(two); + + len = 0; + switch (p->status) { + case DIFF_STATUS_COPIED: + len += snprintf(msg + len, sizeof(msg) - len, + "similarity index %d%%\n" + "copy from %s\n" + "copy to %s\n", + (int)(0.5 + p->score * 100.0/MAX_SCORE), + name_munged, other_munged); + break; + case DIFF_STATUS_RENAMED: + len += snprintf(msg + len, sizeof(msg) - len, + "similarity index %d%%\n" + "rename from %s\n" + "rename to %s\n", + (int)(0.5 + p->score * 100.0/MAX_SCORE), + name_munged, other_munged); + break; + case DIFF_STATUS_MODIFIED: + if (p->score) { + len += snprintf(msg + len, sizeof(msg) - len, + "dissimilarity index %d%%\n", + (int)(0.5 + p->score * + 100.0/MAX_SCORE)); + complete_rewrite = 1; + break; + } + /* fallthru */ + default: + /* nothing */ + ; + } + + if (hashcmp(one->sha1, two->sha1)) { + int abbrev = o->full_index ? 40 : DEFAULT_ABBREV; + + if (o->binary) { + mmfile_t mf; + if ((!fill_mmfile(&mf, one) && mmfile_is_binary(&mf)) || + (!fill_mmfile(&mf, two) && mmfile_is_binary(&mf))) + abbrev = 40; + } + len += snprintf(msg + len, sizeof(msg) - len, + "index %.*s..%.*s", + abbrev, sha1_to_hex(one->sha1), + abbrev, sha1_to_hex(two->sha1)); + if (one->mode == two->mode) + len += snprintf(msg + len, sizeof(msg) - len, + " %06o", one->mode); + len += snprintf(msg + len, sizeof(msg) - len, "\n"); + } + + if (len) + msg[--len] = 0; + xfrm_msg = len ? msg : NULL; + + if (!pgm && + DIFF_FILE_VALID(one) && DIFF_FILE_VALID(two) && + (S_IFMT & one->mode) != (S_IFMT & two->mode)) { + /* a filepair that changes between file and symlink + * needs to be split into deletion and creation. + */ + struct diff_filespec *null = alloc_filespec(two->path); + run_diff_cmd(NULL, name, other, one, null, xfrm_msg, o, 0); + free(null); + null = alloc_filespec(one->path); + run_diff_cmd(NULL, name, other, null, two, xfrm_msg, o, 0); + free(null); + } + else + run_diff_cmd(pgm, name, other, one, two, xfrm_msg, o, + complete_rewrite); + + free(name_munged); + free(other_munged); +} + +static void run_diffstat(struct diff_filepair *p, struct diff_options *o, + struct diffstat_t *diffstat) +{ + const char *name; + const char *other; + int complete_rewrite = 0; + + if (DIFF_PAIR_UNMERGED(p)) { + /* unmerged */ + builtin_diffstat(p->one->path, NULL, NULL, NULL, diffstat, o, 0); + return; + } + + name = p->one->path; + other = (strcmp(name, p->two->path) ? p->two->path : NULL); + + diff_fill_sha1_info(p->one); + diff_fill_sha1_info(p->two); + + if (p->status == DIFF_STATUS_MODIFIED && p->score) + complete_rewrite = 1; + builtin_diffstat(name, other, p->one, p->two, diffstat, o, complete_rewrite); +} + +static void run_checkdiff(struct diff_filepair *p, struct diff_options *o) +{ + const char *name; + const char *other; + + if (DIFF_PAIR_UNMERGED(p)) { + /* unmerged */ + return; + } + + name = p->one->path; + other = (strcmp(name, p->two->path) ? p->two->path : NULL); + + diff_fill_sha1_info(p->one); + diff_fill_sha1_info(p->two); + + builtin_checkdiff(name, other, p->one, p->two); +} + +void diff_setup(struct diff_options *options) +{ + memset(options, 0, sizeof(*options)); + options->line_termination = '\n'; + options->break_opt = -1; + options->rename_limit = -1; + options->context = 3; + options->msg_sep = ""; + + options->change = diff_change; + options->add_remove = diff_addremove; + options->color_diff = diff_use_color_default; + options->detect_rename = diff_detect_rename_default; +} + +int diff_setup_done(struct diff_options *options) +{ + int count = 0; + + if (options->output_format & DIFF_FORMAT_NAME) + count++; + if (options->output_format & DIFF_FORMAT_NAME_STATUS) + count++; + if (options->output_format & DIFF_FORMAT_CHECKDIFF) + count++; + if (options->output_format & DIFF_FORMAT_NO_OUTPUT) + count++; + if (count > 1) + die("--name-only, --name-status, --check and -s are mutually exclusive"); + + if (options->find_copies_harder) + options->detect_rename = DIFF_DETECT_COPY; + + if (options->output_format & (DIFF_FORMAT_NAME | + DIFF_FORMAT_NAME_STATUS | + DIFF_FORMAT_CHECKDIFF | + DIFF_FORMAT_NO_OUTPUT)) + options->output_format &= ~(DIFF_FORMAT_RAW | + DIFF_FORMAT_NUMSTAT | + DIFF_FORMAT_DIFFSTAT | + DIFF_FORMAT_SHORTSTAT | + DIFF_FORMAT_SUMMARY | + DIFF_FORMAT_PATCH); + + /* + * These cases always need recursive; we do not drop caller-supplied + * recursive bits for other formats here. + */ + if (options->output_format & (DIFF_FORMAT_PATCH | + DIFF_FORMAT_NUMSTAT | + DIFF_FORMAT_DIFFSTAT | + DIFF_FORMAT_SHORTSTAT | + DIFF_FORMAT_SUMMARY | + DIFF_FORMAT_CHECKDIFF)) + options->recursive = 1; + /* + * Also pickaxe would not work very well if you do not say recursive + */ + if (options->pickaxe) + options->recursive = 1; + + if (options->detect_rename && options->rename_limit < 0) + options->rename_limit = diff_rename_limit_default; + if (options->setup & DIFF_SETUP_USE_CACHE) { + if (!active_cache) + /* read-cache does not die even when it fails + * so it is safe for us to do this here. Also + * it does not smudge active_cache or active_nr + * when it fails, so we do not have to worry about + * cleaning it up ourselves either. + */ + read_cache(); + } + if (options->setup & DIFF_SETUP_USE_SIZE_CACHE) + use_size_cache = 1; + if (options->abbrev <= 0 || 40 < options->abbrev) + options->abbrev = 40; /* full */ + + return 0; +} + +static int opt_arg(const char *arg, int arg_short, const char *arg_long, int *val) +{ + char c, *eq; + int len; + + if (*arg != '-') + return 0; + c = *++arg; + if (!c) + return 0; + if (c == arg_short) { + c = *++arg; + if (!c) + return 1; + if (val && isdigit(c)) { + char *end; + int n = strtoul(arg, &end, 10); + if (*end) + return 0; + *val = n; + return 1; + } + return 0; + } + if (c != '-') + return 0; + arg++; + eq = strchr(arg, '='); + if (eq) + len = eq - arg; + else + len = strlen(arg); + if (!len || strncmp(arg, arg_long, len)) + return 0; + if (eq) { + int n; + char *end; + if (!isdigit(*++eq)) + return 0; + n = strtoul(eq, &end, 10); + if (*end) + return 0; + *val = n; + } + return 1; +} + +int diff_opt_parse(struct diff_options *options, const char **av, int ac) +{ + const char *arg = av[0]; + if (!strcmp(arg, "-p") || !strcmp(arg, "-u")) + options->output_format |= DIFF_FORMAT_PATCH; + else if (opt_arg(arg, 'U', "unified", &options->context)) + options->output_format |= DIFF_FORMAT_PATCH; + else if (!strcmp(arg, "--raw")) + options->output_format |= DIFF_FORMAT_RAW; + else if (!strcmp(arg, "--patch-with-raw")) { + options->output_format |= DIFF_FORMAT_PATCH | DIFF_FORMAT_RAW; + } + else if (!strcmp(arg, "--numstat")) { + options->output_format |= DIFF_FORMAT_NUMSTAT; + } + else if (!strcmp(arg, "--shortstat")) { + options->output_format |= DIFF_FORMAT_SHORTSTAT; + } + else if (!strncmp(arg, "--stat", 6)) { + char *end; + int width = options->stat_width; + int name_width = options->stat_name_width; + arg += 6; + end = (char *)arg; + + switch (*arg) { + case '-': + if (!strncmp(arg, "-width=", 7)) + width = strtoul(arg + 7, &end, 10); + else if (!strncmp(arg, "-name-width=", 12)) + name_width = strtoul(arg + 12, &end, 10); + break; + case '=': + width = strtoul(arg+1, &end, 10); + if (*end == ',') + name_width = strtoul(end+1, &end, 10); + } + + /* Important! This checks all the error cases! */ + if (*end) + return 0; + options->output_format |= DIFF_FORMAT_DIFFSTAT; + options->stat_name_width = name_width; + options->stat_width = width; + } + else if (!strcmp(arg, "--check")) + options->output_format |= DIFF_FORMAT_CHECKDIFF; + else if (!strcmp(arg, "--summary")) + options->output_format |= DIFF_FORMAT_SUMMARY; + else if (!strcmp(arg, "--patch-with-stat")) { + options->output_format |= DIFF_FORMAT_PATCH | DIFF_FORMAT_DIFFSTAT; + } + else if (!strcmp(arg, "-z")) + options->line_termination = 0; + else if (!strncmp(arg, "-l", 2)) + options->rename_limit = strtoul(arg+2, NULL, 10); + else if (!strcmp(arg, "--full-index")) + options->full_index = 1; + else if (!strcmp(arg, "--binary")) { + options->output_format |= DIFF_FORMAT_PATCH; + options->binary = 1; + } + else if (!strcmp(arg, "-a") || !strcmp(arg, "--text")) { + options->text = 1; + } + else if (!strcmp(arg, "--name-only")) + options->output_format |= DIFF_FORMAT_NAME; + else if (!strcmp(arg, "--name-status")) + options->output_format |= DIFF_FORMAT_NAME_STATUS; + else if (!strcmp(arg, "-R")) + options->reverse_diff = 1; + else if (!strncmp(arg, "-S", 2)) + options->pickaxe = arg + 2; + else if (!strcmp(arg, "-s")) { + options->output_format |= DIFF_FORMAT_NO_OUTPUT; + } + else if (!strncmp(arg, "-O", 2)) + options->orderfile = arg + 2; + else if (!strncmp(arg, "--diff-filter=", 14)) + options->filter = arg + 14; + else if (!strcmp(arg, "--pickaxe-all")) + options->pickaxe_opts = DIFF_PICKAXE_ALL; + else if (!strcmp(arg, "--pickaxe-regex")) + options->pickaxe_opts = DIFF_PICKAXE_REGEX; + else if (!strncmp(arg, "-B", 2)) { + if ((options->break_opt = + diff_scoreopt_parse(arg)) == -1) + return -1; + } + else if (!strncmp(arg, "-M", 2)) { + if ((options->rename_score = + diff_scoreopt_parse(arg)) == -1) + return -1; + options->detect_rename = DIFF_DETECT_RENAME; + } + else if (!strncmp(arg, "-C", 2)) { + if ((options->rename_score = + diff_scoreopt_parse(arg)) == -1) + return -1; + options->detect_rename = DIFF_DETECT_COPY; + } + else if (!strcmp(arg, "--find-copies-harder")) + options->find_copies_harder = 1; + else if (!strcmp(arg, "--abbrev")) + options->abbrev = DEFAULT_ABBREV; + else if (!strncmp(arg, "--abbrev=", 9)) { + options->abbrev = strtoul(arg + 9, NULL, 10); + if (options->abbrev < MINIMUM_ABBREV) + options->abbrev = MINIMUM_ABBREV; + else if (40 < options->abbrev) + options->abbrev = 40; + } + else if (!strcmp(arg, "--color")) + options->color_diff = 1; + else if (!strcmp(arg, "--no-color")) + options->color_diff = 0; + else if (!strcmp(arg, "-w") || !strcmp(arg, "--ignore-all-space")) + options->xdl_opts |= XDF_IGNORE_WHITESPACE; + else if (!strcmp(arg, "-b") || !strcmp(arg, "--ignore-space-change")) + options->xdl_opts |= XDF_IGNORE_WHITESPACE_CHANGE; + else if (!strcmp(arg, "--color-words")) + options->color_diff = options->color_diff_words = 1; + else if (!strcmp(arg, "--no-renames")) + options->detect_rename = 0; + else + return 0; + return 1; +} + +static int parse_num(const char **cp_p) +{ + unsigned long num, scale; + int ch, dot; + const char *cp = *cp_p; + + num = 0; + scale = 1; + dot = 0; + for(;;) { + ch = *cp; + if ( !dot && ch == '.' ) { + scale = 1; + dot = 1; + } else if ( ch == '%' ) { + scale = dot ? scale*100 : 100; + cp++; /* % is always at the end */ + break; + } else if ( ch >= '0' && ch <= '9' ) { + if ( scale < 100000 ) { + scale *= 10; + num = (num*10) + (ch-'0'); + } + } else { + break; + } + cp++; + } + *cp_p = cp; + + /* user says num divided by scale and we say internally that + * is MAX_SCORE * num / scale. + */ + return (num >= scale) ? MAX_SCORE : (MAX_SCORE * num / scale); +} + +int diff_scoreopt_parse(const char *opt) +{ + int opt1, opt2, cmd; + + if (*opt++ != '-') + return -1; + cmd = *opt++; + if (cmd != 'M' && cmd != 'C' && cmd != 'B') + return -1; /* that is not a -M, -C nor -B option */ + + opt1 = parse_num(&opt); + if (cmd != 'B') + opt2 = 0; + else { + if (*opt == 0) + opt2 = 0; + else if (*opt != '/') + return -1; /* we expect -B80/99 or -B80 */ + else { + opt++; + opt2 = parse_num(&opt); + } + } + if (*opt != 0) + return -1; + return opt1 | (opt2 << 16); +} + +struct diff_queue_struct diff_queued_diff; + +void diff_q(struct diff_queue_struct *queue, struct diff_filepair *dp) +{ + if (queue->alloc <= queue->nr) { + queue->alloc = alloc_nr(queue->alloc); + queue->queue = xrealloc(queue->queue, + sizeof(dp) * queue->alloc); + } + queue->queue[queue->nr++] = dp; +} + +struct diff_filepair *diff_queue(struct diff_queue_struct *queue, + struct diff_filespec *one, + struct diff_filespec *two) +{ + struct diff_filepair *dp = xcalloc(1, sizeof(*dp)); + dp->one = one; + dp->two = two; + if (queue) + diff_q(queue, dp); + return dp; +} + +void diff_free_filepair(struct diff_filepair *p) +{ + diff_free_filespec_data(p->one); + diff_free_filespec_data(p->two); + free(p->one); + free(p->two); + free(p); +} + +/* This is different from find_unique_abbrev() in that + * it stuffs the result with dots for alignment. + */ +const char *diff_unique_abbrev(const unsigned char *sha1, int len) +{ + int abblen; + const char *abbrev; + if (len == 40) + return sha1_to_hex(sha1); + + abbrev = find_unique_abbrev(sha1, len); + if (!abbrev) + return sha1_to_hex(sha1); + abblen = strlen(abbrev); + if (abblen < 37) { + static char hex[41]; + if (len < abblen && abblen <= len + 2) + sprintf(hex, "%s%.*s", abbrev, len+3-abblen, ".."); + else + sprintf(hex, "%s...", abbrev); + return hex; + } + return sha1_to_hex(sha1); +} + +static void diff_flush_raw(struct diff_filepair *p, + struct diff_options *options) +{ + int two_paths; + char status[10]; + int abbrev = options->abbrev; + const char *path_one, *path_two; + int inter_name_termination = '\t'; + int line_termination = options->line_termination; + + if (!line_termination) + inter_name_termination = 0; + + path_one = p->one->path; + path_two = p->two->path; + if (line_termination) { + path_one = quote_one(path_one); + path_two = quote_one(path_two); + } + + if (p->score) + sprintf(status, "%c%03d", p->status, + (int)(0.5 + p->score * 100.0/MAX_SCORE)); + else { + status[0] = p->status; + status[1] = 0; + } + switch (p->status) { + case DIFF_STATUS_COPIED: + case DIFF_STATUS_RENAMED: + two_paths = 1; + break; + case DIFF_STATUS_ADDED: + case DIFF_STATUS_DELETED: + two_paths = 0; + break; + default: + two_paths = 0; + break; + } + if (!(options->output_format & DIFF_FORMAT_NAME_STATUS)) { + printf(":%06o %06o %s ", + p->one->mode, p->two->mode, + diff_unique_abbrev(p->one->sha1, abbrev)); + printf("%s ", + diff_unique_abbrev(p->two->sha1, abbrev)); + } + printf("%s%c%s", status, inter_name_termination, path_one); + if (two_paths) + printf("%c%s", inter_name_termination, path_two); + putchar(line_termination); + if (path_one != p->one->path) + free((void*)path_one); + if (path_two != p->two->path) + free((void*)path_two); +} + +static void diff_flush_name(struct diff_filepair *p, struct diff_options *opt) +{ + char *path = p->two->path; + + if (opt->line_termination) + path = quote_one(p->two->path); + printf("%s%c", path, opt->line_termination); + if (p->two->path != path) + free(path); +} + +int diff_unmodified_pair(struct diff_filepair *p) +{ + /* This function is written stricter than necessary to support + * the currently implemented transformers, but the idea is to + * let transformers to produce diff_filepairs any way they want, + * and filter and clean them up here before producing the output. + */ + struct diff_filespec *one, *two; + + if (DIFF_PAIR_UNMERGED(p)) + return 0; /* unmerged is interesting */ + + one = p->one; + two = p->two; + + /* deletion, addition, mode or type change + * and rename are all interesting. + */ + if (DIFF_FILE_VALID(one) != DIFF_FILE_VALID(two) || + DIFF_PAIR_MODE_CHANGED(p) || + strcmp(one->path, two->path)) + return 0; + + /* both are valid and point at the same path. that is, we are + * dealing with a change. + */ + if (one->sha1_valid && two->sha1_valid && + !hashcmp(one->sha1, two->sha1)) + return 1; /* no change */ + if (!one->sha1_valid && !two->sha1_valid) + return 1; /* both look at the same file on the filesystem. */ + return 0; +} + +static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o) +{ + if (diff_unmodified_pair(p)) + return; + + if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) || + (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode))) + return; /* no tree diffs in patch format */ + + run_diff(p, o); +} + +static void diff_flush_stat(struct diff_filepair *p, struct diff_options *o, + struct diffstat_t *diffstat) +{ + if (diff_unmodified_pair(p)) + return; + + if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) || + (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode))) + return; /* no tree diffs in patch format */ + + run_diffstat(p, o, diffstat); +} + +static void diff_flush_checkdiff(struct diff_filepair *p, + struct diff_options *o) +{ + if (diff_unmodified_pair(p)) + return; + + if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) || + (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode))) + return; /* no tree diffs in patch format */ + + run_checkdiff(p, o); +} + +int diff_queue_is_empty(void) +{ + struct diff_queue_struct *q = &diff_queued_diff; + int i; + for (i = 0; i < q->nr; i++) + if (!diff_unmodified_pair(q->queue[i])) + return 0; + return 1; +} + +#if DIFF_DEBUG +void diff_debug_filespec(struct diff_filespec *s, int x, const char *one) +{ + fprintf(stderr, "queue[%d] %s (%s) %s %06o %s\n", + x, one ? one : "", + s->path, + DIFF_FILE_VALID(s) ? "valid" : "invalid", + s->mode, + s->sha1_valid ? sha1_to_hex(s->sha1) : ""); + fprintf(stderr, "queue[%d] %s size %lu flags %d\n", + x, one ? one : "", + s->size, s->xfrm_flags); +} + +void diff_debug_filepair(const struct diff_filepair *p, int i) +{ + diff_debug_filespec(p->one, i, "one"); + diff_debug_filespec(p->two, i, "two"); + fprintf(stderr, "score %d, status %c stays %d broken %d\n", + p->score, p->status ? p->status : '?', + p->source_stays, p->broken_pair); +} + +void diff_debug_queue(const char *msg, struct diff_queue_struct *q) +{ + int i; + if (msg) + fprintf(stderr, "%s\n", msg); + fprintf(stderr, "q->nr = %d\n", q->nr); + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + diff_debug_filepair(p, i); + } +} +#endif + +static void diff_resolve_rename_copy(void) +{ + int i, j; + struct diff_filepair *p, *pp; + struct diff_queue_struct *q = &diff_queued_diff; + + diff_debug_queue("resolve-rename-copy", q); + + for (i = 0; i < q->nr; i++) { + p = q->queue[i]; + p->status = 0; /* undecided */ + if (DIFF_PAIR_UNMERGED(p)) + p->status = DIFF_STATUS_UNMERGED; + else if (!DIFF_FILE_VALID(p->one)) + p->status = DIFF_STATUS_ADDED; + else if (!DIFF_FILE_VALID(p->two)) + p->status = DIFF_STATUS_DELETED; + else if (DIFF_PAIR_TYPE_CHANGED(p)) + p->status = DIFF_STATUS_TYPE_CHANGED; + + /* from this point on, we are dealing with a pair + * whose both sides are valid and of the same type, i.e. + * either in-place edit or rename/copy edit. + */ + else if (DIFF_PAIR_RENAME(p)) { + if (p->source_stays) { + p->status = DIFF_STATUS_COPIED; + continue; + } + /* See if there is some other filepair that + * copies from the same source as us. If so + * we are a copy. Otherwise we are either a + * copy if the path stays, or a rename if it + * does not, but we already handled "stays" case. + */ + for (j = i + 1; j < q->nr; j++) { + pp = q->queue[j]; + if (strcmp(pp->one->path, p->one->path)) + continue; /* not us */ + if (!DIFF_PAIR_RENAME(pp)) + continue; /* not a rename/copy */ + /* pp is a rename/copy from the same source */ + p->status = DIFF_STATUS_COPIED; + break; + } + if (!p->status) + p->status = DIFF_STATUS_RENAMED; + } + else if (hashcmp(p->one->sha1, p->two->sha1) || + p->one->mode != p->two->mode) + p->status = DIFF_STATUS_MODIFIED; + else { + /* This is a "no-change" entry and should not + * happen anymore, but prepare for broken callers. + */ + error("feeding unmodified %s to diffcore", + p->one->path); + p->status = DIFF_STATUS_UNKNOWN; + } + } + diff_debug_queue("resolve-rename-copy done", q); +} + +static int check_pair_status(struct diff_filepair *p) +{ + switch (p->status) { + case DIFF_STATUS_UNKNOWN: + return 0; + case 0: + die("internal error in diff-resolve-rename-copy"); + default: + return 1; + } +} + +static void flush_one_pair(struct diff_filepair *p, struct diff_options *opt) +{ + int fmt = opt->output_format; + + if (fmt & DIFF_FORMAT_CHECKDIFF) + diff_flush_checkdiff(p, opt); + else if (fmt & (DIFF_FORMAT_RAW | DIFF_FORMAT_NAME_STATUS)) + diff_flush_raw(p, opt); + else if (fmt & DIFF_FORMAT_NAME) + diff_flush_name(p, opt); +} + +static void show_file_mode_name(const char *newdelete, struct diff_filespec *fs) +{ + char *name = quote_one(fs->path); + if (fs->mode) + printf(" %s mode %06o %s\n", newdelete, fs->mode, name); + else + printf(" %s %s\n", newdelete, name); + free(name); +} + + +static void show_mode_change(struct diff_filepair *p, int show_name) +{ + if (p->one->mode && p->two->mode && p->one->mode != p->two->mode) { + if (show_name) { + char *name = quote_one(p->two->path); + printf(" mode change %06o => %06o %s\n", + p->one->mode, p->two->mode, name); + free(name); + } + else + printf(" mode change %06o => %06o\n", + p->one->mode, p->two->mode); + } +} + +static void show_rename_copy(const char *renamecopy, struct diff_filepair *p) +{ + char *names = pprint_rename(p->one->path, p->two->path); + + printf(" %s %s (%d%%)\n", renamecopy, names, + (int)(0.5 + p->score * 100.0/MAX_SCORE)); + free(names); + show_mode_change(p, 0); +} + +static void diff_summary(struct diff_filepair *p) +{ + switch(p->status) { + case DIFF_STATUS_DELETED: + show_file_mode_name("delete", p->one); + break; + case DIFF_STATUS_ADDED: + show_file_mode_name("create", p->two); + break; + case DIFF_STATUS_COPIED: + show_rename_copy("copy", p); + break; + case DIFF_STATUS_RENAMED: + show_rename_copy("rename", p); + break; + default: + if (p->score) { + char *name = quote_one(p->two->path); + printf(" rewrite %s (%d%%)\n", name, + (int)(0.5 + p->score * 100.0/MAX_SCORE)); + free(name); + show_mode_change(p, 0); + } else show_mode_change(p, 1); + break; + } +} + +struct patch_id_t { + struct xdiff_emit_state xm; + SHA_CTX *ctx; + int patchlen; +}; + +static int remove_space(char *line, int len) +{ + int i; + char *dst = line; + unsigned char c; + + for (i = 0; i < len; i++) + if (!isspace((c = line[i]))) + *dst++ = c; + + return dst - line; +} + +static void patch_id_consume(void *priv, char *line, unsigned long len) +{ + struct patch_id_t *data = priv; + int new_len; + + /* Ignore line numbers when computing the SHA1 of the patch */ + if (!strncmp(line, "@@ -", 4)) + return; + + new_len = remove_space(line, len); + + SHA1_Update(data->ctx, line, new_len); + data->patchlen += new_len; +} + +/* returns 0 upon success, and writes result into sha1 */ +static int diff_get_patch_id(struct diff_options *options, unsigned char *sha1) +{ + struct diff_queue_struct *q = &diff_queued_diff; + int i; + SHA_CTX ctx; + struct patch_id_t data; + char buffer[PATH_MAX * 4 + 20]; + + SHA1_Init(&ctx); + memset(&data, 0, sizeof(struct patch_id_t)); + data.ctx = &ctx; + data.xm.consume = patch_id_consume; + + for (i = 0; i < q->nr; i++) { + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + mmfile_t mf1, mf2; + struct diff_filepair *p = q->queue[i]; + int len1, len2; + + if (p->status == 0) + return error("internal diff status error"); + if (p->status == DIFF_STATUS_UNKNOWN) + continue; + if (diff_unmodified_pair(p)) + continue; + if ((DIFF_FILE_VALID(p->one) && S_ISDIR(p->one->mode)) || + (DIFF_FILE_VALID(p->two) && S_ISDIR(p->two->mode))) + continue; + if (DIFF_PAIR_UNMERGED(p)) + continue; + + diff_fill_sha1_info(p->one); + diff_fill_sha1_info(p->two); + if (fill_mmfile(&mf1, p->one) < 0 || + fill_mmfile(&mf2, p->two) < 0) + return error("unable to read files to diff"); + + /* Maybe hash p->two? into the patch id? */ + if (mmfile_is_binary(&mf2)) + continue; + + len1 = remove_space(p->one->path, strlen(p->one->path)); + len2 = remove_space(p->two->path, strlen(p->two->path)); + if (p->one->mode == 0) + len1 = snprintf(buffer, sizeof(buffer), + "diff--gita/%.*sb/%.*s" + "newfilemode%06o" + "---/dev/null" + "+++b/%.*s", + len1, p->one->path, + len2, p->two->path, + p->two->mode, + len2, p->two->path); + else if (p->two->mode == 0) + len1 = snprintf(buffer, sizeof(buffer), + "diff--gita/%.*sb/%.*s" + "deletedfilemode%06o" + "---a/%.*s" + "+++/dev/null", + len1, p->one->path, + len2, p->two->path, + p->one->mode, + len1, p->one->path); + else + len1 = snprintf(buffer, sizeof(buffer), + "diff--gita/%.*sb/%.*s" + "---a/%.*s" + "+++b/%.*s", + len1, p->one->path, + len2, p->two->path, + len1, p->one->path, + len2, p->two->path); + SHA1_Update(&ctx, buffer, len1); + + xpp.flags = XDF_NEED_MINIMAL; + xecfg.ctxlen = 3; + xecfg.flags = XDL_EMIT_FUNCNAMES; + ecb.outf = xdiff_outf; + ecb.priv = &data; + xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb); + } + + SHA1_Final(sha1, &ctx); + return 0; +} + +int diff_flush_patch_id(struct diff_options *options, unsigned char *sha1) +{ + struct diff_queue_struct *q = &diff_queued_diff; + int i; + int result = diff_get_patch_id(options, sha1); + + for (i = 0; i < q->nr; i++) + diff_free_filepair(q->queue[i]); + + free(q->queue); + q->queue = NULL; + q->nr = q->alloc = 0; + + return result; +} + +static int is_summary_empty(const struct diff_queue_struct *q) +{ + int i; + + for (i = 0; i < q->nr; i++) { + const struct diff_filepair *p = q->queue[i]; + + switch (p->status) { + case DIFF_STATUS_DELETED: + case DIFF_STATUS_ADDED: + case DIFF_STATUS_COPIED: + case DIFF_STATUS_RENAMED: + return 0; + default: + if (p->score) + return 0; + if (p->one->mode && p->two->mode && + p->one->mode != p->two->mode) + return 0; + break; + } + } + return 1; +} + +void diff_flush(struct diff_options *options) +{ + struct diff_queue_struct *q = &diff_queued_diff; + int i, output_format = options->output_format; + int separator = 0; + + /* + * Order: raw, stat, summary, patch + * or: name/name-status/checkdiff (other bits clear) + */ + if (!q->nr) + goto free_queue; + + if (output_format & (DIFF_FORMAT_RAW | + DIFF_FORMAT_NAME | + DIFF_FORMAT_NAME_STATUS | + DIFF_FORMAT_CHECKDIFF)) { + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (check_pair_status(p)) + flush_one_pair(p, options); + } + separator++; + } + + if (output_format & (DIFF_FORMAT_DIFFSTAT|DIFF_FORMAT_SHORTSTAT|DIFF_FORMAT_NUMSTAT)) { + struct diffstat_t diffstat; + + memset(&diffstat, 0, sizeof(struct diffstat_t)); + diffstat.xm.consume = diffstat_consume; + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (check_pair_status(p)) + diff_flush_stat(p, options, &diffstat); + } + if (output_format & DIFF_FORMAT_NUMSTAT) + show_numstat(&diffstat, options); + if (output_format & DIFF_FORMAT_DIFFSTAT) + show_stats(&diffstat, options); + else if (output_format & DIFF_FORMAT_SHORTSTAT) + show_shortstats(&diffstat); + separator++; + } + + if (output_format & DIFF_FORMAT_SUMMARY && !is_summary_empty(q)) { + for (i = 0; i < q->nr; i++) + diff_summary(q->queue[i]); + separator++; + } + + if (output_format & DIFF_FORMAT_PATCH) { + if (separator) { + if (options->stat_sep) { + /* attach patch instead of inline */ + fputs(options->stat_sep, stdout); + } else { + putchar(options->line_termination); + } + } + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (check_pair_status(p)) + diff_flush_patch(p, options); + } + } + + if (output_format & DIFF_FORMAT_CALLBACK) + options->format_callback(q, options, options->format_callback_data); + + for (i = 0; i < q->nr; i++) + diff_free_filepair(q->queue[i]); +free_queue: + free(q->queue); + q->queue = NULL; + q->nr = q->alloc = 0; +} + +static void diffcore_apply_filter(const char *filter) +{ + int i; + struct diff_queue_struct *q = &diff_queued_diff; + struct diff_queue_struct outq; + outq.queue = NULL; + outq.nr = outq.alloc = 0; + + if (!filter) + return; + + if (strchr(filter, DIFF_STATUS_FILTER_AON)) { + int found; + for (i = found = 0; !found && i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (((p->status == DIFF_STATUS_MODIFIED) && + ((p->score && + strchr(filter, DIFF_STATUS_FILTER_BROKEN)) || + (!p->score && + strchr(filter, DIFF_STATUS_MODIFIED)))) || + ((p->status != DIFF_STATUS_MODIFIED) && + strchr(filter, p->status))) + found++; + } + if (found) + return; + + /* otherwise we will clear the whole queue + * by copying the empty outq at the end of this + * function, but first clear the current entries + * in the queue. + */ + for (i = 0; i < q->nr; i++) + diff_free_filepair(q->queue[i]); + } + else { + /* Only the matching ones */ + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + + if (((p->status == DIFF_STATUS_MODIFIED) && + ((p->score && + strchr(filter, DIFF_STATUS_FILTER_BROKEN)) || + (!p->score && + strchr(filter, DIFF_STATUS_MODIFIED)))) || + ((p->status != DIFF_STATUS_MODIFIED) && + strchr(filter, p->status))) + diff_q(&outq, p); + else + diff_free_filepair(p); + } + } + free(q->queue); + *q = outq; +} + +void diffcore_std(struct diff_options *options) +{ + if (options->break_opt != -1) + diffcore_break(options->break_opt); + if (options->detect_rename) + diffcore_rename(options); + if (options->break_opt != -1) + diffcore_merge_broken(); + if (options->pickaxe) + diffcore_pickaxe(options->pickaxe, options->pickaxe_opts); + if (options->orderfile) + diffcore_order(options->orderfile); + diff_resolve_rename_copy(); + diffcore_apply_filter(options->filter); +} + + +void diffcore_std_no_resolve(struct diff_options *options) +{ + if (options->pickaxe) + diffcore_pickaxe(options->pickaxe, options->pickaxe_opts); + if (options->orderfile) + diffcore_order(options->orderfile); + diffcore_apply_filter(options->filter); +} + +void diff_addremove(struct diff_options *options, + int addremove, unsigned mode, + const unsigned char *sha1, + const char *base, const char *path) +{ + char concatpath[PATH_MAX]; + struct diff_filespec *one, *two; + + /* This may look odd, but it is a preparation for + * feeding "there are unchanged files which should + * not produce diffs, but when you are doing copy + * detection you would need them, so here they are" + * entries to the diff-core. They will be prefixed + * with something like '=' or '*' (I haven't decided + * which but should not make any difference). + * Feeding the same new and old to diff_change() + * also has the same effect. + * Before the final output happens, they are pruned after + * merged into rename/copy pairs as appropriate. + */ + if (options->reverse_diff) + addremove = (addremove == '+' ? '-' : + addremove == '-' ? '+' : addremove); + + if (!path) path = ""; + sprintf(concatpath, "%s%s", base, path); + one = alloc_filespec(concatpath); + two = alloc_filespec(concatpath); + + if (addremove != '+') + fill_filespec(one, sha1, mode); + if (addremove != '-') + fill_filespec(two, sha1, mode); + + diff_queue(&diff_queued_diff, one, two); +} + +void diff_change(struct diff_options *options, + unsigned old_mode, unsigned new_mode, + const unsigned char *old_sha1, + const unsigned char *new_sha1, + const char *base, const char *path) +{ + char concatpath[PATH_MAX]; + struct diff_filespec *one, *two; + + if (options->reverse_diff) { + unsigned tmp; + const unsigned char *tmp_c; + tmp = old_mode; old_mode = new_mode; new_mode = tmp; + tmp_c = old_sha1; old_sha1 = new_sha1; new_sha1 = tmp_c; + } + if (!path) path = ""; + sprintf(concatpath, "%s%s", base, path); + one = alloc_filespec(concatpath); + two = alloc_filespec(concatpath); + fill_filespec(one, old_sha1, old_mode); + fill_filespec(two, new_sha1, new_mode); + + diff_queue(&diff_queued_diff, one, two); +} + +void diff_unmerge(struct diff_options *options, + const char *path, + unsigned mode, const unsigned char *sha1) +{ + struct diff_filespec *one, *two; + one = alloc_filespec(path); + two = alloc_filespec(path); + fill_filespec(one, sha1, mode); + diff_queue(&diff_queued_diff, one, two)->is_unmerged = 1; +} diff --git a/diff.h b/diff.h new file mode 100644 index 0000000000..eece65ddcc --- /dev/null +++ b/diff.h @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#ifndef DIFF_H +#define DIFF_H + +#include "tree-walk.h" + +struct rev_info; +struct diff_options; +struct diff_queue_struct; + +typedef void (*change_fn_t)(struct diff_options *options, + unsigned old_mode, unsigned new_mode, + const unsigned char *old_sha1, + const unsigned char *new_sha1, + const char *base, const char *path); + +typedef void (*add_remove_fn_t)(struct diff_options *options, + int addremove, unsigned mode, + const unsigned char *sha1, + const char *base, const char *path); + +typedef void (*diff_format_fn_t)(struct diff_queue_struct *q, + struct diff_options *options, void *data); + +#define DIFF_FORMAT_RAW 0x0001 +#define DIFF_FORMAT_DIFFSTAT 0x0002 +#define DIFF_FORMAT_NUMSTAT 0x0004 +#define DIFF_FORMAT_SUMMARY 0x0008 +#define DIFF_FORMAT_PATCH 0x0010 +#define DIFF_FORMAT_SHORTSTAT 0x0020 + +/* These override all above */ +#define DIFF_FORMAT_NAME 0x0100 +#define DIFF_FORMAT_NAME_STATUS 0x0200 +#define DIFF_FORMAT_CHECKDIFF 0x0400 + +/* Same as output_format = 0 but we know that -s flag was given + * and we should not give default value to output_format. + */ +#define DIFF_FORMAT_NO_OUTPUT 0x0800 + +#define DIFF_FORMAT_CALLBACK 0x1000 + +struct diff_options { + const char *filter; + const char *orderfile; + const char *pickaxe; + const char *single_follow; + unsigned recursive:1, + tree_in_recursive:1, + binary:1, + text:1, + full_index:1, + silent_on_remove:1, + find_copies_harder:1, + color_diff:1, + color_diff_words:1; + int context; + int break_opt; + int detect_rename; + int line_termination; + int output_format; + int pickaxe_opts; + int rename_score; + int reverse_diff; + int rename_limit; + int setup; + int abbrev; + const char *msg_sep; + const char *stat_sep; + long xdl_opts; + + int stat_width; + int stat_name_width; + + int nr_paths; + const char **paths; + int *pathlens; + change_fn_t change; + add_remove_fn_t add_remove; + diff_format_fn_t format_callback; + void *format_callback_data; +}; + +enum color_diff { + DIFF_RESET = 0, + DIFF_PLAIN = 1, + DIFF_METAINFO = 2, + DIFF_FRAGINFO = 3, + DIFF_FILE_OLD = 4, + DIFF_FILE_NEW = 5, + DIFF_COMMIT = 6, + DIFF_WHITESPACE = 7, +}; +const char *diff_get_color(int diff_use_color, enum color_diff ix); + +extern const char mime_boundary_leader[]; + +extern void diff_tree_setup_paths(const char **paths, struct diff_options *); +extern void diff_tree_release_paths(struct diff_options *); +extern int diff_tree(struct tree_desc *t1, struct tree_desc *t2, + const char *base, struct diff_options *opt); +extern int diff_tree_sha1(const unsigned char *old, const unsigned char *new, + const char *base, struct diff_options *opt); +extern int diff_root_tree_sha1(const unsigned char *new, const char *base, + struct diff_options *opt); + +struct combine_diff_path { + struct combine_diff_path *next; + int len; + char *path; + unsigned int mode; + unsigned char sha1[20]; + struct combine_diff_parent { + char status; + unsigned int mode; + unsigned char sha1[20]; + } parent[FLEX_ARRAY]; +}; +#define combine_diff_path_size(n, l) \ + (sizeof(struct combine_diff_path) + \ + sizeof(struct combine_diff_parent) * (n) + (l) + 1) + +extern void show_combined_diff(struct combine_diff_path *elem, int num_parent, + int dense, struct rev_info *); + +extern void diff_tree_combined(const unsigned char *sha1, const unsigned char parent[][20], int num_parent, int dense, struct rev_info *rev); + +extern void diff_tree_combined_merge(const unsigned char *sha1, int, struct rev_info *); + +extern void diff_addremove(struct diff_options *, + int addremove, + unsigned mode, + const unsigned char *sha1, + const char *base, + const char *path); + +extern void diff_change(struct diff_options *, + unsigned mode1, unsigned mode2, + const unsigned char *sha1, + const unsigned char *sha2, + const char *base, const char *path); + +extern void diff_unmerge(struct diff_options *, + const char *path, + unsigned mode, + const unsigned char *sha1); + +extern int diff_scoreopt_parse(const char *opt); + +#define DIFF_SETUP_REVERSE 1 +#define DIFF_SETUP_USE_CACHE 2 +#define DIFF_SETUP_USE_SIZE_CACHE 4 + +extern int git_diff_ui_config(const char *var, const char *value); +extern void diff_setup(struct diff_options *); +extern int diff_opt_parse(struct diff_options *, const char **, int); +extern int diff_setup_done(struct diff_options *); + +#define DIFF_DETECT_RENAME 1 +#define DIFF_DETECT_COPY 2 + +#define DIFF_PICKAXE_ALL 1 +#define DIFF_PICKAXE_REGEX 2 + +extern void diffcore_std(struct diff_options *); + +extern void diffcore_std_no_resolve(struct diff_options *); + +#define COMMON_DIFF_OPTIONS_HELP \ +"\ncommon diff options:\n" \ +" -z output diff-raw with lines terminated with NUL.\n" \ +" -p output patch format.\n" \ +" -u synonym for -p.\n" \ +" --patch-with-raw\n" \ +" output both a patch and the diff-raw format.\n" \ +" --stat show diffstat instead of patch.\n" \ +" --numstat show numeric diffstat instead of patch.\n" \ +" --patch-with-stat\n" \ +" output a patch and prepend its diffstat.\n" \ +" --name-only show only names of changed files.\n" \ +" --name-status show names and status of changed files.\n" \ +" --full-index show full object name on index lines.\n" \ +" --abbrev=<n> abbreviate object names in diff-tree header and diff-raw.\n" \ +" -R swap input file pairs.\n" \ +" -B detect complete rewrites.\n" \ +" -M detect renames.\n" \ +" -C detect copies.\n" \ +" --find-copies-harder\n" \ +" try unchanged files as candidate for copy detection.\n" \ +" -l<n> limit rename attempts up to <n> paths.\n" \ +" -O<file> reorder diffs according to the <file>.\n" \ +" -S<string> find filepair whose only one side contains the string.\n" \ +" --pickaxe-all\n" \ +" show all files diff when -S is used and hit is found.\n" \ +" -a --text treat all files as text.\n" + +extern int diff_queue_is_empty(void); +extern void diff_flush(struct diff_options*); + +/* diff-raw status letters */ +#define DIFF_STATUS_ADDED 'A' +#define DIFF_STATUS_COPIED 'C' +#define DIFF_STATUS_DELETED 'D' +#define DIFF_STATUS_MODIFIED 'M' +#define DIFF_STATUS_RENAMED 'R' +#define DIFF_STATUS_TYPE_CHANGED 'T' +#define DIFF_STATUS_UNKNOWN 'X' +#define DIFF_STATUS_UNMERGED 'U' + +/* these are not diff-raw status letters proper, but used by + * diffcore-filter insn to specify additional restrictions. + */ +#define DIFF_STATUS_FILTER_AON '*' +#define DIFF_STATUS_FILTER_BROKEN 'B' + +extern const char *diff_unique_abbrev(const unsigned char *, int); + +extern int run_diff_files(struct rev_info *revs, int silent_on_removed); + +extern int run_diff_index(struct rev_info *revs, int cached); + +extern int do_diff_cache(const unsigned char *, struct diff_options *); +extern int diff_flush_patch_id(struct diff_options *, unsigned char *); + +#endif /* DIFF_H */ diff --git a/diffcore-break.c b/diffcore-break.c new file mode 100644 index 0000000000..acb18db1db --- /dev/null +++ b/diffcore-break.c @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" + +static int should_break(struct diff_filespec *src, + struct diff_filespec *dst, + int break_score, + int *merge_score_p) +{ + /* dst is recorded as a modification of src. Are they so + * different that we are better off recording this as a pair + * of delete and create? + * + * There are two criteria used in this algorithm. For the + * purposes of helping later rename/copy, we take both delete + * and insert into account and estimate the amount of "edit". + * If the edit is very large, we break this pair so that + * rename/copy can pick the pieces up to match with other + * files. + * + * On the other hand, we would want to ignore inserts for the + * pure "complete rewrite" detection. As long as most of the + * existing contents were removed from the file, it is a + * complete rewrite, and if sizable chunk from the original + * still remains in the result, it is not a rewrite. It does + * not matter how much or how little new material is added to + * the file. + * + * The score we leave for such a broken filepair uses the + * latter definition so that later clean-up stage can find the + * pieces that should not have been broken according to the + * latter definition after rename/copy runs, and merge the + * broken pair that have a score lower than given criteria + * back together. The break operation itself happens + * according to the former definition. + * + * The minimum_edit parameter tells us when to break (the + * amount of "edit" required for us to consider breaking the + * pair). We leave the amount of deletion in *merge_score_p + * when we return. + * + * The value we return is 1 if we want the pair to be broken, + * or 0 if we do not. + */ + unsigned long delta_size, base_size, src_copied, literal_added, + src_removed; + + *merge_score_p = 0; /* assume no deletion --- "do not break" + * is the default. + */ + + if (!S_ISREG(src->mode) || !S_ISREG(dst->mode)) + return 0; /* leave symlink rename alone */ + + if (src->sha1_valid && dst->sha1_valid && + !hashcmp(src->sha1, dst->sha1)) + return 0; /* they are the same */ + + if (diff_populate_filespec(src, 0) || diff_populate_filespec(dst, 0)) + return 0; /* error but caught downstream */ + + base_size = ((src->size < dst->size) ? src->size : dst->size); + if (base_size < MINIMUM_BREAK_SIZE) + return 0; /* we do not break too small filepair */ + + if (diffcore_count_changes(src->data, src->size, + dst->data, dst->size, + NULL, NULL, + 0, + &src_copied, &literal_added)) + return 0; + + /* sanity */ + if (src->size < src_copied) + src_copied = src->size; + if (dst->size < literal_added + src_copied) { + if (src_copied < dst->size) + literal_added = dst->size - src_copied; + else + literal_added = 0; + } + src_removed = src->size - src_copied; + + /* Compute merge-score, which is "how much is removed + * from the source material". The clean-up stage will + * merge the surviving pair together if the score is + * less than the minimum, after rename/copy runs. + */ + *merge_score_p = src_removed * MAX_SCORE / src->size; + + /* Extent of damage, which counts both inserts and + * deletes. + */ + delta_size = src_removed + literal_added; + if (delta_size * MAX_SCORE / base_size < break_score) + return 0; + + /* If you removed a lot without adding new material, that is + * not really a rewrite. + */ + if ((src->size * break_score < src_removed * MAX_SCORE) && + (literal_added * 20 < src_removed) && + (literal_added * 20 < src_copied)) + return 0; + + return 1; +} + +void diffcore_break(int break_score) +{ + struct diff_queue_struct *q = &diff_queued_diff; + struct diff_queue_struct outq; + + /* When the filepair has this much edit (insert and delete), + * it is first considered to be a rewrite and broken into a + * create and delete filepair. This is to help breaking a + * file that had too much new stuff added, possibly from + * moving contents from another file, so that rename/copy can + * match it with the other file. + * + * int break_score; we reuse incoming parameter for this. + */ + + /* After a pair is broken according to break_score and + * subjected to rename/copy, both of them may survive intact, + * due to lack of suitable rename/copy peer. Or, the caller + * may be calling us without using rename/copy. When that + * happens, we merge the broken pieces back into one + * modification together if the pair did not have more than + * this much delete. For this computation, we do not take + * insert into account at all. If you start from a 100-line + * file and delete 97 lines of it, it does not matter if you + * add 27 lines to it to make a new 30-line file or if you add + * 997 lines to it to make a 1000-line file. Either way what + * you did was a rewrite of 97%. On the other hand, if you + * delete 3 lines, keeping 97 lines intact, it does not matter + * if you add 3 lines to it to make a new 100-line file or if + * you add 903 lines to it to make a new 1000-line file. + * Either way you did a lot of additions and not a rewrite. + * This merge happens to catch the latter case. A merge_score + * of 80% would be a good default value (a broken pair that + * has score lower than merge_score will be merged back + * together). + */ + int merge_score; + int i; + + /* See comment on DEFAULT_BREAK_SCORE and + * DEFAULT_MERGE_SCORE in diffcore.h + */ + merge_score = (break_score >> 16) & 0xFFFF; + break_score = (break_score & 0xFFFF); + + if (!break_score) + break_score = DEFAULT_BREAK_SCORE; + if (!merge_score) + merge_score = DEFAULT_MERGE_SCORE; + + outq.nr = outq.alloc = 0; + outq.queue = NULL; + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + int score; + + /* We deal only with in-place edit of non directory. + * We do not break anything else. + */ + if (DIFF_FILE_VALID(p->one) && DIFF_FILE_VALID(p->two) && + !S_ISDIR(p->one->mode) && !S_ISDIR(p->two->mode) && + !strcmp(p->one->path, p->two->path)) { + if (should_break(p->one, p->two, + break_score, &score)) { + /* Split this into delete and create */ + struct diff_filespec *null_one, *null_two; + struct diff_filepair *dp; + + /* Set score to 0 for the pair that + * needs to be merged back together + * should they survive rename/copy. + * Also we do not want to break very + * small files. + */ + if (score < merge_score) + score = 0; + + /* deletion of one */ + null_one = alloc_filespec(p->one->path); + dp = diff_queue(&outq, p->one, null_one); + dp->score = score; + dp->broken_pair = 1; + + /* creation of two */ + null_two = alloc_filespec(p->two->path); + dp = diff_queue(&outq, null_two, p->two); + dp->score = score; + dp->broken_pair = 1; + + free(p); /* not diff_free_filepair(), we are + * reusing one and two here. + */ + continue; + } + } + diff_q(&outq, p); + } + free(q->queue); + *q = outq; + + return; +} + +static void merge_broken(struct diff_filepair *p, + struct diff_filepair *pp, + struct diff_queue_struct *outq) +{ + /* p and pp are broken pairs we want to merge */ + struct diff_filepair *c = p, *d = pp, *dp; + if (DIFF_FILE_VALID(p->one)) { + /* this must be a delete half */ + d = p; c = pp; + } + /* Sanity check */ + if (!DIFF_FILE_VALID(d->one)) + die("internal error in merge #1"); + if (DIFF_FILE_VALID(d->two)) + die("internal error in merge #2"); + if (DIFF_FILE_VALID(c->one)) + die("internal error in merge #3"); + if (!DIFF_FILE_VALID(c->two)) + die("internal error in merge #4"); + + dp = diff_queue(outq, d->one, c->two); + dp->score = p->score; + diff_free_filespec_data(d->two); + diff_free_filespec_data(c->one); + free(d); + free(c); +} + +void diffcore_merge_broken(void) +{ + struct diff_queue_struct *q = &diff_queued_diff; + struct diff_queue_struct outq; + int i, j; + + outq.nr = outq.alloc = 0; + outq.queue = NULL; + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (!p) + /* we already merged this with its peer */ + continue; + else if (p->broken_pair && + !strcmp(p->one->path, p->two->path)) { + /* If the peer also survived rename/copy, then + * we merge them back together. + */ + for (j = i + 1; j < q->nr; j++) { + struct diff_filepair *pp = q->queue[j]; + if (pp->broken_pair && + !strcmp(pp->one->path, pp->two->path) && + !strcmp(p->one->path, pp->two->path)) { + /* Peer survived. Merge them */ + merge_broken(p, pp, &outq); + q->queue[j] = NULL; + break; + } + } + if (q->nr <= j) + /* The peer did not survive, so we keep + * it in the output. + */ + diff_q(&outq, p); + } + else + diff_q(&outq, p); + } + free(q->queue); + *q = outq; + + return; +} diff --git a/diffcore-delta.c b/diffcore-delta.c new file mode 100644 index 0000000000..7338a40c59 --- /dev/null +++ b/diffcore-delta.c @@ -0,0 +1,213 @@ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" + +/* + * Idea here is very simple. + * + * We have total of (sz-N+1) N-byte overlapping sequences in buf whose + * size is sz. If the same N-byte sequence appears in both source and + * destination, we say the byte that starts that sequence is shared + * between them (i.e. copied from source to destination). + * + * For each possible N-byte sequence, if the source buffer has more + * instances of it than the destination buffer, that means the + * difference are the number of bytes not copied from source to + * destination. If the counts are the same, everything was copied + * from source to destination. If the destination has more, + * everything was copied, and destination added more. + * + * We are doing an approximation so we do not really have to waste + * memory by actually storing the sequence. We just hash them into + * somewhere around 2^16 hashbuckets and count the occurrences. + * + * The length of the sequence is arbitrarily set to 8 for now. + */ + +/* Wild guess at the initial hash size */ +#define INITIAL_HASH_SIZE 9 + +/* We leave more room in smaller hash but do not let it + * grow to have unused hole too much. + */ +#define INITIAL_FREE(sz_log2) ((1<<(sz_log2))*(sz_log2-3)/(sz_log2)) + +/* A prime rather carefully chosen between 2^16..2^17, so that + * HASHBASE < INITIAL_FREE(17). We want to keep the maximum hashtable + * size under the current 2<<17 maximum, which can hold this many + * different values before overflowing to hashtable of size 2<<18. + */ +#define HASHBASE 107927 + +struct spanhash { + unsigned int hashval; + unsigned int cnt; +}; +struct spanhash_top { + int alloc_log2; + int free; + struct spanhash data[FLEX_ARRAY]; +}; + +static struct spanhash *spanhash_find(struct spanhash_top *top, + unsigned int hashval) +{ + int sz = 1 << top->alloc_log2; + int bucket = hashval & (sz - 1); + while (1) { + struct spanhash *h = &(top->data[bucket++]); + if (!h->cnt) + return NULL; + if (h->hashval == hashval) + return h; + if (sz <= bucket) + bucket = 0; + } +} + +static struct spanhash_top *spanhash_rehash(struct spanhash_top *orig) +{ + struct spanhash_top *new; + int i; + int osz = 1 << orig->alloc_log2; + int sz = osz << 1; + + new = xmalloc(sizeof(*orig) + sizeof(struct spanhash) * sz); + new->alloc_log2 = orig->alloc_log2 + 1; + new->free = INITIAL_FREE(new->alloc_log2); + memset(new->data, 0, sizeof(struct spanhash) * sz); + for (i = 0; i < osz; i++) { + struct spanhash *o = &(orig->data[i]); + int bucket; + if (!o->cnt) + continue; + bucket = o->hashval & (sz - 1); + while (1) { + struct spanhash *h = &(new->data[bucket++]); + if (!h->cnt) { + h->hashval = o->hashval; + h->cnt = o->cnt; + new->free--; + break; + } + if (sz <= bucket) + bucket = 0; + } + } + free(orig); + return new; +} + +static struct spanhash_top *add_spanhash(struct spanhash_top *top, + unsigned int hashval, int cnt) +{ + int bucket, lim; + struct spanhash *h; + + lim = (1 << top->alloc_log2); + bucket = hashval & (lim - 1); + while (1) { + h = &(top->data[bucket++]); + if (!h->cnt) { + h->hashval = hashval; + h->cnt = cnt; + top->free--; + if (top->free < 0) + return spanhash_rehash(top); + return top; + } + if (h->hashval == hashval) { + h->cnt += cnt; + return top; + } + if (lim <= bucket) + bucket = 0; + } +} + +static struct spanhash_top *hash_chars(unsigned char *buf, unsigned int sz) +{ + int i, n; + unsigned int accum1, accum2, hashval; + struct spanhash_top *hash; + + i = INITIAL_HASH_SIZE; + hash = xmalloc(sizeof(*hash) + sizeof(struct spanhash) * (1<<i)); + hash->alloc_log2 = i; + hash->free = INITIAL_FREE(i); + memset(hash->data, 0, sizeof(struct spanhash) * (1<<i)); + + n = 0; + accum1 = accum2 = 0; + while (sz) { + unsigned int c = *buf++; + unsigned int old_1 = accum1; + sz--; + accum1 = (accum1 << 7) ^ (accum2 >> 25); + accum2 = (accum2 << 7) ^ (old_1 >> 25); + accum1 += c; + if (++n < 64 && c != '\n') + continue; + hashval = (accum1 + accum2 * 0x61) % HASHBASE; + hash = add_spanhash(hash, hashval, n); + n = 0; + accum1 = accum2 = 0; + } + return hash; +} + +int diffcore_count_changes(void *src, unsigned long src_size, + void *dst, unsigned long dst_size, + void **src_count_p, + void **dst_count_p, + unsigned long delta_limit, + unsigned long *src_copied, + unsigned long *literal_added) +{ + int i, ssz; + struct spanhash_top *src_count, *dst_count; + unsigned long sc, la; + + src_count = dst_count = NULL; + if (src_count_p) + src_count = *src_count_p; + if (!src_count) { + src_count = hash_chars(src, src_size); + if (src_count_p) + *src_count_p = src_count; + } + if (dst_count_p) + dst_count = *dst_count_p; + if (!dst_count) { + dst_count = hash_chars(dst, dst_size); + if (dst_count_p) + *dst_count_p = dst_count; + } + sc = la = 0; + + ssz = 1 << src_count->alloc_log2; + for (i = 0; i < ssz; i++) { + struct spanhash *s = &(src_count->data[i]); + struct spanhash *d; + unsigned dst_cnt, src_cnt; + if (!s->cnt) + continue; + src_cnt = s->cnt; + d = spanhash_find(dst_count, s->hashval); + dst_cnt = d ? d->cnt : 0; + if (src_cnt < dst_cnt) { + la += dst_cnt - src_cnt; + sc += src_cnt; + } + else + sc += dst_cnt; + } + + if (!src_count_p) + free(src_count); + if (!dst_count_p) + free(dst_count); + *src_copied = sc; + *literal_added = la; + return 0; +} diff --git a/diffcore-order.c b/diffcore-order.c new file mode 100644 index 0000000000..7ad0946185 --- /dev/null +++ b/diffcore-order.c @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" + +static char **order; +static int order_cnt; + +static void prepare_order(const char *orderfile) +{ + int fd, cnt, pass; + void *map; + char *cp, *endp; + struct stat st; + + if (order) + return; + + fd = open(orderfile, O_RDONLY); + if (fd < 0) + return; + if (fstat(fd, &st)) { + close(fd); + return; + } + map = mmap(NULL, st.st_size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0); + close(fd); + if (map == MAP_FAILED) + return; + endp = (char *) map + st.st_size; + for (pass = 0; pass < 2; pass++) { + cnt = 0; + cp = map; + while (cp < endp) { + char *ep; + for (ep = cp; ep < endp && *ep != '\n'; ep++) + ; + /* cp to ep has one line */ + if (*cp == '\n' || *cp == '#') + ; /* comment */ + else if (pass == 0) + cnt++; + else { + if (*ep == '\n') { + *ep = 0; + order[cnt] = cp; + } + else { + order[cnt] = xmalloc(ep-cp+1); + memcpy(order[cnt], cp, ep-cp); + order[cnt][ep-cp] = 0; + } + cnt++; + } + if (ep < endp) + ep++; + cp = ep; + } + if (pass == 0) { + order_cnt = cnt; + order = xmalloc(sizeof(*order) * cnt); + } + } +} + +struct pair_order { + struct diff_filepair *pair; + int orig_order; + int order; +}; + +static int match_order(const char *path) +{ + int i; + char p[PATH_MAX]; + + for (i = 0; i < order_cnt; i++) { + strcpy(p, path); + while (p[0]) { + char *cp; + if (!fnmatch(order[i], p, 0)) + return i; + cp = strrchr(p, '/'); + if (!cp) + break; + *cp = 0; + } + } + return order_cnt; +} + +static int compare_pair_order(const void *a_, const void *b_) +{ + struct pair_order const *a, *b; + a = (struct pair_order const *)a_; + b = (struct pair_order const *)b_; + if (a->order != b->order) + return a->order - b->order; + return a->orig_order - b->orig_order; +} + +void diffcore_order(const char *orderfile) +{ + struct diff_queue_struct *q = &diff_queued_diff; + struct pair_order *o; + int i; + + if (!q->nr) + return; + + o = xmalloc(sizeof(*o) * q->nr); + prepare_order(orderfile); + for (i = 0; i < q->nr; i++) { + o[i].pair = q->queue[i]; + o[i].orig_order = i; + o[i].order = match_order(o[i].pair->two->path); + } + qsort(o, q->nr, sizeof(*o), compare_pair_order); + for (i = 0; i < q->nr; i++) + q->queue[i] = o[i].pair; + free(o); + return; +} diff --git a/diffcore-pickaxe.c b/diffcore-pickaxe.c new file mode 100644 index 0000000000..286919e714 --- /dev/null +++ b/diffcore-pickaxe.c @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" + +static unsigned int contains(struct diff_filespec *one, + const char *needle, unsigned long len, + regex_t *regexp) +{ + unsigned int cnt; + unsigned long offset, sz; + const char *data; + if (diff_populate_filespec(one, 0)) + return 0; + if (!len) + return 0; + + sz = one->size; + data = one->data; + cnt = 0; + + if (regexp) { + regmatch_t regmatch; + int flags = 0; + + while (*data && !regexec(regexp, data, 1, ®match, flags)) { + flags |= REG_NOTBOL; + data += regmatch.rm_so; + if (*data) data++; + cnt++; + } + + } else { /* Classic exact string match */ + /* Yes, I've heard of strstr(), but the thing is *data may + * not be NUL terminated. Sue me. + */ + for (offset = 0; offset + len <= sz; offset++) { + /* we count non-overlapping occurrences of needle */ + if (!memcmp(needle, data + offset, len)) { + offset += len - 1; + cnt++; + } + } + } + return cnt; +} + +void diffcore_pickaxe(const char *needle, int opts) +{ + struct diff_queue_struct *q = &diff_queued_diff; + unsigned long len = strlen(needle); + int i, has_changes; + regex_t regex, *regexp = NULL; + struct diff_queue_struct outq; + outq.queue = NULL; + outq.nr = outq.alloc = 0; + + if (opts & DIFF_PICKAXE_REGEX) { + int err; + err = regcomp(®ex, needle, REG_EXTENDED | REG_NEWLINE); + if (err) { + /* The POSIX.2 people are surely sick */ + char errbuf[1024]; + regerror(err, ®ex, errbuf, 1024); + regfree(®ex); + die("invalid pickaxe regex: %s", errbuf); + } + regexp = ®ex; + } + + if (opts & DIFF_PICKAXE_ALL) { + /* Showing the whole changeset if needle exists */ + for (i = has_changes = 0; !has_changes && i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (!DIFF_FILE_VALID(p->one)) { + if (!DIFF_FILE_VALID(p->two)) + continue; /* ignore unmerged */ + /* created */ + if (contains(p->two, needle, len, regexp)) + has_changes++; + } + else if (!DIFF_FILE_VALID(p->two)) { + if (contains(p->one, needle, len, regexp)) + has_changes++; + } + else if (!diff_unmodified_pair(p) && + contains(p->one, needle, len, regexp) != + contains(p->two, needle, len, regexp)) + has_changes++; + } + if (has_changes) + return; /* not munge the queue */ + + /* otherwise we will clear the whole queue + * by copying the empty outq at the end of this + * function, but first clear the current entries + * in the queue. + */ + for (i = 0; i < q->nr; i++) + diff_free_filepair(q->queue[i]); + } + else + /* Showing only the filepairs that has the needle */ + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + has_changes = 0; + if (!DIFF_FILE_VALID(p->one)) { + if (!DIFF_FILE_VALID(p->two)) + ; /* ignore unmerged */ + /* created */ + else if (contains(p->two, needle, len, regexp)) + has_changes = 1; + } + else if (!DIFF_FILE_VALID(p->two)) { + if (contains(p->one, needle, len, regexp)) + has_changes = 1; + } + else if (!diff_unmodified_pair(p) && + contains(p->one, needle, len, regexp) != + contains(p->two, needle, len, regexp)) + has_changes = 1; + + if (has_changes) + diff_q(&outq, p); + else + diff_free_filepair(p); + } + + if (opts & DIFF_PICKAXE_REGEX) { + regfree(®ex); + } + + free(q->queue); + *q = outq; + return; +} diff --git a/diffcore-rename.c b/diffcore-rename.c new file mode 100644 index 0000000000..91fa2bea51 --- /dev/null +++ b/diffcore-rename.c @@ -0,0 +1,468 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" + +/* Table of rename/copy destinations */ + +static struct diff_rename_dst { + struct diff_filespec *two; + struct diff_filepair *pair; +} *rename_dst; +static int rename_dst_nr, rename_dst_alloc; + +static struct diff_rename_dst *locate_rename_dst(struct diff_filespec *two, + int insert_ok) +{ + int first, last; + + first = 0; + last = rename_dst_nr; + while (last > first) { + int next = (last + first) >> 1; + struct diff_rename_dst *dst = &(rename_dst[next]); + int cmp = strcmp(two->path, dst->two->path); + if (!cmp) + return dst; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + /* not found */ + if (!insert_ok) + return NULL; + /* insert to make it at "first" */ + if (rename_dst_alloc <= rename_dst_nr) { + rename_dst_alloc = alloc_nr(rename_dst_alloc); + rename_dst = xrealloc(rename_dst, + rename_dst_alloc * sizeof(*rename_dst)); + } + rename_dst_nr++; + if (first < rename_dst_nr) + memmove(rename_dst + first + 1, rename_dst + first, + (rename_dst_nr - first - 1) * sizeof(*rename_dst)); + rename_dst[first].two = alloc_filespec(two->path); + fill_filespec(rename_dst[first].two, two->sha1, two->mode); + rename_dst[first].pair = NULL; + return &(rename_dst[first]); +} + +/* Table of rename/copy src files */ +static struct diff_rename_src { + struct diff_filespec *one; + unsigned short score; /* to remember the break score */ + unsigned src_path_left : 1; +} *rename_src; +static int rename_src_nr, rename_src_alloc; + +static struct diff_rename_src *register_rename_src(struct diff_filespec *one, + int src_path_left, + unsigned short score) +{ + int first, last; + + first = 0; + last = rename_src_nr; + while (last > first) { + int next = (last + first) >> 1; + struct diff_rename_src *src = &(rename_src[next]); + int cmp = strcmp(one->path, src->one->path); + if (!cmp) + return src; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + + /* insert to make it at "first" */ + if (rename_src_alloc <= rename_src_nr) { + rename_src_alloc = alloc_nr(rename_src_alloc); + rename_src = xrealloc(rename_src, + rename_src_alloc * sizeof(*rename_src)); + } + rename_src_nr++; + if (first < rename_src_nr) + memmove(rename_src + first + 1, rename_src + first, + (rename_src_nr - first - 1) * sizeof(*rename_src)); + rename_src[first].one = one; + rename_src[first].score = score; + rename_src[first].src_path_left = src_path_left; + return &(rename_src[first]); +} + +static int is_exact_match(struct diff_filespec *src, + struct diff_filespec *dst, + int contents_too) +{ + if (src->sha1_valid && dst->sha1_valid && + !hashcmp(src->sha1, dst->sha1)) + return 1; + if (!contents_too) + return 0; + if (diff_populate_filespec(src, 1) || diff_populate_filespec(dst, 1)) + return 0; + if (src->size != dst->size) + return 0; + if (src->sha1_valid && dst->sha1_valid) + return !hashcmp(src->sha1, dst->sha1); + if (diff_populate_filespec(src, 0) || diff_populate_filespec(dst, 0)) + return 0; + if (src->size == dst->size && + !memcmp(src->data, dst->data, src->size)) + return 1; + return 0; +} + +struct diff_score { + int src; /* index in rename_src */ + int dst; /* index in rename_dst */ + int score; +}; + +static int estimate_similarity(struct diff_filespec *src, + struct diff_filespec *dst, + int minimum_score) +{ + /* src points at a file that existed in the original tree (or + * optionally a file in the destination tree) and dst points + * at a newly created file. They may be quite similar, in which + * case we want to say src is renamed to dst or src is copied into + * dst, and then some edit has been applied to dst. + * + * Compare them and return how similar they are, representing + * the score as an integer between 0 and MAX_SCORE. + * + * When there is an exact match, it is considered a better + * match than anything else; the destination does not even + * call into this function in that case. + */ + unsigned long max_size, delta_size, base_size, src_copied, literal_added; + unsigned long delta_limit; + int score; + + /* We deal only with regular files. Symlink renames are handled + * only when they are exact matches --- in other words, no edits + * after renaming. + */ + if (!S_ISREG(src->mode) || !S_ISREG(dst->mode)) + return 0; + + max_size = ((src->size > dst->size) ? src->size : dst->size); + base_size = ((src->size < dst->size) ? src->size : dst->size); + delta_size = max_size - base_size; + + /* We would not consider edits that change the file size so + * drastically. delta_size must be smaller than + * (MAX_SCORE-minimum_score)/MAX_SCORE * min(src->size, dst->size). + * + * Note that base_size == 0 case is handled here already + * and the final score computation below would not have a + * divide-by-zero issue. + */ + if (base_size * (MAX_SCORE-minimum_score) < delta_size * MAX_SCORE) + return 0; + + if (diff_populate_filespec(src, 0) || diff_populate_filespec(dst, 0)) + return 0; /* error but caught downstream */ + + + delta_limit = base_size * (MAX_SCORE-minimum_score) / MAX_SCORE; + if (diffcore_count_changes(src->data, src->size, + dst->data, dst->size, + &src->cnt_data, &dst->cnt_data, + delta_limit, + &src_copied, &literal_added)) + return 0; + + /* How similar are they? + * what percentage of material in dst are from source? + */ + if (!dst->size) + score = 0; /* should not happen */ + else + score = src_copied * MAX_SCORE / max_size; + return score; +} + +static void record_rename_pair(int dst_index, int src_index, int score) +{ + struct diff_filespec *one, *two, *src, *dst; + struct diff_filepair *dp; + + if (rename_dst[dst_index].pair) + die("internal error: dst already matched."); + + src = rename_src[src_index].one; + one = alloc_filespec(src->path); + fill_filespec(one, src->sha1, src->mode); + + dst = rename_dst[dst_index].two; + two = alloc_filespec(dst->path); + fill_filespec(two, dst->sha1, dst->mode); + + dp = diff_queue(NULL, one, two); + dp->renamed_pair = 1; + if (!strcmp(src->path, dst->path)) + dp->score = rename_src[src_index].score; + else + dp->score = score; + dp->source_stays = rename_src[src_index].src_path_left; + rename_dst[dst_index].pair = dp; +} + +/* + * We sort the rename similarity matrix with the score, in descending + * order (the most similar first). + */ +static int score_compare(const void *a_, const void *b_) +{ + const struct diff_score *a = a_, *b = b_; + return b->score - a->score; +} + +static int compute_stays(struct diff_queue_struct *q, + struct diff_filespec *one) +{ + int i; + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (strcmp(one->path, p->two->path)) + continue; + if (DIFF_PAIR_RENAME(p)) { + return 0; /* something else is renamed into this */ + } + } + return 1; +} + +void diffcore_rename(struct diff_options *options) +{ + int detect_rename = options->detect_rename; + int minimum_score = options->rename_score; + int rename_limit = options->rename_limit; + struct diff_queue_struct *q = &diff_queued_diff; + struct diff_queue_struct outq; + struct diff_score *mx; + int i, j, rename_count, contents_too; + int num_create, num_src, dst_cnt; + + if (!minimum_score) + minimum_score = DEFAULT_RENAME_SCORE; + rename_count = 0; + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (!DIFF_FILE_VALID(p->one)) { + if (!DIFF_FILE_VALID(p->two)) + continue; /* unmerged */ + else if (options->single_follow && + strcmp(options->single_follow, p->two->path)) + continue; /* not interested */ + else + locate_rename_dst(p->two, 1); + } + else if (!DIFF_FILE_VALID(p->two)) { + /* If the source is a broken "delete", and + * they did not really want to get broken, + * that means the source actually stays. + */ + int stays = (p->broken_pair && !p->score); + register_rename_src(p->one, stays, p->score); + } + else if (detect_rename == DIFF_DETECT_COPY) + register_rename_src(p->one, 1, p->score); + } + if (rename_dst_nr == 0 || rename_src_nr == 0 || + (0 < rename_limit && rename_limit < rename_dst_nr)) + goto cleanup; /* nothing to do */ + + /* We really want to cull the candidates list early + * with cheap tests in order to avoid doing deltas. + * The first round matches up the up-to-date entries, + * and then during the second round we try to match + * cache-dirty entries as well. + */ + for (contents_too = 0; contents_too < 2; contents_too++) { + for (i = 0; i < rename_dst_nr; i++) { + struct diff_filespec *two = rename_dst[i].two; + if (rename_dst[i].pair) + continue; /* dealt with an earlier round */ + for (j = 0; j < rename_src_nr; j++) { + struct diff_filespec *one = rename_src[j].one; + if (!is_exact_match(one, two, contents_too)) + continue; + record_rename_pair(i, j, MAX_SCORE); + rename_count++; + break; /* we are done with this entry */ + } + } + } + + /* Have we run out the created file pool? If so we can avoid + * doing the delta matrix altogether. + */ + if (rename_count == rename_dst_nr) + goto cleanup; + + if (minimum_score == MAX_SCORE) + goto cleanup; + + num_create = (rename_dst_nr - rename_count); + num_src = rename_src_nr; + mx = xmalloc(sizeof(*mx) * num_create * num_src); + for (dst_cnt = i = 0; i < rename_dst_nr; i++) { + int base = dst_cnt * num_src; + struct diff_filespec *two = rename_dst[i].two; + if (rename_dst[i].pair) + continue; /* dealt with exact match already. */ + for (j = 0; j < rename_src_nr; j++) { + struct diff_filespec *one = rename_src[j].one; + struct diff_score *m = &mx[base+j]; + m->src = j; + m->dst = i; + m->score = estimate_similarity(one, two, + minimum_score); + } + /* We do not need the text anymore */ + diff_free_filespec_data(two); + dst_cnt++; + } + /* cost matrix sorted by most to least similar pair */ + qsort(mx, num_create * num_src, sizeof(*mx), score_compare); + for (i = 0; i < num_create * num_src; i++) { + struct diff_rename_dst *dst = &rename_dst[mx[i].dst]; + if (dst->pair) + continue; /* already done, either exact or fuzzy. */ + if (mx[i].score < minimum_score) + break; /* there is no more usable pair. */ + record_rename_pair(mx[i].dst, mx[i].src, mx[i].score); + rename_count++; + } + free(mx); + + cleanup: + /* At this point, we have found some renames and copies and they + * are recorded in rename_dst. The original list is still in *q. + */ + outq.queue = NULL; + outq.nr = outq.alloc = 0; + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + struct diff_filepair *pair_to_free = NULL; + + if (!DIFF_FILE_VALID(p->one) && DIFF_FILE_VALID(p->two)) { + /* + * Creation + * + * We would output this create record if it has + * not been turned into a rename/copy already. + */ + struct diff_rename_dst *dst = + locate_rename_dst(p->two, 0); + if (dst && dst->pair) { + diff_q(&outq, dst->pair); + pair_to_free = p; + } + else + /* no matching rename/copy source, so + * record this as a creation. + */ + diff_q(&outq, p); + } + else if (DIFF_FILE_VALID(p->one) && !DIFF_FILE_VALID(p->two)) { + /* + * Deletion + * + * We would output this delete record if: + * + * (1) this is a broken delete and the counterpart + * broken create remains in the output; or + * (2) this is not a broken delete, and rename_dst + * does not have a rename/copy to move p->one->path + * out of existence. + * + * Otherwise, the counterpart broken create + * has been turned into a rename-edit; or + * delete did not have a matching create to + * begin with. + */ + if (DIFF_PAIR_BROKEN(p)) { + /* broken delete */ + struct diff_rename_dst *dst = + locate_rename_dst(p->one, 0); + if (dst && dst->pair) + /* counterpart is now rename/copy */ + pair_to_free = p; + } + else { + for (j = 0; j < rename_dst_nr; j++) { + if (!rename_dst[j].pair) + continue; + if (strcmp(rename_dst[j].pair-> + one->path, + p->one->path)) + continue; + break; + } + if (j < rename_dst_nr) + /* this path remains */ + pair_to_free = p; + } + + if (pair_to_free) + ; + else + diff_q(&outq, p); + } + else if (!diff_unmodified_pair(p)) + /* all the usual ones need to be kept */ + diff_q(&outq, p); + else + /* no need to keep unmodified pairs */ + pair_to_free = p; + + if (pair_to_free) + diff_free_filepair(pair_to_free); + } + diff_debug_queue("done copying original", &outq); + + free(q->queue); + *q = outq; + diff_debug_queue("done collapsing", q); + + /* We need to see which rename source really stays here; + * earlier we only checked if the path is left in the result, + * but even if a path remains in the result, if that is coming + * from copying something else on top of it, then the original + * source is lost and does not stay. + */ + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (DIFF_PAIR_RENAME(p) && p->source_stays) { + /* If one appears as the target of a rename-copy, + * then mark p->source_stays = 0; otherwise + * leave it as is. + */ + p->source_stays = compute_stays(q, p->one); + } + } + + for (i = 0; i < rename_dst_nr; i++) { + diff_free_filespec_data(rename_dst[i].two); + free(rename_dst[i].two); + } + + free(rename_dst); + rename_dst = NULL; + rename_dst_nr = rename_dst_alloc = 0; + free(rename_src); + rename_src = NULL; + rename_src_nr = rename_src_alloc = 0; + return; +} diff --git a/diffcore.h b/diffcore.h new file mode 100644 index 0000000000..1ea80671e3 --- /dev/null +++ b/diffcore.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#ifndef _DIFFCORE_H_ +#define _DIFFCORE_H_ + +/* This header file is internal between diff.c and its diff transformers + * (e.g. diffcore-rename, diffcore-pickaxe). Never include this header + * in anything else. + */ + +/* We internally use unsigned short as the score value, + * and rely on an int capable to hold 32-bits. -B can take + * -Bmerge_score/break_score format and the two scores are + * passed around in one int (high 16-bit for merge and low 16-bit + * for break). + */ +#define MAX_SCORE 60000.0 +#define DEFAULT_RENAME_SCORE 30000 /* rename/copy similarity minimum (50%) */ +#define DEFAULT_BREAK_SCORE 30000 /* minimum for break to happen (50%) */ +#define DEFAULT_MERGE_SCORE 36000 /* maximum for break-merge to happen 60%) */ + +#define MINIMUM_BREAK_SIZE 400 /* do not break a file smaller than this */ + +struct diff_filespec { + unsigned char sha1[20]; + char *path; + void *data; + void *cnt_data; + unsigned long size; + int xfrm_flags; /* for use by the xfrm */ + unsigned short mode; /* file mode */ + unsigned sha1_valid : 1; /* if true, use sha1 and trust mode; + * if false, use the name and read from + * the filesystem. + */ +#define DIFF_FILE_VALID(spec) (((spec)->mode) != 0) + unsigned should_free : 1; /* data should be free()'ed */ + unsigned should_munmap : 1; /* data should be munmap()'ed */ +}; + +extern struct diff_filespec *alloc_filespec(const char *); +extern void fill_filespec(struct diff_filespec *, const unsigned char *, + unsigned short); + +extern int diff_populate_filespec(struct diff_filespec *, int); +extern void diff_free_filespec_data(struct diff_filespec *); + +struct diff_filepair { + struct diff_filespec *one; + struct diff_filespec *two; + unsigned short int score; + char status; /* M C R N D U (see Documentation/diff-format.txt) */ + unsigned source_stays : 1; /* all of R/C are copies */ + unsigned broken_pair : 1; + unsigned renamed_pair : 1; + unsigned is_unmerged : 1; +}; +#define DIFF_PAIR_UNMERGED(p) ((p)->is_unmerged) + +#define DIFF_PAIR_RENAME(p) ((p)->renamed_pair) + +#define DIFF_PAIR_BROKEN(p) \ + ( (!DIFF_FILE_VALID((p)->one) != !DIFF_FILE_VALID((p)->two)) && \ + ((p)->broken_pair != 0) ) + +#define DIFF_PAIR_TYPE_CHANGED(p) \ + ((S_IFMT & (p)->one->mode) != (S_IFMT & (p)->two->mode)) + +#define DIFF_PAIR_MODE_CHANGED(p) ((p)->one->mode != (p)->two->mode) + +extern void diff_free_filepair(struct diff_filepair *); + +extern int diff_unmodified_pair(struct diff_filepair *); + +struct diff_queue_struct { + struct diff_filepair **queue; + int alloc; + int nr; +}; + +extern struct diff_queue_struct diff_queued_diff; +extern struct diff_filepair *diff_queue(struct diff_queue_struct *, + struct diff_filespec *, + struct diff_filespec *); +extern void diff_q(struct diff_queue_struct *, struct diff_filepair *); + +extern void diffcore_pathspec(const char **pathspec); +extern void diffcore_break(int); +extern void diffcore_rename(struct diff_options *); +extern void diffcore_merge_broken(void); +extern void diffcore_pickaxe(const char *needle, int opts); +extern void diffcore_order(const char *orderfile); + +#define DIFF_DEBUG 0 +#if DIFF_DEBUG +void diff_debug_filespec(struct diff_filespec *, int, const char *); +void diff_debug_filepair(const struct diff_filepair *, int); +void diff_debug_queue(const char *, struct diff_queue_struct *); +#else +#define diff_debug_filespec(a,b,c) do {} while(0) +#define diff_debug_filepair(a,b) do {} while(0) +#define diff_debug_queue(a,b) do {} while(0) +#endif + +extern int diffcore_count_changes(void *src, unsigned long src_size, + void *dst, unsigned long dst_size, + void **src_count_p, + void **dst_count_p, + unsigned long delta_limit, + unsigned long *src_copied, + unsigned long *literal_added); + +#endif @@ -0,0 +1,427 @@ +/* + * This handles recursive filename detection with exclude + * files, index knowledge etc.. + * + * Copyright (C) Linus Torvalds, 2005-2006 + * Junio Hamano, 2005-2006 + */ +#include "cache.h" +#include "dir.h" + +int common_prefix(const char **pathspec) +{ + const char *path, *slash, *next; + int prefix; + + if (!pathspec) + return 0; + + path = *pathspec; + slash = strrchr(path, '/'); + if (!slash) + return 0; + + prefix = slash - path + 1; + while ((next = *++pathspec) != NULL) { + int len = strlen(next); + if (len >= prefix && !memcmp(path, next, len)) + continue; + for (;;) { + if (!len) + return 0; + if (next[--len] != '/') + continue; + if (memcmp(path, next, len+1)) + continue; + prefix = len + 1; + break; + } + } + return prefix; +} + +/* + * Does 'match' matches the given name? + * A match is found if + * + * (1) the 'match' string is leading directory of 'name', or + * (2) the 'match' string is a wildcard and matches 'name', or + * (3) the 'match' string is exactly the same as 'name'. + * + * and the return value tells which case it was. + * + * It returns 0 when there is no match. + */ +static int match_one(const char *match, const char *name, int namelen) +{ + int matchlen; + + /* If the match was just the prefix, we matched */ + matchlen = strlen(match); + if (!matchlen) + return MATCHED_RECURSIVELY; + + /* + * If we don't match the matchstring exactly, + * we need to match by fnmatch + */ + if (strncmp(match, name, matchlen)) + return !fnmatch(match, name, 0) ? MATCHED_FNMATCH : 0; + + if (!name[matchlen]) + return MATCHED_EXACTLY; + if (match[matchlen-1] == '/' || name[matchlen] == '/') + return MATCHED_RECURSIVELY; + return 0; +} + +/* + * Given a name and a list of pathspecs, see if the name matches + * any of the pathspecs. The caller is also interested in seeing + * all pathspec matches some names it calls this function with + * (otherwise the user could have mistyped the unmatched pathspec), + * and a mark is left in seen[] array for pathspec element that + * actually matched anything. + */ +int match_pathspec(const char **pathspec, const char *name, int namelen, int prefix, char *seen) +{ + int retval; + const char *match; + + name += prefix; + namelen -= prefix; + + for (retval = 0; (match = *pathspec++) != NULL; seen++) { + int how; + if (retval && *seen == MATCHED_EXACTLY) + continue; + match += prefix; + how = match_one(match, name, namelen); + if (how) { + if (retval < how) + retval = how; + if (*seen < how) + *seen = how; + } + } + return retval; +} + +void add_exclude(const char *string, const char *base, + int baselen, struct exclude_list *which) +{ + struct exclude *x = xmalloc(sizeof (*x)); + + x->pattern = string; + x->base = base; + x->baselen = baselen; + if (which->nr == which->alloc) { + which->alloc = alloc_nr(which->alloc); + which->excludes = xrealloc(which->excludes, + which->alloc * sizeof(x)); + } + which->excludes[which->nr++] = x; +} + +static int add_excludes_from_file_1(const char *fname, + const char *base, + int baselen, + struct exclude_list *which) +{ + struct stat st; + int fd, i; + long size; + char *buf, *entry; + + fd = open(fname, O_RDONLY); + if (fd < 0 || fstat(fd, &st) < 0) + goto err; + size = st.st_size; + if (size == 0) { + close(fd); + return 0; + } + buf = xmalloc(size+1); + if (read_in_full(fd, buf, size) != size) + goto err; + close(fd); + + buf[size++] = '\n'; + entry = buf; + for (i = 0; i < size; i++) { + if (buf[i] == '\n') { + if (entry != buf + i && entry[0] != '#') { + buf[i - (i && buf[i-1] == '\r')] = 0; + add_exclude(entry, base, baselen, which); + } + entry = buf + i + 1; + } + } + return 0; + + err: + if (0 <= fd) + close(fd); + return -1; +} + +void add_excludes_from_file(struct dir_struct *dir, const char *fname) +{ + if (add_excludes_from_file_1(fname, "", 0, + &dir->exclude_list[EXC_FILE]) < 0) + die("cannot use %s as an exclude file", fname); +} + +int push_exclude_per_directory(struct dir_struct *dir, const char *base, int baselen) +{ + char exclude_file[PATH_MAX]; + struct exclude_list *el = &dir->exclude_list[EXC_DIRS]; + int current_nr = el->nr; + + if (dir->exclude_per_dir) { + memcpy(exclude_file, base, baselen); + strcpy(exclude_file + baselen, dir->exclude_per_dir); + add_excludes_from_file_1(exclude_file, base, baselen, el); + } + return current_nr; +} + +void pop_exclude_per_directory(struct dir_struct *dir, int stk) +{ + struct exclude_list *el = &dir->exclude_list[EXC_DIRS]; + + while (stk < el->nr) + free(el->excludes[--el->nr]); +} + +/* Scan the list and let the last match determines the fate. + * Return 1 for exclude, 0 for include and -1 for undecided. + */ +static int excluded_1(const char *pathname, + int pathlen, + struct exclude_list *el) +{ + int i; + + if (el->nr) { + for (i = el->nr - 1; 0 <= i; i--) { + struct exclude *x = el->excludes[i]; + const char *exclude = x->pattern; + int to_exclude = 1; + + if (*exclude == '!') { + to_exclude = 0; + exclude++; + } + + if (!strchr(exclude, '/')) { + /* match basename */ + const char *basename = strrchr(pathname, '/'); + basename = (basename) ? basename+1 : pathname; + if (fnmatch(exclude, basename, 0) == 0) + return to_exclude; + } + else { + /* match with FNM_PATHNAME: + * exclude has base (baselen long) implicitly + * in front of it. + */ + int baselen = x->baselen; + if (*exclude == '/') + exclude++; + + if (pathlen < baselen || + (baselen && pathname[baselen-1] != '/') || + strncmp(pathname, x->base, baselen)) + continue; + + if (fnmatch(exclude, pathname+baselen, + FNM_PATHNAME) == 0) + return to_exclude; + } + } + } + return -1; /* undecided */ +} + +int excluded(struct dir_struct *dir, const char *pathname) +{ + int pathlen = strlen(pathname); + int st; + + for (st = EXC_CMDL; st <= EXC_FILE; st++) { + switch (excluded_1(pathname, pathlen, &dir->exclude_list[st])) { + case 0: + return 0; + case 1: + return 1; + } + } + return 0; +} + +struct dir_entry *dir_add_name(struct dir_struct *dir, const char *pathname, int len) +{ + struct dir_entry *ent; + + if (cache_name_pos(pathname, len) >= 0) + return NULL; + + if (dir->nr == dir->alloc) { + int alloc = alloc_nr(dir->alloc); + dir->alloc = alloc; + dir->entries = xrealloc(dir->entries, alloc*sizeof(ent)); + } + ent = xmalloc(sizeof(*ent) + len + 1); + ent->ignored = ent->ignored_dir = 0; + ent->len = len; + memcpy(ent->name, pathname, len); + ent->name[len] = 0; + dir->entries[dir->nr++] = ent; + return ent; +} + +static int dir_exists(const char *dirname, int len) +{ + int pos = cache_name_pos(dirname, len); + if (pos >= 0) + return 1; + pos = -pos-1; + if (pos >= active_nr) /* can't */ + return 0; + return !strncmp(active_cache[pos]->name, dirname, len); +} + +/* + * Read a directory tree. We currently ignore anything but + * directories, regular files and symlinks. That's because git + * doesn't handle them at all yet. Maybe that will change some + * day. + * + * Also, we ignore the name ".git" (even if it is not a directory). + * That likely will not change. + */ +static int read_directory_recursive(struct dir_struct *dir, const char *path, const char *base, int baselen, int check_only) +{ + DIR *fdir = opendir(path); + int contents = 0; + + if (fdir) { + int exclude_stk; + struct dirent *de; + char fullname[PATH_MAX + 1]; + memcpy(fullname, base, baselen); + + exclude_stk = push_exclude_per_directory(dir, base, baselen); + + while ((de = readdir(fdir)) != NULL) { + int len; + + if ((de->d_name[0] == '.') && + (de->d_name[1] == 0 || + !strcmp(de->d_name + 1, ".") || + !strcmp(de->d_name + 1, "git"))) + continue; + len = strlen(de->d_name); + memcpy(fullname + baselen, de->d_name, len+1); + if (excluded(dir, fullname) != dir->show_ignored) { + if (!dir->show_ignored || DTYPE(de) != DT_DIR) { + continue; + } + } + + switch (DTYPE(de)) { + struct stat st; + default: + continue; + case DT_UNKNOWN: + if (lstat(fullname, &st)) + continue; + if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) + break; + if (!S_ISDIR(st.st_mode)) + continue; + /* fallthrough */ + case DT_DIR: + memcpy(fullname + baselen + len, "/", 2); + len++; + if (dir->show_other_directories && + !dir_exists(fullname, baselen + len)) { + if (dir->hide_empty_directories && + !read_directory_recursive(dir, + fullname, fullname, + baselen + len, 1)) + continue; + break; + } + + contents += read_directory_recursive(dir, + fullname, fullname, baselen + len, 0); + continue; + case DT_REG: + case DT_LNK: + break; + } + contents++; + if (check_only) + goto exit_early; + else + dir_add_name(dir, fullname, baselen + len); + } +exit_early: + closedir(fdir); + + pop_exclude_per_directory(dir, exclude_stk); + } + + return contents; +} + +static int cmp_name(const void *p1, const void *p2) +{ + const struct dir_entry *e1 = *(const struct dir_entry **)p1; + const struct dir_entry *e2 = *(const struct dir_entry **)p2; + + return cache_name_compare(e1->name, e1->len, + e2->name, e2->len); +} + +int read_directory(struct dir_struct *dir, const char *path, const char *base, int baselen) +{ + /* + * Make sure to do the per-directory exclude for all the + * directories leading up to our base. + */ + if (baselen) { + if (dir->exclude_per_dir) { + char *p, *pp = xmalloc(baselen+1); + memcpy(pp, base, baselen+1); + p = pp; + while (1) { + char save = *p; + *p = 0; + push_exclude_per_directory(dir, pp, p-pp); + *p++ = save; + if (!save) + break; + p = strchr(p, '/'); + if (p) + p++; + else + p = pp + baselen; + } + free(pp); + } + } + + read_directory_recursive(dir, path, base, baselen, 0); + qsort(dir->entries, dir->nr, sizeof(struct dir_entry *), cmp_name); + return dir->nr; +} + +int +file_exists(const char *f) +{ + struct stat sb; + return stat(f, &sb) == 0; +} @@ -0,0 +1,62 @@ +#ifndef DIR_H +#define DIR_H + +/* + * We maintain three exclude pattern lists: + * EXC_CMDL lists patterns explicitly given on the command line. + * EXC_DIRS lists patterns obtained from per-directory ignore files. + * EXC_FILE lists patterns from fallback ignore files. + */ +#define EXC_CMDL 0 +#define EXC_DIRS 1 +#define EXC_FILE 2 + + +struct dir_entry { + unsigned int ignored : 1; + unsigned int ignored_dir : 1; + unsigned int len : 30; + char name[FLEX_ARRAY]; /* more */ +}; + +struct exclude_list { + int nr; + int alloc; + struct exclude { + const char *pattern; + const char *base; + int baselen; + } **excludes; +}; + +struct dir_struct { + int nr, alloc; + unsigned int show_ignored:1, + show_other_directories:1, + hide_empty_directories:1; + struct dir_entry **entries; + + /* Exclude info */ + const char *exclude_per_dir; + struct exclude_list exclude_list[3]; +}; + +extern int common_prefix(const char **pathspec); + +#define MATCHED_RECURSIVELY 1 +#define MATCHED_FNMATCH 2 +#define MATCHED_EXACTLY 3 +extern int match_pathspec(const char **pathspec, const char *name, int namelen, int prefix, char *seen); + +extern int read_directory(struct dir_struct *, const char *path, const char *base, int baselen); +extern int push_exclude_per_directory(struct dir_struct *, const char *, int); +extern void pop_exclude_per_directory(struct dir_struct *, int); + +extern int excluded(struct dir_struct *, const char *); +extern void add_excludes_from_file(struct dir_struct *, const char *fname); +extern void add_exclude(const char *string, const char *base, + int baselen, struct exclude_list *which); +extern int file_exists(const char *); +extern struct dir_entry *dir_add_name(struct dir_struct *dir, const char *pathname, int len); + +#endif diff --git a/dump-cache-tree.c b/dump-cache-tree.c new file mode 100644 index 0000000000..1f73f1ea7d --- /dev/null +++ b/dump-cache-tree.c @@ -0,0 +1,64 @@ +#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 (hashcmp(it->sha1, ref->sha1) || + 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, ""); +} diff --git a/entry.c b/entry.c new file mode 100644 index 0000000000..0ebf0f0c19 --- /dev/null +++ b/entry.c @@ -0,0 +1,172 @@ +#include "cache.h" +#include "blob.h" + +static void create_directories(const char *path, struct checkout *state) +{ + int len = strlen(path); + char *buf = xmalloc(len + 1); + const char *slash = path; + + while ((slash = strchr(slash+1, '/')) != NULL) { + len = slash - path; + memcpy(buf, path, len); + buf[len] = 0; + if (mkdir(buf, 0777)) { + if (errno == EEXIST) { + struct stat st; + if (len > state->base_dir_len && state->force && !unlink(buf) && !mkdir(buf, 0777)) + continue; + if (!stat(buf, &st) && S_ISDIR(st.st_mode)) + continue; /* ok */ + } + die("cannot create directory at %s", buf); + } + } + free(buf); +} + +static void remove_subtree(const char *path) +{ + DIR *dir = opendir(path); + struct dirent *de; + char pathbuf[PATH_MAX]; + char *name; + + if (!dir) + die("cannot opendir %s", path); + strcpy(pathbuf, path); + name = pathbuf + strlen(path); + *name++ = '/'; + while ((de = readdir(dir)) != NULL) { + struct stat st; + if ((de->d_name[0] == '.') && + ((de->d_name[1] == 0) || + ((de->d_name[1] == '.') && de->d_name[2] == 0))) + continue; + strcpy(name, de->d_name); + if (lstat(pathbuf, &st)) + die("cannot lstat %s", pathbuf); + if (S_ISDIR(st.st_mode)) + remove_subtree(pathbuf); + else if (unlink(pathbuf)) + die("cannot unlink %s", pathbuf); + } + closedir(dir); + if (rmdir(path)) + die("cannot rmdir %s", path); +} + +static int create_file(const char *path, unsigned int mode) +{ + mode = (mode & 0100) ? 0777 : 0666; + return open(path, O_WRONLY | O_CREAT | O_EXCL, mode); +} + +static int write_entry(struct cache_entry *ce, char *path, struct checkout *state, int to_tempfile) +{ + int fd; + void *new; + unsigned long size; + long wrote; + char type[20]; + + new = read_sha1_file(ce->sha1, type, &size); + if (!new || strcmp(type, blob_type)) { + if (new) + free(new); + return error("git-checkout-index: unable to read sha1 file of %s (%s)", + path, sha1_to_hex(ce->sha1)); + } + switch (ntohl(ce->ce_mode) & S_IFMT) { + case S_IFREG: + if (to_tempfile) { + strcpy(path, ".merge_file_XXXXXX"); + fd = mkstemp(path); + } else + fd = create_file(path, ntohl(ce->ce_mode)); + if (fd < 0) { + free(new); + return error("git-checkout-index: unable to create file %s (%s)", + path, strerror(errno)); + } + wrote = write_in_full(fd, new, size); + close(fd); + free(new); + if (wrote != size) + return error("git-checkout-index: unable to write file %s", path); + break; + case S_IFLNK: + if (to_tempfile) { + strcpy(path, ".merge_link_XXXXXX"); + fd = mkstemp(path); + if (fd < 0) { + free(new); + return error("git-checkout-index: unable to create " + "file %s (%s)", path, strerror(errno)); + } + wrote = write_in_full(fd, new, size); + close(fd); + free(new); + if (wrote != size) + return error("git-checkout-index: unable to write file %s", + path); + } else { + wrote = symlink(new, path); + free(new); + if (wrote) + return error("git-checkout-index: unable to create " + "symlink %s (%s)", path, strerror(errno)); + } + break; + default: + free(new); + return error("git-checkout-index: unknown file mode for %s", path); + } + + if (state->refresh_cache) { + struct stat st; + lstat(ce->name, &st); + fill_stat_cache_info(ce, &st); + } + return 0; +} + +int checkout_entry(struct cache_entry *ce, struct checkout *state, char *topath) +{ + static char path[PATH_MAX + 1]; + struct stat st; + int len = state->base_dir_len; + + if (topath) + return write_entry(ce, topath, state, 1); + + memcpy(path, state->base_dir, len); + strcpy(path + len, ce->name); + + if (!lstat(path, &st)) { + unsigned changed = ce_match_stat(ce, &st, 1); + if (!changed) + return 0; + if (!state->force) { + if (!state->quiet) + fprintf(stderr, "git-checkout-index: %s already exists\n", path); + return -1; + } + + /* + * We unlink the old file, to get the new one with the + * right permissions (including umask, which is nasty + * to emulate by hand - much easier to let the system + * just do the right thing) + */ + unlink(path); + if (S_ISDIR(st.st_mode)) { + if (!state->force) + return error("%s is a directory", path); + remove_subtree(path); + } + } else if (state->not_new) + return 0; + create_directories(path, state); + return write_entry(ce, path, state, 0); +} diff --git a/environment.c b/environment.c new file mode 100644 index 0000000000..54c22f8248 --- /dev/null +++ b/environment.c @@ -0,0 +1,105 @@ +/* + * We put all the git config variables in this same object + * file, so that programs can link against the config parser + * without having to link against all the rest of git. + * + * In particular, no need to bring in libz etc unless needed, + * even if you might want to know where the git directory etc + * are. + */ +#include "cache.h" + +char git_default_email[MAX_GITNAME]; +char git_default_name[MAX_GITNAME]; +int use_legacy_headers = 1; +int trust_executable_bit = 1; +int assume_unchanged; +int prefer_symlink_refs; +int is_bare_repository_cfg = -1; /* unspecified */ +int log_all_ref_updates = -1; /* unspecified */ +int warn_ambiguous_refs = 1; +int repository_format_version; +char *git_commit_encoding; +char *git_log_output_encoding; +int shared_repository = PERM_UMASK; +const char *apply_default_whitespace; +int zlib_compression_level = Z_DEFAULT_COMPRESSION; +size_t packed_git_window_size = DEFAULT_PACKED_GIT_WINDOW_SIZE; +size_t packed_git_limit = DEFAULT_PACKED_GIT_LIMIT; +int pager_in_use; +int pager_use_color = 1; + +static const char *git_dir; +static char *git_object_dir, *git_index_file, *git_refs_dir, *git_graft_file; + +static void setup_git_env(void) +{ + git_dir = getenv(GIT_DIR_ENVIRONMENT); + if (!git_dir) + git_dir = DEFAULT_GIT_DIR_ENVIRONMENT; + git_object_dir = getenv(DB_ENVIRONMENT); + if (!git_object_dir) { + git_object_dir = xmalloc(strlen(git_dir) + 9); + sprintf(git_object_dir, "%s/objects", git_dir); + } + git_refs_dir = xmalloc(strlen(git_dir) + 6); + sprintf(git_refs_dir, "%s/refs", git_dir); + git_index_file = getenv(INDEX_ENVIRONMENT); + if (!git_index_file) { + git_index_file = xmalloc(strlen(git_dir) + 7); + sprintf(git_index_file, "%s/index", git_dir); + } + git_graft_file = getenv(GRAFT_ENVIRONMENT); + if (!git_graft_file) + git_graft_file = xstrdup(git_path("info/grafts")); +} + +int is_bare_repository(void) +{ + const char *dir, *s; + if (0 <= is_bare_repository_cfg) + return is_bare_repository_cfg; + + dir = get_git_dir(); + if (!strcmp(dir, DEFAULT_GIT_DIR_ENVIRONMENT)) + return 0; + s = strrchr(dir, '/'); + return !s || strcmp(s + 1, DEFAULT_GIT_DIR_ENVIRONMENT); +} + +const char *get_git_dir(void) +{ + if (!git_dir) + setup_git_env(); + return git_dir; +} + +char *get_object_directory(void) +{ + if (!git_object_dir) + setup_git_env(); + return git_object_dir; +} + +char *get_refs_directory(void) +{ + if (!git_refs_dir) + setup_git_env(); + return git_refs_dir; +} + +char *get_index_file(void) +{ + if (!git_index_file) + setup_git_env(); + return git_index_file; +} + +char *get_graft_file(void) +{ + if (!git_graft_file) + setup_git_env(); + return git_graft_file; +} + + diff --git a/exec_cmd.c b/exec_cmd.c new file mode 100644 index 0000000000..3996bce33f --- /dev/null +++ b/exec_cmd.c @@ -0,0 +1,135 @@ +#include "cache.h" +#include "exec_cmd.h" +#include "quote.h" +#define MAX_ARGS 32 + +extern char **environ; +static const char *builtin_exec_path = GIT_EXEC_PATH; +static const char *current_exec_path; + +void git_set_exec_path(const char *exec_path) +{ + current_exec_path = exec_path; +} + + +/* Returns the highest-priority, location to look for git programs. */ +const char *git_exec_path(void) +{ + const char *env; + + if (current_exec_path) + return current_exec_path; + + env = getenv(EXEC_PATH_ENVIRONMENT); + if (env && *env) { + return env; + } + + return builtin_exec_path; +} + + +int execv_git_cmd(const char **argv) +{ + char git_command[PATH_MAX + 1]; + int i; + const char *paths[] = { current_exec_path, + getenv(EXEC_PATH_ENVIRONMENT), + 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 || !*exec_dir) continue; + + if (*exec_dir != '/') { + if (!getcwd(git_command, sizeof(git_command))) { + fprintf(stderr, "git: cannot determine " + "current directory: %s\n", + strerror(errno)); + break; + } + len = strlen(git_command); + + /* Trivial cleanup */ + while (!strncmp(exec_dir, "./", 2)) { + exec_dir += 2; + while (*exec_dir == '/') + 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); + 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; + } + + /* argv[0] must be the git command, but the argv array + * belongs to the caller, and my be reused in + * subsequent loop iterations. Save argv[0] and + * restore it on error. + */ + + tmp = argv[0]; + argv[0] = git_command; + + trace_argv_printf(argv, -1, "trace: exec:"); + + /* execve() can only ever return if it fails */ + execve(git_command, (char **)argv, environ); + + trace_printf("trace: exec failed: %s\n", strerror(errno)); + + argv[0] = tmp; + } + return -1; + +} + + +int execl_git_cmd(const char *cmd,...) +{ + int argc; + const char *argv[MAX_ARGS + 1]; + const char *arg; + va_list param; + + va_start(param, cmd); + argv[0] = cmd; + argc = 1; + while (argc < MAX_ARGS) { + arg = argv[argc++] = va_arg(param, char *); + if (!arg) + break; + } + va_end(param); + if (MAX_ARGS <= argc) + return error("too many args to run %s", cmd); + + argv[argc] = NULL; + return execv_git_cmd(argv); +} diff --git a/exec_cmd.h b/exec_cmd.h new file mode 100644 index 0000000000..989621ff4e --- /dev/null +++ b/exec_cmd.h @@ -0,0 +1,10 @@ +#ifndef __GIT_EXEC_CMD_H_ +#define __GIT_EXEC_CMD_H_ + +extern void git_set_exec_path(const char *exec_path); +extern const char* git_exec_path(void); +extern int execv_git_cmd(const char **argv); /* NULL terminated */ +extern int execl_git_cmd(const char *cmd, ...); + + +#endif /* __GIT_EXEC_CMD_H_ */ diff --git a/fast-import.c b/fast-import.c new file mode 100644 index 0000000000..f9cfc72637 --- /dev/null +++ b/fast-import.c @@ -0,0 +1,2077 @@ +/* +Format of STDIN stream: + + stream ::= cmd*; + + cmd ::= new_blob + | new_commit + | new_tag + | reset_branch + | checkpoint + ; + + new_blob ::= 'blob' lf + mark? + file_content; + file_content ::= data; + + new_commit ::= 'commit' sp ref_str lf + mark? + ('author' sp name '<' email '>' when lf)? + 'committer' sp name '<' email '>' when lf + commit_msg + ('from' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf)? + ('merge' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf)* + file_change* + lf; + commit_msg ::= data; + + file_change ::= file_clr | file_del | file_obm | file_inm; + file_clr ::= 'deleteall' lf; + file_del ::= 'D' sp path_str lf; + file_obm ::= 'M' sp mode sp (hexsha1 | idnum) sp path_str lf; + file_inm ::= 'M' sp mode sp 'inline' sp path_str lf + data; + + new_tag ::= 'tag' sp tag_str lf + 'from' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf + 'tagger' sp name '<' email '>' when lf + tag_msg; + tag_msg ::= data; + + reset_branch ::= 'reset' sp ref_str lf + ('from' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf)? + lf; + + checkpoint ::= 'checkpoint' lf + lf; + + # note: the first idnum in a stream should be 1 and subsequent + # idnums should not have gaps between values as this will cause + # the stream parser to reserve space for the gapped values. An + # idnum can be updated in the future to a new object by issuing + # a new mark directive with the old idnum. + # + mark ::= 'mark' sp idnum lf; + data ::= (delimited_data | exact_data) + lf; + + # note: delim may be any string but must not contain lf. + # data_line may contain any data but must not be exactly + # delim. + delimited_data ::= 'data' sp '<<' delim lf + (data_line lf)* + delim lf; + + # note: declen indicates the length of binary_data in bytes. + # declen does not include the lf preceeding the binary data. + # + exact_data ::= 'data' sp declen lf + binary_data; + + # note: quoted strings are C-style quoting supporting \c for + # common escapes of 'c' (e..g \n, \t, \\, \") or \nnn where nnn + # is the signed byte value in octal. Note that the only + # characters which must actually be escaped to protect the + # stream formatting is: \, " and LF. Otherwise these values + # are UTF8. + # + ref_str ::= ref; + sha1exp_str ::= sha1exp; + tag_str ::= tag; + path_str ::= path | '"' quoted(path) '"' ; + mode ::= '100644' | '644' + | '100755' | '755' + | '120000' + ; + + declen ::= # unsigned 32 bit value, ascii base10 notation; + bigint ::= # unsigned integer value, ascii base10 notation; + binary_data ::= # file content, not interpreted; + + when ::= raw_when | rfc2822_when; + raw_when ::= ts sp tz; + rfc2822_when ::= # Valid RFC 2822 date and time; + + sp ::= # ASCII space character; + lf ::= # ASCII newline (LF) character; + + # note: a colon (':') must precede the numerical value assigned to + # an idnum. This is to distinguish it from a ref or tag name as + # GIT does not permit ':' in ref or tag strings. + # + idnum ::= ':' bigint; + path ::= # GIT style file path, e.g. "a/b/c"; + ref ::= # GIT ref name, e.g. "refs/heads/MOZ_GECKO_EXPERIMENT"; + tag ::= # GIT tag name, e.g. "FIREFOX_1_5"; + sha1exp ::= # Any valid GIT SHA1 expression; + hexsha1 ::= # SHA1 in hexadecimal format; + + # note: name and email are UTF8 strings, however name must not + # contain '<' or lf and email must not contain any of the + # following: '<', '>', lf. + # + name ::= # valid GIT author/committer name; + email ::= # valid GIT author/committer email; + ts ::= # time since the epoch in seconds, ascii base10 notation; + tz ::= # GIT style timezone; +*/ + +#include "builtin.h" +#include "cache.h" +#include "object.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "delta.h" +#include "pack.h" +#include "refs.h" +#include "csum-file.h" +#include "strbuf.h" +#include "quote.h" + +#define PACK_ID_BITS 16 +#define MAX_PACK_ID ((1<<PACK_ID_BITS)-1) + +struct object_entry +{ + struct object_entry *next; + uint32_t offset; + unsigned type : TYPE_BITS; + unsigned pack_id : PACK_ID_BITS; + unsigned char sha1[20]; +}; + +struct object_entry_pool +{ + struct object_entry_pool *next_pool; + struct object_entry *next_free; + struct object_entry *end; + struct object_entry entries[FLEX_ARRAY]; /* more */ +}; + +struct mark_set +{ + union { + struct object_entry *marked[1024]; + struct mark_set *sets[1024]; + } data; + unsigned int shift; +}; + +struct last_object +{ + void *data; + unsigned long len; + uint32_t offset; + unsigned int depth; + unsigned no_free:1; +}; + +struct mem_pool +{ + struct mem_pool *next_pool; + char *next_free; + char *end; + char space[FLEX_ARRAY]; /* more */ +}; + +struct atom_str +{ + struct atom_str *next_atom; + unsigned short str_len; + char str_dat[FLEX_ARRAY]; /* more */ +}; + +struct tree_content; +struct tree_entry +{ + struct tree_content *tree; + struct atom_str* name; + struct tree_entry_ms + { + uint16_t mode; + unsigned char sha1[20]; + } versions[2]; +}; + +struct tree_content +{ + unsigned int entry_capacity; /* must match avail_tree_content */ + unsigned int entry_count; + unsigned int delta_depth; + struct tree_entry *entries[FLEX_ARRAY]; /* more */ +}; + +struct avail_tree_content +{ + unsigned int entry_capacity; /* must match tree_content */ + struct avail_tree_content *next_avail; +}; + +struct branch +{ + struct branch *table_next_branch; + struct branch *active_next_branch; + const char *name; + struct tree_entry branch_tree; + uintmax_t last_commit; + unsigned int pack_id; + unsigned char sha1[20]; +}; + +struct tag +{ + struct tag *next_tag; + const char *name; + unsigned int pack_id; + unsigned char sha1[20]; +}; + +struct dbuf +{ + void *buffer; + size_t capacity; +}; + +struct hash_list +{ + struct hash_list *next; + unsigned char sha1[20]; +}; + +typedef enum { + WHENSPEC_RAW = 1, + WHENSPEC_RFC2822, + WHENSPEC_NOW, +} whenspec_type; + +/* Configured limits on output */ +static unsigned long max_depth = 10; +static unsigned long max_packsize = (1LL << 32) - 1; +static int force_update; + +/* Stats and misc. counters */ +static uintmax_t alloc_count; +static uintmax_t marks_set_count; +static uintmax_t object_count_by_type[1 << TYPE_BITS]; +static uintmax_t duplicate_count_by_type[1 << TYPE_BITS]; +static uintmax_t delta_count_by_type[1 << TYPE_BITS]; +static unsigned long object_count; +static unsigned long branch_count; +static unsigned long branch_load_count; +static int failure; +static FILE *pack_edges; + +/* Memory pools */ +static size_t mem_pool_alloc = 2*1024*1024 - sizeof(struct mem_pool); +static size_t total_allocd; +static struct mem_pool *mem_pool; + +/* Atom management */ +static unsigned int atom_table_sz = 4451; +static unsigned int atom_cnt; +static struct atom_str **atom_table; + +/* The .pack file being generated */ +static unsigned int pack_id; +static struct packed_git *pack_data; +static struct packed_git **all_packs; +static unsigned long pack_size; + +/* Table of objects we've written. */ +static unsigned int object_entry_alloc = 5000; +static struct object_entry_pool *blocks; +static struct object_entry *object_table[1 << 16]; +static struct mark_set *marks; +static const char* mark_file; + +/* Our last blob */ +static struct last_object last_blob; + +/* Tree management */ +static unsigned int tree_entry_alloc = 1000; +static void *avail_tree_entry; +static unsigned int avail_tree_table_sz = 100; +static struct avail_tree_content **avail_tree_table; +static struct dbuf old_tree; +static struct dbuf new_tree; + +/* Branch data */ +static unsigned long max_active_branches = 5; +static unsigned long cur_active_branches; +static unsigned long branch_table_sz = 1039; +static struct branch **branch_table; +static struct branch *active_branches; + +/* Tag data */ +static struct tag *first_tag; +static struct tag *last_tag; + +/* Input stream parsing */ +static whenspec_type whenspec = WHENSPEC_RAW; +static struct strbuf command_buf; +static uintmax_t next_mark; +static struct dbuf new_data; + + +static void alloc_objects(unsigned int cnt) +{ + struct object_entry_pool *b; + + b = xmalloc(sizeof(struct object_entry_pool) + + cnt * sizeof(struct object_entry)); + b->next_pool = blocks; + b->next_free = b->entries; + b->end = b->entries + cnt; + blocks = b; + alloc_count += cnt; +} + +static struct object_entry *new_object(unsigned char *sha1) +{ + struct object_entry *e; + + if (blocks->next_free == blocks->end) + alloc_objects(object_entry_alloc); + + e = blocks->next_free++; + hashcpy(e->sha1, sha1); + return e; +} + +static struct object_entry *find_object(unsigned char *sha1) +{ + unsigned int h = sha1[0] << 8 | sha1[1]; + struct object_entry *e; + for (e = object_table[h]; e; e = e->next) + if (!hashcmp(sha1, e->sha1)) + return e; + return NULL; +} + +static struct object_entry *insert_object(unsigned char *sha1) +{ + unsigned int h = sha1[0] << 8 | sha1[1]; + struct object_entry *e = object_table[h]; + struct object_entry *p = NULL; + + while (e) { + if (!hashcmp(sha1, e->sha1)) + return e; + p = e; + e = e->next; + } + + e = new_object(sha1); + e->next = NULL; + e->offset = 0; + if (p) + p->next = e; + else + object_table[h] = e; + return e; +} + +static unsigned int hc_str(const char *s, size_t len) +{ + unsigned int r = 0; + while (len-- > 0) + r = r * 31 + *s++; + return r; +} + +static void *pool_alloc(size_t len) +{ + struct mem_pool *p; + void *r; + + for (p = mem_pool; p; p = p->next_pool) + if ((p->end - p->next_free >= len)) + break; + + if (!p) { + if (len >= (mem_pool_alloc/2)) { + total_allocd += len; + return xmalloc(len); + } + total_allocd += sizeof(struct mem_pool) + mem_pool_alloc; + p = xmalloc(sizeof(struct mem_pool) + mem_pool_alloc); + p->next_pool = mem_pool; + p->next_free = p->space; + p->end = p->next_free + mem_pool_alloc; + mem_pool = p; + } + + r = p->next_free; + /* round out to a pointer alignment */ + if (len & (sizeof(void*) - 1)) + len += sizeof(void*) - (len & (sizeof(void*) - 1)); + p->next_free += len; + return r; +} + +static void *pool_calloc(size_t count, size_t size) +{ + size_t len = count * size; + void *r = pool_alloc(len); + memset(r, 0, len); + return r; +} + +static char *pool_strdup(const char *s) +{ + char *r = pool_alloc(strlen(s) + 1); + strcpy(r, s); + return r; +} + +static void size_dbuf(struct dbuf *b, size_t maxlen) +{ + if (b->buffer) { + if (b->capacity >= maxlen) + return; + free(b->buffer); + } + b->capacity = ((maxlen / 1024) + 1) * 1024; + b->buffer = xmalloc(b->capacity); +} + +static void insert_mark(uintmax_t idnum, struct object_entry *oe) +{ + struct mark_set *s = marks; + while ((idnum >> s->shift) >= 1024) { + s = pool_calloc(1, sizeof(struct mark_set)); + s->shift = marks->shift + 10; + s->data.sets[0] = marks; + marks = s; + } + while (s->shift) { + uintmax_t i = idnum >> s->shift; + idnum -= i << s->shift; + if (!s->data.sets[i]) { + s->data.sets[i] = pool_calloc(1, sizeof(struct mark_set)); + s->data.sets[i]->shift = s->shift - 10; + } + s = s->data.sets[i]; + } + if (!s->data.marked[idnum]) + marks_set_count++; + s->data.marked[idnum] = oe; +} + +static struct object_entry *find_mark(uintmax_t idnum) +{ + uintmax_t orig_idnum = idnum; + struct mark_set *s = marks; + struct object_entry *oe = NULL; + if ((idnum >> s->shift) < 1024) { + while (s && s->shift) { + uintmax_t i = idnum >> s->shift; + idnum -= i << s->shift; + s = s->data.sets[i]; + } + if (s) + oe = s->data.marked[idnum]; + } + if (!oe) + die("mark :%ju not declared", orig_idnum); + return oe; +} + +static struct atom_str *to_atom(const char *s, unsigned short len) +{ + unsigned int hc = hc_str(s, len) % atom_table_sz; + struct atom_str *c; + + for (c = atom_table[hc]; c; c = c->next_atom) + if (c->str_len == len && !strncmp(s, c->str_dat, len)) + return c; + + c = pool_alloc(sizeof(struct atom_str) + len + 1); + c->str_len = len; + strncpy(c->str_dat, s, len); + c->str_dat[len] = 0; + c->next_atom = atom_table[hc]; + atom_table[hc] = c; + atom_cnt++; + return c; +} + +static struct branch *lookup_branch(const char *name) +{ + unsigned int hc = hc_str(name, strlen(name)) % branch_table_sz; + struct branch *b; + + for (b = branch_table[hc]; b; b = b->table_next_branch) + if (!strcmp(name, b->name)) + return b; + return NULL; +} + +static struct branch *new_branch(const char *name) +{ + unsigned int hc = hc_str(name, strlen(name)) % branch_table_sz; + struct branch* b = lookup_branch(name); + + if (b) + die("Invalid attempt to create duplicate branch: %s", name); + if (check_ref_format(name)) + die("Branch name doesn't conform to GIT standards: %s", name); + + b = pool_calloc(1, sizeof(struct branch)); + b->name = pool_strdup(name); + b->table_next_branch = branch_table[hc]; + b->branch_tree.versions[0].mode = S_IFDIR; + b->branch_tree.versions[1].mode = S_IFDIR; + b->pack_id = MAX_PACK_ID; + branch_table[hc] = b; + branch_count++; + return b; +} + +static unsigned int hc_entries(unsigned int cnt) +{ + cnt = cnt & 7 ? (cnt / 8) + 1 : cnt / 8; + return cnt < avail_tree_table_sz ? cnt : avail_tree_table_sz - 1; +} + +static struct tree_content *new_tree_content(unsigned int cnt) +{ + struct avail_tree_content *f, *l = NULL; + struct tree_content *t; + unsigned int hc = hc_entries(cnt); + + for (f = avail_tree_table[hc]; f; l = f, f = f->next_avail) + if (f->entry_capacity >= cnt) + break; + + if (f) { + if (l) + l->next_avail = f->next_avail; + else + avail_tree_table[hc] = f->next_avail; + } else { + cnt = cnt & 7 ? ((cnt / 8) + 1) * 8 : cnt; + f = pool_alloc(sizeof(*t) + sizeof(t->entries[0]) * cnt); + f->entry_capacity = cnt; + } + + t = (struct tree_content*)f; + t->entry_count = 0; + t->delta_depth = 0; + return t; +} + +static void release_tree_entry(struct tree_entry *e); +static void release_tree_content(struct tree_content *t) +{ + struct avail_tree_content *f = (struct avail_tree_content*)t; + unsigned int hc = hc_entries(f->entry_capacity); + f->next_avail = avail_tree_table[hc]; + avail_tree_table[hc] = f; +} + +static void release_tree_content_recursive(struct tree_content *t) +{ + unsigned int i; + for (i = 0; i < t->entry_count; i++) + release_tree_entry(t->entries[i]); + release_tree_content(t); +} + +static struct tree_content *grow_tree_content( + struct tree_content *t, + int amt) +{ + struct tree_content *r = new_tree_content(t->entry_count + amt); + r->entry_count = t->entry_count; + r->delta_depth = t->delta_depth; + memcpy(r->entries,t->entries,t->entry_count*sizeof(t->entries[0])); + release_tree_content(t); + return r; +} + +static struct tree_entry *new_tree_entry(void) +{ + struct tree_entry *e; + + if (!avail_tree_entry) { + unsigned int n = tree_entry_alloc; + total_allocd += n * sizeof(struct tree_entry); + avail_tree_entry = e = xmalloc(n * sizeof(struct tree_entry)); + while (n-- > 1) { + *((void**)e) = e + 1; + e++; + } + *((void**)e) = NULL; + } + + e = avail_tree_entry; + avail_tree_entry = *((void**)e); + return e; +} + +static void release_tree_entry(struct tree_entry *e) +{ + if (e->tree) + release_tree_content_recursive(e->tree); + *((void**)e) = avail_tree_entry; + avail_tree_entry = e; +} + +static void start_packfile(void) +{ + static char tmpfile[PATH_MAX]; + struct packed_git *p; + struct pack_header hdr; + int pack_fd; + + snprintf(tmpfile, sizeof(tmpfile), + "%s/pack_XXXXXX", get_object_directory()); + pack_fd = mkstemp(tmpfile); + if (pack_fd < 0) + die("Can't create %s: %s", tmpfile, strerror(errno)); + p = xcalloc(1, sizeof(*p) + strlen(tmpfile) + 2); + strcpy(p->pack_name, tmpfile); + p->pack_fd = pack_fd; + + hdr.hdr_signature = htonl(PACK_SIGNATURE); + hdr.hdr_version = htonl(2); + hdr.hdr_entries = 0; + write_or_die(p->pack_fd, &hdr, sizeof(hdr)); + + pack_data = p; + pack_size = sizeof(hdr); + object_count = 0; + + all_packs = xrealloc(all_packs, sizeof(*all_packs) * (pack_id + 1)); + all_packs[pack_id] = p; +} + +static void fixup_header_footer(void) +{ + static const int buf_sz = 128 * 1024; + int pack_fd = pack_data->pack_fd; + SHA_CTX c; + struct pack_header hdr; + char *buf; + + if (lseek(pack_fd, 0, SEEK_SET) != 0) + die("Failed seeking to start: %s", strerror(errno)); + if (read_in_full(pack_fd, &hdr, sizeof(hdr)) != sizeof(hdr)) + die("Unable to reread header of %s", pack_data->pack_name); + if (lseek(pack_fd, 0, SEEK_SET) != 0) + die("Failed seeking to start: %s", strerror(errno)); + hdr.hdr_entries = htonl(object_count); + write_or_die(pack_fd, &hdr, sizeof(hdr)); + + SHA1_Init(&c); + SHA1_Update(&c, &hdr, sizeof(hdr)); + + buf = xmalloc(buf_sz); + for (;;) { + size_t n = xread(pack_fd, buf, buf_sz); + if (!n) + break; + if (n < 0) + die("Failed to checksum %s", pack_data->pack_name); + SHA1_Update(&c, buf, n); + } + free(buf); + + SHA1_Final(pack_data->sha1, &c); + write_or_die(pack_fd, pack_data->sha1, sizeof(pack_data->sha1)); + close(pack_fd); +} + +static int oecmp (const void *a_, const void *b_) +{ + struct object_entry *a = *((struct object_entry**)a_); + struct object_entry *b = *((struct object_entry**)b_); + return hashcmp(a->sha1, b->sha1); +} + +static char *create_index(void) +{ + static char tmpfile[PATH_MAX]; + SHA_CTX ctx; + struct sha1file *f; + struct object_entry **idx, **c, **last, *e; + struct object_entry_pool *o; + uint32_t array[256]; + int i, idx_fd; + + /* Build the sorted table of object IDs. */ + idx = xmalloc(object_count * sizeof(struct object_entry*)); + c = idx; + for (o = blocks; o; o = o->next_pool) + for (e = o->next_free; e-- != o->entries;) + if (pack_id == e->pack_id) + *c++ = e; + last = idx + object_count; + if (c != last) + die("internal consistency error creating the index"); + qsort(idx, object_count, sizeof(struct object_entry*), oecmp); + + /* Generate the fan-out array. */ + c = idx; + for (i = 0; i < 256; i++) { + struct object_entry **next = c;; + while (next < last) { + if ((*next)->sha1[0] != i) + break; + next++; + } + array[i] = htonl(next - idx); + c = next; + } + + snprintf(tmpfile, sizeof(tmpfile), + "%s/index_XXXXXX", get_object_directory()); + idx_fd = mkstemp(tmpfile); + if (idx_fd < 0) + die("Can't create %s: %s", tmpfile, strerror(errno)); + f = sha1fd(idx_fd, tmpfile); + sha1write(f, array, 256 * sizeof(int)); + SHA1_Init(&ctx); + for (c = idx; c != last; c++) { + uint32_t offset = htonl((*c)->offset); + sha1write(f, &offset, 4); + sha1write(f, (*c)->sha1, sizeof((*c)->sha1)); + SHA1_Update(&ctx, (*c)->sha1, 20); + } + sha1write(f, pack_data->sha1, sizeof(pack_data->sha1)); + sha1close(f, NULL, 1); + free(idx); + SHA1_Final(pack_data->sha1, &ctx); + return tmpfile; +} + +static char *keep_pack(char *curr_index_name) +{ + static char name[PATH_MAX]; + static char *keep_msg = "fast-import"; + int keep_fd; + + chmod(pack_data->pack_name, 0444); + chmod(curr_index_name, 0444); + + snprintf(name, sizeof(name), "%s/pack/pack-%s.keep", + get_object_directory(), sha1_to_hex(pack_data->sha1)); + keep_fd = open(name, O_RDWR|O_CREAT|O_EXCL, 0600); + if (keep_fd < 0) + die("cannot create keep file"); + write(keep_fd, keep_msg, strlen(keep_msg)); + close(keep_fd); + + snprintf(name, sizeof(name), "%s/pack/pack-%s.pack", + get_object_directory(), sha1_to_hex(pack_data->sha1)); + if (move_temp_to_file(pack_data->pack_name, name)) + die("cannot store pack file"); + + snprintf(name, sizeof(name), "%s/pack/pack-%s.idx", + get_object_directory(), sha1_to_hex(pack_data->sha1)); + if (move_temp_to_file(curr_index_name, name)) + die("cannot store index file"); + return name; +} + +static void unkeep_all_packs(void) +{ + static char name[PATH_MAX]; + int k; + + for (k = 0; k < pack_id; k++) { + struct packed_git *p = all_packs[k]; + snprintf(name, sizeof(name), "%s/pack/pack-%s.keep", + get_object_directory(), sha1_to_hex(p->sha1)); + unlink(name); + } +} + +static void end_packfile(void) +{ + struct packed_git *old_p = pack_data, *new_p; + + if (object_count) { + char *idx_name; + int i; + struct branch *b; + struct tag *t; + + fixup_header_footer(); + idx_name = keep_pack(create_index()); + + /* Register the packfile with core git's machinary. */ + new_p = add_packed_git(idx_name, strlen(idx_name), 1); + if (!new_p) + die("core git rejected index %s", idx_name); + new_p->windows = old_p->windows; + all_packs[pack_id] = new_p; + install_packed_git(new_p); + + /* Print the boundary */ + if (pack_edges) { + fprintf(pack_edges, "%s:", new_p->pack_name); + for (i = 0; i < branch_table_sz; i++) { + for (b = branch_table[i]; b; b = b->table_next_branch) { + if (b->pack_id == pack_id) + fprintf(pack_edges, " %s", sha1_to_hex(b->sha1)); + } + } + for (t = first_tag; t; t = t->next_tag) { + if (t->pack_id == pack_id) + fprintf(pack_edges, " %s", sha1_to_hex(t->sha1)); + } + fputc('\n', pack_edges); + fflush(pack_edges); + } + + pack_id++; + } + else + unlink(old_p->pack_name); + free(old_p); + + /* We can't carry a delta across packfiles. */ + free(last_blob.data); + last_blob.data = NULL; + last_blob.len = 0; + last_blob.offset = 0; + last_blob.depth = 0; +} + +static void cycle_packfile(void) +{ + end_packfile(); + start_packfile(); +} + +static size_t encode_header( + enum object_type type, + size_t size, + unsigned char *hdr) +{ + int n = 1; + unsigned char c; + + if (type < OBJ_COMMIT || type > OBJ_REF_DELTA) + die("bad type %d", type); + + c = (type << 4) | (size & 15); + size >>= 4; + while (size) { + *hdr++ = c | 0x80; + c = size & 0x7f; + size >>= 7; + n++; + } + *hdr = c; + return n; +} + +static int store_object( + enum object_type type, + void *dat, + size_t datlen, + struct last_object *last, + unsigned char *sha1out, + uintmax_t mark) +{ + void *out, *delta; + struct object_entry *e; + unsigned char hdr[96]; + unsigned char sha1[20]; + unsigned long hdrlen, deltalen; + SHA_CTX c; + z_stream s; + + hdrlen = sprintf((char*)hdr,"%s %lu", type_names[type], + (unsigned long)datlen) + 1; + SHA1_Init(&c); + SHA1_Update(&c, hdr, hdrlen); + SHA1_Update(&c, dat, datlen); + SHA1_Final(sha1, &c); + if (sha1out) + hashcpy(sha1out, sha1); + + e = insert_object(sha1); + if (mark) + insert_mark(mark, e); + if (e->offset) { + duplicate_count_by_type[type]++; + return 1; + } + + if (last && last->data && last->depth < max_depth) { + delta = diff_delta(last->data, last->len, + dat, datlen, + &deltalen, 0); + if (delta && deltalen >= datlen) { + free(delta); + delta = NULL; + } + } else + delta = NULL; + + memset(&s, 0, sizeof(s)); + deflateInit(&s, zlib_compression_level); + if (delta) { + s.next_in = delta; + s.avail_in = deltalen; + } else { + s.next_in = dat; + s.avail_in = datlen; + } + s.avail_out = deflateBound(&s, s.avail_in); + s.next_out = out = xmalloc(s.avail_out); + while (deflate(&s, Z_FINISH) == Z_OK) + /* nothing */; + deflateEnd(&s); + + /* Determine if we should auto-checkpoint. */ + if ((pack_size + 60 + s.total_out) > max_packsize + || (pack_size + 60 + s.total_out) < pack_size) { + + /* This new object needs to *not* have the current pack_id. */ + e->pack_id = pack_id + 1; + cycle_packfile(); + + /* We cannot carry a delta into the new pack. */ + if (delta) { + free(delta); + delta = NULL; + + memset(&s, 0, sizeof(s)); + deflateInit(&s, zlib_compression_level); + s.next_in = dat; + s.avail_in = datlen; + s.avail_out = deflateBound(&s, s.avail_in); + s.next_out = out = xrealloc(out, s.avail_out); + while (deflate(&s, Z_FINISH) == Z_OK) + /* nothing */; + deflateEnd(&s); + } + } + + e->type = type; + e->pack_id = pack_id; + e->offset = pack_size; + object_count++; + object_count_by_type[type]++; + + if (delta) { + unsigned long ofs = e->offset - last->offset; + unsigned pos = sizeof(hdr) - 1; + + delta_count_by_type[type]++; + last->depth++; + + hdrlen = encode_header(OBJ_OFS_DELTA, deltalen, hdr); + write_or_die(pack_data->pack_fd, hdr, hdrlen); + pack_size += hdrlen; + + hdr[pos] = ofs & 127; + while (ofs >>= 7) + hdr[--pos] = 128 | (--ofs & 127); + write_or_die(pack_data->pack_fd, hdr + pos, sizeof(hdr) - pos); + pack_size += sizeof(hdr) - pos; + } else { + if (last) + last->depth = 0; + hdrlen = encode_header(type, datlen, hdr); + write_or_die(pack_data->pack_fd, hdr, hdrlen); + pack_size += hdrlen; + } + + write_or_die(pack_data->pack_fd, out, s.total_out); + pack_size += s.total_out; + + free(out); + free(delta); + if (last) { + if (!last->no_free) + free(last->data); + last->data = dat; + last->offset = e->offset; + last->len = datlen; + } + return 0; +} + +static void *gfi_unpack_entry( + struct object_entry *oe, + unsigned long *sizep) +{ + static char type[20]; + struct packed_git *p = all_packs[oe->pack_id]; + if (p == pack_data) + p->pack_size = pack_size + 20; + return unpack_entry(p, oe->offset, type, sizep); +} + +static const char *get_mode(const char *str, uint16_t *modep) +{ + unsigned char c; + uint16_t mode = 0; + + while ((c = *str++) != ' ') { + if (c < '0' || c > '7') + return NULL; + mode = (mode << 3) + (c - '0'); + } + *modep = mode; + return str; +} + +static void load_tree(struct tree_entry *root) +{ + unsigned char* sha1 = root->versions[1].sha1; + struct object_entry *myoe; + struct tree_content *t; + unsigned long size; + char *buf; + const char *c; + + root->tree = t = new_tree_content(8); + if (is_null_sha1(sha1)) + return; + + myoe = find_object(sha1); + if (myoe) { + if (myoe->type != OBJ_TREE) + die("Not a tree: %s", sha1_to_hex(sha1)); + t->delta_depth = 0; + buf = gfi_unpack_entry(myoe, &size); + } else { + char type[20]; + buf = read_sha1_file(sha1, type, &size); + if (!buf || strcmp(type, tree_type)) + die("Can't load tree %s", sha1_to_hex(sha1)); + } + + c = buf; + while (c != (buf + size)) { + struct tree_entry *e = new_tree_entry(); + + if (t->entry_count == t->entry_capacity) + root->tree = t = grow_tree_content(t, 8); + t->entries[t->entry_count++] = e; + + e->tree = NULL; + c = get_mode(c, &e->versions[1].mode); + if (!c) + die("Corrupt mode in %s", sha1_to_hex(sha1)); + e->versions[0].mode = e->versions[1].mode; + e->name = to_atom(c, (unsigned short)strlen(c)); + c += e->name->str_len + 1; + hashcpy(e->versions[0].sha1, (unsigned char*)c); + hashcpy(e->versions[1].sha1, (unsigned char*)c); + c += 20; + } + free(buf); +} + +static int tecmp0 (const void *_a, const void *_b) +{ + struct tree_entry *a = *((struct tree_entry**)_a); + struct tree_entry *b = *((struct tree_entry**)_b); + return base_name_compare( + a->name->str_dat, a->name->str_len, a->versions[0].mode, + b->name->str_dat, b->name->str_len, b->versions[0].mode); +} + +static int tecmp1 (const void *_a, const void *_b) +{ + struct tree_entry *a = *((struct tree_entry**)_a); + struct tree_entry *b = *((struct tree_entry**)_b); + return base_name_compare( + a->name->str_dat, a->name->str_len, a->versions[1].mode, + b->name->str_dat, b->name->str_len, b->versions[1].mode); +} + +static void mktree(struct tree_content *t, + int v, + unsigned long *szp, + struct dbuf *b) +{ + size_t maxlen = 0; + unsigned int i; + char *c; + + if (!v) + qsort(t->entries,t->entry_count,sizeof(t->entries[0]),tecmp0); + else + qsort(t->entries,t->entry_count,sizeof(t->entries[0]),tecmp1); + + for (i = 0; i < t->entry_count; i++) { + if (t->entries[i]->versions[v].mode) + maxlen += t->entries[i]->name->str_len + 34; + } + + size_dbuf(b, maxlen); + c = b->buffer; + for (i = 0; i < t->entry_count; i++) { + struct tree_entry *e = t->entries[i]; + if (!e->versions[v].mode) + continue; + c += sprintf(c, "%o", (unsigned int)e->versions[v].mode); + *c++ = ' '; + strcpy(c, e->name->str_dat); + c += e->name->str_len + 1; + hashcpy((unsigned char*)c, e->versions[v].sha1); + c += 20; + } + *szp = c - (char*)b->buffer; +} + +static void store_tree(struct tree_entry *root) +{ + struct tree_content *t = root->tree; + unsigned int i, j, del; + unsigned long new_len; + struct last_object lo; + struct object_entry *le; + + if (!is_null_sha1(root->versions[1].sha1)) + return; + + for (i = 0; i < t->entry_count; i++) { + if (t->entries[i]->tree) + store_tree(t->entries[i]); + } + + le = find_object(root->versions[0].sha1); + if (!S_ISDIR(root->versions[0].mode) + || !le + || le->pack_id != pack_id) { + lo.data = NULL; + lo.depth = 0; + } else { + mktree(t, 0, &lo.len, &old_tree); + lo.data = old_tree.buffer; + lo.offset = le->offset; + lo.depth = t->delta_depth; + lo.no_free = 1; + } + + mktree(t, 1, &new_len, &new_tree); + store_object(OBJ_TREE, new_tree.buffer, new_len, + &lo, root->versions[1].sha1, 0); + + t->delta_depth = lo.depth; + for (i = 0, j = 0, del = 0; i < t->entry_count; i++) { + struct tree_entry *e = t->entries[i]; + if (e->versions[1].mode) { + e->versions[0].mode = e->versions[1].mode; + hashcpy(e->versions[0].sha1, e->versions[1].sha1); + t->entries[j++] = e; + } else { + release_tree_entry(e); + del++; + } + } + t->entry_count -= del; +} + +static int tree_content_set( + struct tree_entry *root, + const char *p, + const unsigned char *sha1, + const uint16_t mode) +{ + struct tree_content *t = root->tree; + const char *slash1; + unsigned int i, n; + struct tree_entry *e; + + slash1 = strchr(p, '/'); + if (slash1) + n = slash1 - p; + else + n = strlen(p); + + for (i = 0; i < t->entry_count; i++) { + e = t->entries[i]; + if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) { + if (!slash1) { + if (e->versions[1].mode == mode + && !hashcmp(e->versions[1].sha1, sha1)) + return 0; + e->versions[1].mode = mode; + hashcpy(e->versions[1].sha1, sha1); + if (e->tree) { + release_tree_content_recursive(e->tree); + e->tree = NULL; + } + hashclr(root->versions[1].sha1); + return 1; + } + if (!S_ISDIR(e->versions[1].mode)) { + e->tree = new_tree_content(8); + e->versions[1].mode = S_IFDIR; + } + if (!e->tree) + load_tree(e); + if (tree_content_set(e, slash1 + 1, sha1, mode)) { + hashclr(root->versions[1].sha1); + return 1; + } + return 0; + } + } + + if (t->entry_count == t->entry_capacity) + root->tree = t = grow_tree_content(t, 8); + e = new_tree_entry(); + e->name = to_atom(p, (unsigned short)n); + e->versions[0].mode = 0; + hashclr(e->versions[0].sha1); + t->entries[t->entry_count++] = e; + if (slash1) { + e->tree = new_tree_content(8); + e->versions[1].mode = S_IFDIR; + tree_content_set(e, slash1 + 1, sha1, mode); + } else { + e->tree = NULL; + e->versions[1].mode = mode; + hashcpy(e->versions[1].sha1, sha1); + } + hashclr(root->versions[1].sha1); + return 1; +} + +static int tree_content_remove(struct tree_entry *root, const char *p) +{ + struct tree_content *t = root->tree; + const char *slash1; + unsigned int i, n; + struct tree_entry *e; + + slash1 = strchr(p, '/'); + if (slash1) + n = slash1 - p; + else + n = strlen(p); + + for (i = 0; i < t->entry_count; i++) { + e = t->entries[i]; + if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) { + if (!slash1 || !S_ISDIR(e->versions[1].mode)) + goto del_entry; + if (!e->tree) + load_tree(e); + if (tree_content_remove(e, slash1 + 1)) { + for (n = 0; n < e->tree->entry_count; n++) { + if (e->tree->entries[n]->versions[1].mode) { + hashclr(root->versions[1].sha1); + return 1; + } + } + goto del_entry; + } + return 0; + } + } + return 0; + +del_entry: + if (e->tree) { + release_tree_content_recursive(e->tree); + e->tree = NULL; + } + e->versions[1].mode = 0; + hashclr(e->versions[1].sha1); + hashclr(root->versions[1].sha1); + return 1; +} + +static int update_branch(struct branch *b) +{ + static const char *msg = "fast-import"; + struct ref_lock *lock; + unsigned char old_sha1[20]; + + if (read_ref(b->name, old_sha1)) + hashclr(old_sha1); + lock = lock_any_ref_for_update(b->name, old_sha1); + if (!lock) + return error("Unable to lock %s", b->name); + if (!force_update && !is_null_sha1(old_sha1)) { + struct commit *old_cmit, *new_cmit; + + old_cmit = lookup_commit_reference_gently(old_sha1, 0); + new_cmit = lookup_commit_reference_gently(b->sha1, 0); + if (!old_cmit || !new_cmit) { + unlock_ref(lock); + return error("Branch %s is missing commits.", b->name); + } + + if (!in_merge_bases(old_cmit, new_cmit)) { + unlock_ref(lock); + warn("Not updating %s" + " (new tip %s does not contain %s)", + b->name, sha1_to_hex(b->sha1), sha1_to_hex(old_sha1)); + return -1; + } + } + if (write_ref_sha1(lock, b->sha1, msg) < 0) + return error("Unable to update %s", b->name); + return 0; +} + +static void dump_branches(void) +{ + unsigned int i; + struct branch *b; + + for (i = 0; i < branch_table_sz; i++) { + for (b = branch_table[i]; b; b = b->table_next_branch) + failure |= update_branch(b); + } +} + +static void dump_tags(void) +{ + static const char *msg = "fast-import"; + struct tag *t; + struct ref_lock *lock; + char ref_name[PATH_MAX]; + + for (t = first_tag; t; t = t->next_tag) { + sprintf(ref_name, "tags/%s", t->name); + lock = lock_ref_sha1(ref_name, NULL); + if (!lock || write_ref_sha1(lock, t->sha1, msg) < 0) + failure |= error("Unable to update %s", ref_name); + } +} + +static void dump_marks_helper(FILE *f, + uintmax_t base, + struct mark_set *m) +{ + uintmax_t k; + if (m->shift) { + for (k = 0; k < 1024; k++) { + if (m->data.sets[k]) + dump_marks_helper(f, (base + k) << m->shift, + m->data.sets[k]); + } + } else { + for (k = 0; k < 1024; k++) { + if (m->data.marked[k]) + fprintf(f, ":%ju %s\n", base + k, + sha1_to_hex(m->data.marked[k]->sha1)); + } + } +} + +static void dump_marks(void) +{ + if (mark_file) + { + FILE *f = fopen(mark_file, "w"); + if (f) { + dump_marks_helper(f, 0, marks); + fclose(f); + } else + failure |= error("Unable to write marks file %s: %s", + mark_file, strerror(errno)); + } +} + +static void read_next_command(void) +{ + read_line(&command_buf, stdin, '\n'); +} + +static void cmd_mark(void) +{ + if (!strncmp("mark :", command_buf.buf, 6)) { + next_mark = strtoumax(command_buf.buf + 6, NULL, 10); + read_next_command(); + } + else + next_mark = 0; +} + +static void *cmd_data (size_t *size) +{ + size_t length; + char *buffer; + + if (strncmp("data ", command_buf.buf, 5)) + die("Expected 'data n' command, found: %s", command_buf.buf); + + if (!strncmp("<<", command_buf.buf + 5, 2)) { + char *term = xstrdup(command_buf.buf + 5 + 2); + size_t sz = 8192, term_len = command_buf.len - 5 - 2; + length = 0; + buffer = xmalloc(sz); + for (;;) { + read_next_command(); + if (command_buf.eof) + die("EOF in data (terminator '%s' not found)", term); + if (term_len == command_buf.len + && !strcmp(term, command_buf.buf)) + break; + if (sz < (length + command_buf.len)) { + sz = sz * 3 / 2 + 16; + if (sz < (length + command_buf.len)) + sz = length + command_buf.len; + buffer = xrealloc(buffer, sz); + } + memcpy(buffer + length, + command_buf.buf, + command_buf.len - 1); + length += command_buf.len - 1; + buffer[length++] = '\n'; + } + free(term); + } + else { + size_t n = 0; + length = strtoul(command_buf.buf + 5, NULL, 10); + buffer = xmalloc(length); + while (n < length) { + size_t s = fread(buffer + n, 1, length - n, stdin); + if (!s && feof(stdin)) + die("EOF in data (%lu bytes remaining)", + (unsigned long)(length - n)); + n += s; + } + } + + if (fgetc(stdin) != '\n') + die("An lf did not trail the binary data as expected."); + + *size = length; + return buffer; +} + +static int validate_raw_date(const char *src, char *result, int maxlen) +{ + const char *orig_src = src; + char *endp, sign; + + strtoul(src, &endp, 10); + if (endp == src || *endp != ' ') + return -1; + + src = endp + 1; + if (*src != '-' && *src != '+') + return -1; + sign = *src; + + strtoul(src + 1, &endp, 10); + if (endp == src || *endp || (endp - orig_src) >= maxlen) + return -1; + + strcpy(result, orig_src); + return 0; +} + +static char *parse_ident(const char *buf) +{ + const char *gt; + size_t name_len; + char *ident; + + gt = strrchr(buf, '>'); + if (!gt) + die("Missing > in ident string: %s", buf); + gt++; + if (*gt != ' ') + die("Missing space after > in ident string: %s", buf); + gt++; + name_len = gt - buf; + ident = xmalloc(name_len + 24); + strncpy(ident, buf, name_len); + + switch (whenspec) { + case WHENSPEC_RAW: + if (validate_raw_date(gt, ident + name_len, 24) < 0) + die("Invalid raw date \"%s\" in ident: %s", gt, buf); + break; + case WHENSPEC_RFC2822: + if (parse_date(gt, ident + name_len, 24) < 0) + die("Invalid rfc2822 date \"%s\" in ident: %s", gt, buf); + break; + case WHENSPEC_NOW: + if (strcmp("now", gt)) + die("Date in ident must be 'now': %s", buf); + datestamp(ident + name_len, 24); + break; + } + + return ident; +} + +static void cmd_new_blob(void) +{ + size_t l; + void *d; + + read_next_command(); + cmd_mark(); + d = cmd_data(&l); + + if (store_object(OBJ_BLOB, d, l, &last_blob, NULL, next_mark)) + free(d); +} + +static void unload_one_branch(void) +{ + while (cur_active_branches + && cur_active_branches >= max_active_branches) { + unsigned long min_commit = ULONG_MAX; + struct branch *e, *l = NULL, *p = NULL; + + for (e = active_branches; e; e = e->active_next_branch) { + if (e->last_commit < min_commit) { + p = l; + min_commit = e->last_commit; + } + l = e; + } + + if (p) { + e = p->active_next_branch; + p->active_next_branch = e->active_next_branch; + } else { + e = active_branches; + active_branches = e->active_next_branch; + } + e->active_next_branch = NULL; + if (e->branch_tree.tree) { + release_tree_content_recursive(e->branch_tree.tree); + e->branch_tree.tree = NULL; + } + cur_active_branches--; + } +} + +static void load_branch(struct branch *b) +{ + load_tree(&b->branch_tree); + b->active_next_branch = active_branches; + active_branches = b; + cur_active_branches++; + branch_load_count++; +} + +static void file_change_m(struct branch *b) +{ + const char *p = command_buf.buf + 2; + char *p_uq; + const char *endp; + struct object_entry *oe = oe; + unsigned char sha1[20]; + uint16_t mode, inline_data = 0; + char type[20]; + + p = get_mode(p, &mode); + if (!p) + die("Corrupt mode: %s", command_buf.buf); + switch (mode) { + case S_IFREG | 0644: + case S_IFREG | 0755: + case S_IFLNK: + case 0644: + case 0755: + /* ok */ + break; + default: + die("Corrupt mode: %s", command_buf.buf); + } + + if (*p == ':') { + char *x; + oe = find_mark(strtoumax(p + 1, &x, 10)); + hashcpy(sha1, oe->sha1); + p = x; + } else if (!strncmp("inline", p, 6)) { + inline_data = 1; + p += 6; + } else { + if (get_sha1_hex(p, sha1)) + die("Invalid SHA1: %s", command_buf.buf); + oe = find_object(sha1); + p += 40; + } + if (*p++ != ' ') + die("Missing space after SHA1: %s", command_buf.buf); + + p_uq = unquote_c_style(p, &endp); + if (p_uq) { + if (*endp) + die("Garbage after path in: %s", command_buf.buf); + p = p_uq; + } + + if (inline_data) { + size_t l; + void *d; + if (!p_uq) + p = p_uq = xstrdup(p); + read_next_command(); + d = cmd_data(&l); + if (store_object(OBJ_BLOB, d, l, &last_blob, sha1, 0)) + free(d); + } else if (oe) { + if (oe->type != OBJ_BLOB) + die("Not a blob (actually a %s): %s", + command_buf.buf, type_names[oe->type]); + } else { + if (sha1_object_info(sha1, type, NULL)) + die("Blob not found: %s", command_buf.buf); + if (strcmp(blob_type, type)) + die("Not a blob (actually a %s): %s", + command_buf.buf, type); + } + + tree_content_set(&b->branch_tree, p, sha1, S_IFREG | mode); + free(p_uq); +} + +static void file_change_d(struct branch *b) +{ + const char *p = command_buf.buf + 2; + char *p_uq; + const char *endp; + + p_uq = unquote_c_style(p, &endp); + if (p_uq) { + if (*endp) + die("Garbage after path in: %s", command_buf.buf); + p = p_uq; + } + tree_content_remove(&b->branch_tree, p); + free(p_uq); +} + +static void file_change_deleteall(struct branch *b) +{ + release_tree_content_recursive(b->branch_tree.tree); + hashclr(b->branch_tree.versions[0].sha1); + hashclr(b->branch_tree.versions[1].sha1); + load_tree(&b->branch_tree); +} + +static void cmd_from(struct branch *b) +{ + const char *from; + struct branch *s; + + if (strncmp("from ", command_buf.buf, 5)) + return; + + if (b->last_commit) + die("Can't reinitailize branch %s", b->name); + + from = strchr(command_buf.buf, ' ') + 1; + s = lookup_branch(from); + if (b == s) + die("Can't create a branch from itself: %s", b->name); + else if (s) { + unsigned char *t = s->branch_tree.versions[1].sha1; + hashcpy(b->sha1, s->sha1); + hashcpy(b->branch_tree.versions[0].sha1, t); + hashcpy(b->branch_tree.versions[1].sha1, t); + } else if (*from == ':') { + uintmax_t idnum = strtoumax(from + 1, NULL, 10); + struct object_entry *oe = find_mark(idnum); + unsigned long size; + char *buf; + if (oe->type != OBJ_COMMIT) + die("Mark :%ju not a commit", idnum); + hashcpy(b->sha1, oe->sha1); + buf = gfi_unpack_entry(oe, &size); + if (!buf || size < 46) + die("Not a valid commit: %s", from); + if (memcmp("tree ", buf, 5) + || get_sha1_hex(buf + 5, b->branch_tree.versions[1].sha1)) + die("The commit %s is corrupt", sha1_to_hex(b->sha1)); + free(buf); + hashcpy(b->branch_tree.versions[0].sha1, + b->branch_tree.versions[1].sha1); + } else if (!get_sha1(from, b->sha1)) { + if (is_null_sha1(b->sha1)) { + hashclr(b->branch_tree.versions[0].sha1); + hashclr(b->branch_tree.versions[1].sha1); + } else { + unsigned long size; + char *buf; + + buf = read_object_with_reference(b->sha1, + type_names[OBJ_COMMIT], &size, b->sha1); + if (!buf || size < 46) + die("Not a valid commit: %s", from); + if (memcmp("tree ", buf, 5) + || get_sha1_hex(buf + 5, b->branch_tree.versions[1].sha1)) + die("The commit %s is corrupt", sha1_to_hex(b->sha1)); + free(buf); + hashcpy(b->branch_tree.versions[0].sha1, + b->branch_tree.versions[1].sha1); + } + } else + die("Invalid ref name or SHA1 expression: %s", from); + + read_next_command(); +} + +static struct hash_list *cmd_merge(unsigned int *count) +{ + struct hash_list *list = NULL, *n, *e = e; + const char *from; + struct branch *s; + + *count = 0; + while (!strncmp("merge ", command_buf.buf, 6)) { + from = strchr(command_buf.buf, ' ') + 1; + n = xmalloc(sizeof(*n)); + s = lookup_branch(from); + if (s) + hashcpy(n->sha1, s->sha1); + else if (*from == ':') { + uintmax_t idnum = strtoumax(from + 1, NULL, 10); + struct object_entry *oe = find_mark(idnum); + if (oe->type != OBJ_COMMIT) + die("Mark :%ju not a commit", idnum); + hashcpy(n->sha1, oe->sha1); + } else if (get_sha1(from, n->sha1)) + die("Invalid ref name or SHA1 expression: %s", from); + + n->next = NULL; + if (list) + e->next = n; + else + list = n; + e = n; + (*count)++; + read_next_command(); + } + return list; +} + +static void cmd_new_commit(void) +{ + struct branch *b; + void *msg; + size_t msglen; + char *sp; + char *author = NULL; + char *committer = NULL; + struct hash_list *merge_list = NULL; + unsigned int merge_count; + + /* Obtain the branch name from the rest of our command */ + sp = strchr(command_buf.buf, ' ') + 1; + b = lookup_branch(sp); + if (!b) + b = new_branch(sp); + + read_next_command(); + cmd_mark(); + if (!strncmp("author ", command_buf.buf, 7)) { + author = parse_ident(command_buf.buf + 7); + read_next_command(); + } + if (!strncmp("committer ", command_buf.buf, 10)) { + committer = parse_ident(command_buf.buf + 10); + read_next_command(); + } + if (!committer) + die("Expected committer but didn't get one"); + msg = cmd_data(&msglen); + read_next_command(); + cmd_from(b); + merge_list = cmd_merge(&merge_count); + + /* ensure the branch is active/loaded */ + if (!b->branch_tree.tree || !max_active_branches) { + unload_one_branch(); + load_branch(b); + } + + /* file_change* */ + for (;;) { + if (1 == command_buf.len) + break; + else if (!strncmp("M ", command_buf.buf, 2)) + file_change_m(b); + else if (!strncmp("D ", command_buf.buf, 2)) + file_change_d(b); + else if (!strcmp("deleteall", command_buf.buf)) + file_change_deleteall(b); + else + die("Unsupported file_change: %s", command_buf.buf); + read_next_command(); + } + + /* build the tree and the commit */ + store_tree(&b->branch_tree); + hashcpy(b->branch_tree.versions[0].sha1, + b->branch_tree.versions[1].sha1); + size_dbuf(&new_data, 114 + msglen + + merge_count * 49 + + (author + ? strlen(author) + strlen(committer) + : 2 * strlen(committer))); + sp = new_data.buffer; + sp += sprintf(sp, "tree %s\n", + sha1_to_hex(b->branch_tree.versions[1].sha1)); + if (!is_null_sha1(b->sha1)) + sp += sprintf(sp, "parent %s\n", sha1_to_hex(b->sha1)); + while (merge_list) { + struct hash_list *next = merge_list->next; + sp += sprintf(sp, "parent %s\n", sha1_to_hex(merge_list->sha1)); + free(merge_list); + merge_list = next; + } + sp += sprintf(sp, "author %s\n", author ? author : committer); + sp += sprintf(sp, "committer %s\n", committer); + *sp++ = '\n'; + memcpy(sp, msg, msglen); + sp += msglen; + free(author); + free(committer); + free(msg); + + if (!store_object(OBJ_COMMIT, + new_data.buffer, sp - (char*)new_data.buffer, + NULL, b->sha1, next_mark)) + b->pack_id = pack_id; + b->last_commit = object_count_by_type[OBJ_COMMIT]; +} + +static void cmd_new_tag(void) +{ + char *sp; + const char *from; + char *tagger; + struct branch *s; + void *msg; + size_t msglen; + struct tag *t; + uintmax_t from_mark = 0; + unsigned char sha1[20]; + + /* Obtain the new tag name from the rest of our command */ + sp = strchr(command_buf.buf, ' ') + 1; + t = pool_alloc(sizeof(struct tag)); + t->next_tag = NULL; + t->name = pool_strdup(sp); + if (last_tag) + last_tag->next_tag = t; + else + first_tag = t; + last_tag = t; + read_next_command(); + + /* from ... */ + if (strncmp("from ", command_buf.buf, 5)) + die("Expected from command, got %s", command_buf.buf); + from = strchr(command_buf.buf, ' ') + 1; + s = lookup_branch(from); + if (s) { + hashcpy(sha1, s->sha1); + } else if (*from == ':') { + struct object_entry *oe; + from_mark = strtoumax(from + 1, NULL, 10); + oe = find_mark(from_mark); + if (oe->type != OBJ_COMMIT) + die("Mark :%ju not a commit", from_mark); + hashcpy(sha1, oe->sha1); + } else if (!get_sha1(from, sha1)) { + unsigned long size; + char *buf; + + buf = read_object_with_reference(sha1, + type_names[OBJ_COMMIT], &size, sha1); + if (!buf || size < 46) + die("Not a valid commit: %s", from); + free(buf); + } else + die("Invalid ref name or SHA1 expression: %s", from); + read_next_command(); + + /* tagger ... */ + if (strncmp("tagger ", command_buf.buf, 7)) + die("Expected tagger command, got %s", command_buf.buf); + tagger = parse_ident(command_buf.buf + 7); + + /* tag payload/message */ + read_next_command(); + msg = cmd_data(&msglen); + + /* build the tag object */ + size_dbuf(&new_data, 67+strlen(t->name)+strlen(tagger)+msglen); + sp = new_data.buffer; + sp += sprintf(sp, "object %s\n", sha1_to_hex(sha1)); + sp += sprintf(sp, "type %s\n", type_names[OBJ_COMMIT]); + sp += sprintf(sp, "tag %s\n", t->name); + sp += sprintf(sp, "tagger %s\n", tagger); + *sp++ = '\n'; + memcpy(sp, msg, msglen); + sp += msglen; + free(tagger); + free(msg); + + if (store_object(OBJ_TAG, new_data.buffer, + sp - (char*)new_data.buffer, + NULL, t->sha1, 0)) + t->pack_id = MAX_PACK_ID; + else + t->pack_id = pack_id; +} + +static void cmd_reset_branch(void) +{ + struct branch *b; + char *sp; + + /* Obtain the branch name from the rest of our command */ + sp = strchr(command_buf.buf, ' ') + 1; + b = lookup_branch(sp); + if (b) { + b->last_commit = 0; + if (b->branch_tree.tree) { + release_tree_content_recursive(b->branch_tree.tree); + b->branch_tree.tree = NULL; + } + } + else + b = new_branch(sp); + read_next_command(); + cmd_from(b); +} + +static void cmd_checkpoint(void) +{ + if (object_count) { + cycle_packfile(); + dump_branches(); + dump_tags(); + dump_marks(); + } + read_next_command(); +} + +static const char fast_import_usage[] = +"git-fast-import [--date-format=f] [--max-pack-size=n] [--depth=n] [--active-branches=n] [--export-marks=marks.file]"; + +int main(int argc, const char **argv) +{ + int i, show_stats = 1; + + git_config(git_default_config); + + for (i = 1; i < argc; i++) { + const char *a = argv[i]; + + if (*a != '-' || !strcmp(a, "--")) + break; + else if (!strncmp(a, "--date-format=", 14)) { + const char *fmt = a + 14; + if (!strcmp(fmt, "raw")) + whenspec = WHENSPEC_RAW; + else if (!strcmp(fmt, "rfc2822")) + whenspec = WHENSPEC_RFC2822; + else if (!strcmp(fmt, "now")) + whenspec = WHENSPEC_NOW; + else + die("unknown --date-format argument %s", fmt); + } + else if (!strncmp(a, "--max-pack-size=", 16)) + max_packsize = strtoumax(a + 16, NULL, 0) * 1024 * 1024; + else if (!strncmp(a, "--depth=", 8)) + max_depth = strtoul(a + 8, NULL, 0); + else if (!strncmp(a, "--active-branches=", 18)) + max_active_branches = strtoul(a + 18, NULL, 0); + else if (!strncmp(a, "--export-marks=", 15)) + mark_file = a + 15; + else if (!strncmp(a, "--export-pack-edges=", 20)) { + if (pack_edges) + fclose(pack_edges); + pack_edges = fopen(a + 20, "a"); + if (!pack_edges) + die("Cannot open %s: %s", a + 20, strerror(errno)); + } else if (!strcmp(a, "--force")) + force_update = 1; + else if (!strcmp(a, "--quiet")) + show_stats = 0; + else if (!strcmp(a, "--stats")) + show_stats = 1; + else + die("unknown option %s", a); + } + if (i != argc) + usage(fast_import_usage); + + alloc_objects(object_entry_alloc); + strbuf_init(&command_buf); + + atom_table = xcalloc(atom_table_sz, sizeof(struct atom_str*)); + branch_table = xcalloc(branch_table_sz, sizeof(struct branch*)); + avail_tree_table = xcalloc(avail_tree_table_sz, sizeof(struct avail_tree_content*)); + marks = pool_calloc(1, sizeof(struct mark_set)); + + start_packfile(); + for (;;) { + read_next_command(); + if (command_buf.eof) + break; + else if (!strcmp("blob", command_buf.buf)) + cmd_new_blob(); + else if (!strncmp("commit ", command_buf.buf, 7)) + cmd_new_commit(); + else if (!strncmp("tag ", command_buf.buf, 4)) + cmd_new_tag(); + else if (!strncmp("reset ", command_buf.buf, 6)) + cmd_reset_branch(); + else if (!strcmp("checkpoint", command_buf.buf)) + cmd_checkpoint(); + else + die("Unsupported command: %s", command_buf.buf); + } + end_packfile(); + + dump_branches(); + dump_tags(); + unkeep_all_packs(); + dump_marks(); + + if (pack_edges) + fclose(pack_edges); + + if (show_stats) { + uintmax_t total_count = 0, duplicate_count = 0; + for (i = 0; i < ARRAY_SIZE(object_count_by_type); i++) + total_count += object_count_by_type[i]; + for (i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++) + duplicate_count += duplicate_count_by_type[i]; + + fprintf(stderr, "%s statistics:\n", argv[0]); + fprintf(stderr, "---------------------------------------------------------------------\n"); + fprintf(stderr, "Alloc'd objects: %10ju\n", alloc_count); + fprintf(stderr, "Total objects: %10ju (%10ju duplicates )\n", total_count, duplicate_count); + fprintf(stderr, " blobs : %10ju (%10ju duplicates %10ju deltas)\n", object_count_by_type[OBJ_BLOB], duplicate_count_by_type[OBJ_BLOB], delta_count_by_type[OBJ_BLOB]); + fprintf(stderr, " trees : %10ju (%10ju duplicates %10ju deltas)\n", object_count_by_type[OBJ_TREE], duplicate_count_by_type[OBJ_TREE], delta_count_by_type[OBJ_TREE]); + fprintf(stderr, " commits: %10ju (%10ju duplicates %10ju deltas)\n", object_count_by_type[OBJ_COMMIT], duplicate_count_by_type[OBJ_COMMIT], delta_count_by_type[OBJ_COMMIT]); + fprintf(stderr, " tags : %10ju (%10ju duplicates %10ju deltas)\n", object_count_by_type[OBJ_TAG], duplicate_count_by_type[OBJ_TAG], delta_count_by_type[OBJ_TAG]); + fprintf(stderr, "Total branches: %10lu (%10lu loads )\n", branch_count, branch_load_count); + fprintf(stderr, " marks: %10ju (%10ju unique )\n", (((uintmax_t)1) << marks->shift) * 1024, marks_set_count); + fprintf(stderr, " atoms: %10u\n", atom_cnt); + fprintf(stderr, "Memory total: %10ju KiB\n", (total_allocd + alloc_count*sizeof(struct object_entry))/1024); + fprintf(stderr, " pools: %10lu KiB\n", (unsigned long)(total_allocd/1024)); + fprintf(stderr, " objects: %10ju KiB\n", (alloc_count*sizeof(struct object_entry))/1024); + fprintf(stderr, "---------------------------------------------------------------------\n"); + pack_report(); + fprintf(stderr, "---------------------------------------------------------------------\n"); + fprintf(stderr, "\n"); + } + + return failure ? 1 : 0; +} diff --git a/fetch-pack.c b/fetch-pack.c new file mode 100644 index 0000000000..c787106764 --- /dev/null +++ b/fetch-pack.c @@ -0,0 +1,783 @@ +#include "cache.h" +#include "refs.h" +#include "pkt-line.h" +#include "commit.h" +#include "tag.h" +#include "exec_cmd.h" +#include "pack.h" +#include "sideband.h" + +static int keep_pack; +static int transfer_unpack_limit = -1; +static int fetch_unpack_limit = -1; +static int unpack_limit = 100; +static int quiet; +static int verbose; +static int fetch_all; +static int depth; +static const char fetch_pack_usage[] = +"git-fetch-pack [--all] [--quiet|-q] [--keep|-k] [--thin] [--upload-pack=<git-upload-pack>] [--depth=<n>] [-v] [<host>:]<directory> [<refs>...]"; +static const char *uploadpack = "git-upload-pack"; + +#define COMPLETE (1U << 0) +#define COMMON (1U << 1) +#define COMMON_REF (1U << 2) +#define SEEN (1U << 3) +#define POPPED (1U << 4) + +/* + * After sending this many "have"s if we do not get any new ACK , we + * give up traversing our history. + */ +#define MAX_IN_VAIN 256 + +static struct commit_list *rev_list; +static int non_common_revs, multi_ack, use_thin_pack, use_sideband; + +static void rev_list_push(struct commit *commit, int mark) +{ + if (!(commit->object.flags & mark)) { + commit->object.flags |= mark; + + if (!(commit->object.parsed)) + parse_commit(commit); + + insert_by_date(commit, &rev_list); + + if (!(commit->object.flags & COMMON)) + non_common_revs++; + } +} + +static int rev_list_insert_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + struct object *o = deref_tag(parse_object(sha1), path, 0); + + if (o && o->type == OBJ_COMMIT) + rev_list_push((struct commit *)o, SEEN); + + return 0; +} + +/* + This function marks a rev and its ancestors as common. + In some cases, it is desirable to mark only the ancestors (for example + when only the server does not yet know that they are common). +*/ + +static void mark_common(struct commit *commit, + int ancestors_only, int dont_parse) +{ + if (commit != NULL && !(commit->object.flags & COMMON)) { + struct object *o = (struct object *)commit; + + if (!ancestors_only) + o->flags |= COMMON; + + if (!(o->flags & SEEN)) + rev_list_push(commit, SEEN); + else { + struct commit_list *parents; + + if (!ancestors_only && !(o->flags & POPPED)) + non_common_revs--; + if (!o->parsed && !dont_parse) + parse_commit(commit); + + for (parents = commit->parents; + parents; + parents = parents->next) + mark_common(parents->item, 0, dont_parse); + } + } +} + +/* + Get the next rev to send, ignoring the common. +*/ + +static const unsigned char* get_rev(void) +{ + struct commit *commit = NULL; + + while (commit == NULL) { + unsigned int mark; + struct commit_list* parents; + + if (rev_list == NULL || non_common_revs == 0) + return NULL; + + commit = rev_list->item; + if (!(commit->object.parsed)) + parse_commit(commit); + commit->object.flags |= POPPED; + if (!(commit->object.flags & COMMON)) + non_common_revs--; + + parents = commit->parents; + + if (commit->object.flags & COMMON) { + /* do not send "have", and ignore ancestors */ + commit = NULL; + mark = COMMON | SEEN; + } else if (commit->object.flags & COMMON_REF) + /* send "have", and ignore ancestors */ + mark = COMMON | SEEN; + else + /* send "have", also for its ancestors */ + mark = SEEN; + + while (parents) { + if (!(parents->item->object.flags & SEEN)) + rev_list_push(parents->item, mark); + if (mark & COMMON) + mark_common(parents->item, 1, 0); + parents = parents->next; + } + + rev_list = rev_list->next; + } + + return commit->object.sha1; +} + +static int find_common(int fd[2], unsigned char *result_sha1, + struct ref *refs) +{ + int fetching; + int count = 0, flushes = 0, retval; + const unsigned char *sha1; + unsigned in_vain = 0; + int got_continue = 0; + + for_each_ref(rev_list_insert_ref, NULL); + + fetching = 0; + for ( ; refs ; refs = refs->next) { + unsigned char *remote = refs->old_sha1; + struct object *o; + + /* + * If that object is complete (i.e. it is an ancestor of a + * local ref), we tell them we have it but do not have to + * tell them about its ancestors, which they already know + * about. + * + * We use lookup_object here because we are only + * interested in the case we *know* the object is + * reachable and we have already scanned it. + */ + if (((o = lookup_object(remote)) != NULL) && + (o->flags & COMPLETE)) { + continue; + } + + if (!fetching) + packet_write(fd[1], "want %s%s%s%s%s%s\n", + sha1_to_hex(remote), + (multi_ack ? " multi_ack" : ""), + (use_sideband == 2 ? " side-band-64k" : ""), + (use_sideband == 1 ? " side-band" : ""), + (use_thin_pack ? " thin-pack" : ""), + " ofs-delta"); + else + packet_write(fd[1], "want %s\n", sha1_to_hex(remote)); + fetching++; + } + if (is_repository_shallow()) + write_shallow_commits(fd[1], 1); + if (depth > 0) + packet_write(fd[1], "deepen %d", depth); + packet_flush(fd[1]); + if (!fetching) + return 1; + + if (depth > 0) { + char line[1024]; + unsigned char sha1[20]; + int len; + + while ((len = packet_read_line(fd[0], line, sizeof(line)))) { + if (!strncmp("shallow ", line, 8)) { + if (get_sha1_hex(line + 8, sha1)) + die("invalid shallow line: %s", line); + register_shallow(sha1); + continue; + } + if (!strncmp("unshallow ", line, 10)) { + if (get_sha1_hex(line + 10, sha1)) + die("invalid unshallow line: %s", line); + if (!lookup_object(sha1)) + die("object not found: %s", line); + /* make sure that it is parsed as shallow */ + parse_object(sha1); + if (unregister_shallow(sha1)) + die("no shallow found: %s", line); + continue; + } + die("expected shallow/unshallow, got %s", line); + } + } + + flushes = 0; + retval = -1; + while ((sha1 = get_rev())) { + packet_write(fd[1], "have %s\n", sha1_to_hex(sha1)); + if (verbose) + fprintf(stderr, "have %s\n", sha1_to_hex(sha1)); + in_vain++; + if (!(31 & ++count)) { + int ack; + + packet_flush(fd[1]); + flushes++; + + /* + * We keep one window "ahead" of the other side, and + * will wait for an ACK only on the next one + */ + if (count == 32) + continue; + + do { + ack = get_ack(fd[0], result_sha1); + if (verbose && ack) + fprintf(stderr, "got ack %d %s\n", ack, + sha1_to_hex(result_sha1)); + if (ack == 1) { + flushes = 0; + multi_ack = 0; + retval = 0; + goto done; + } else if (ack == 2) { + struct commit *commit = + lookup_commit(result_sha1); + mark_common(commit, 0, 1); + retval = 0; + in_vain = 0; + got_continue = 1; + } + } while (ack); + flushes--; + if (got_continue && MAX_IN_VAIN < in_vain) { + if (verbose) + fprintf(stderr, "giving up\n"); + break; /* give up */ + } + } + } +done: + packet_write(fd[1], "done\n"); + if (verbose) + fprintf(stderr, "done\n"); + if (retval != 0) { + multi_ack = 0; + flushes++; + } + while (flushes || multi_ack) { + int ack = get_ack(fd[0], result_sha1); + if (ack) { + if (verbose) + fprintf(stderr, "got ack (%d) %s\n", ack, + sha1_to_hex(result_sha1)); + if (ack == 1) + return 0; + multi_ack = 1; + continue; + } + flushes--; + } + return retval; +} + +static struct commit_list *complete; + +static int mark_complete(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + struct object *o = parse_object(sha1); + + while (o && o->type == OBJ_TAG) { + struct tag *t = (struct tag *) o; + if (!t->tagged) + break; /* broken repository */ + o->flags |= COMPLETE; + o = parse_object(t->tagged->sha1); + } + if (o && o->type == OBJ_COMMIT) { + struct commit *commit = (struct commit *)o; + commit->object.flags |= COMPLETE; + insert_by_date(commit, &complete); + } + return 0; +} + +static void mark_recent_complete_commits(unsigned long cutoff) +{ + while (complete && cutoff <= complete->item->date) { + if (verbose) + fprintf(stderr, "Marking %s as complete\n", + sha1_to_hex(complete->item->object.sha1)); + pop_most_recent_commit(&complete, COMPLETE); + } +} + +static void filter_refs(struct ref **refs, int nr_match, char **match) +{ + struct ref **return_refs; + struct ref *newlist = NULL; + struct ref **newtail = &newlist; + struct ref *ref, *next; + struct ref *fastarray[32]; + + if (nr_match && !fetch_all) { + if (ARRAY_SIZE(fastarray) < nr_match) + return_refs = xcalloc(nr_match, sizeof(struct ref *)); + else { + return_refs = fastarray; + memset(return_refs, 0, sizeof(struct ref *) * nr_match); + } + } + else + return_refs = NULL; + + for (ref = *refs; ref; ref = next) { + next = ref->next; + if (!memcmp(ref->name, "refs/", 5) && + check_ref_format(ref->name + 5)) + ; /* trash */ + else if (fetch_all && + (!depth || strncmp(ref->name, "refs/tags/", 10) )) { + *newtail = ref; + ref->next = NULL; + newtail = &ref->next; + continue; + } + else { + int order = path_match(ref->name, nr_match, match); + if (order) { + return_refs[order-1] = ref; + continue; /* we will link it later */ + } + } + free(ref); + } + + if (!fetch_all) { + int i; + for (i = 0; i < nr_match; i++) { + ref = return_refs[i]; + if (ref) { + *newtail = ref; + ref->next = NULL; + newtail = &ref->next; + } + } + if (return_refs != fastarray) + free(return_refs); + } + *refs = newlist; +} + +static int everything_local(struct ref **refs, int nr_match, char **match) +{ + struct ref *ref; + int retval; + unsigned long cutoff = 0; + + track_object_refs = 0; + save_commit_buffer = 0; + + for (ref = *refs; ref; ref = ref->next) { + struct object *o; + + o = parse_object(ref->old_sha1); + if (!o) + continue; + + /* We already have it -- which may mean that we were + * in sync with the other side at some time after + * that (it is OK if we guess wrong here). + */ + if (o->type == OBJ_COMMIT) { + struct commit *commit = (struct commit *)o; + if (!cutoff || cutoff < commit->date) + cutoff = commit->date; + } + } + + if (!depth) { + for_each_ref(mark_complete, NULL); + if (cutoff) + mark_recent_complete_commits(cutoff); + } + + /* + * Mark all complete remote refs as common refs. + * Don't mark them common yet; the server has to be told so first. + */ + for (ref = *refs; ref; ref = ref->next) { + struct object *o = deref_tag(lookup_object(ref->old_sha1), + NULL, 0); + + if (!o || o->type != OBJ_COMMIT || !(o->flags & COMPLETE)) + continue; + + if (!(o->flags & SEEN)) { + rev_list_push((struct commit *)o, COMMON_REF | SEEN); + + mark_common((struct commit *)o, 1, 1); + } + } + + filter_refs(refs, nr_match, match); + + for (retval = 1, ref = *refs; ref ; ref = ref->next) { + const unsigned char *remote = ref->old_sha1; + unsigned char local[20]; + struct object *o; + + o = lookup_object(remote); + if (!o || !(o->flags & COMPLETE)) { + retval = 0; + if (!verbose) + continue; + fprintf(stderr, + "want %s (%s)\n", sha1_to_hex(remote), + ref->name); + continue; + } + + hashcpy(ref->new_sha1, local); + if (!verbose) + continue; + fprintf(stderr, + "already have %s (%s)\n", sha1_to_hex(remote), + ref->name); + } + return retval; +} + +static pid_t setup_sideband(int fd[2], int xd[2]) +{ + pid_t side_pid; + + if (!use_sideband) { + fd[0] = xd[0]; + fd[1] = xd[1]; + return 0; + } + /* xd[] is talking with upload-pack; subprocess reads from + * xd[0], spits out band#2 to stderr, and feeds us band#1 + * through our fd[0]. + */ + if (pipe(fd) < 0) + die("fetch-pack: unable to set up pipe"); + side_pid = fork(); + if (side_pid < 0) + die("fetch-pack: unable to fork off sideband demultiplexer"); + if (!side_pid) { + /* subprocess */ + close(fd[0]); + if (xd[0] != xd[1]) + close(xd[1]); + if (recv_sideband("fetch-pack", xd[0], fd[1], 2)) + exit(1); + exit(0); + } + close(xd[0]); + close(fd[1]); + fd[1] = xd[1]; + return side_pid; +} + +static int get_pack(int xd[2]) +{ + int status; + pid_t pid, side_pid; + int fd[2]; + const char *argv[20]; + char keep_arg[256]; + char hdr_arg[256]; + const char **av; + int do_keep = keep_pack; + + side_pid = setup_sideband(fd, xd); + + av = argv; + *hdr_arg = 0; + if (unpack_limit) { + struct pack_header header; + + if (read_pack_header(fd[0], &header)) + die("protocol error: bad pack header"); + snprintf(hdr_arg, sizeof(hdr_arg), "--pack_header=%u,%u", + ntohl(header.hdr_version), ntohl(header.hdr_entries)); + if (ntohl(header.hdr_entries) < unpack_limit) + do_keep = 0; + else + do_keep = 1; + } + + if (do_keep) { + *av++ = "index-pack"; + *av++ = "--stdin"; + if (!quiet) + *av++ = "-v"; + if (use_thin_pack) + *av++ = "--fix-thin"; + if (keep_pack > 1 || unpack_limit) { + int s = sprintf(keep_arg, + "--keep=fetch-pack %d on ", getpid()); + if (gethostname(keep_arg + s, sizeof(keep_arg) - s)) + strcpy(keep_arg + s, "localhost"); + *av++ = keep_arg; + } + } + else { + *av++ = "unpack-objects"; + if (quiet) + *av++ = "-q"; + } + if (*hdr_arg) + *av++ = hdr_arg; + *av++ = NULL; + + pid = fork(); + if (pid < 0) + die("fetch-pack: unable to fork off %s", argv[0]); + if (!pid) { + dup2(fd[0], 0); + close(fd[0]); + close(fd[1]); + execv_git_cmd(argv); + die("%s exec failed", argv[0]); + } + close(fd[0]); + close(fd[1]); + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) + die("waiting for %s: %s", argv[0], strerror(errno)); + } + if (WIFEXITED(status)) { + int code = WEXITSTATUS(status); + if (code) + die("%s died with error code %d", argv[0], code); + return 0; + } + if (WIFSIGNALED(status)) { + int sig = WTERMSIG(status); + die("%s died of signal %d", argv[0], sig); + } + die("%s died of unnatural causes %d", argv[0], status); +} + +static int fetch_pack(int fd[2], int nr_match, char **match) +{ + struct ref *ref; + unsigned char sha1[20]; + + get_remote_heads(fd[0], &ref, 0, NULL, 0); + if (is_repository_shallow() && !server_supports("shallow")) + die("Server does not support shallow clients"); + if (server_supports("multi_ack")) { + if (verbose) + fprintf(stderr, "Server supports multi_ack\n"); + multi_ack = 1; + } + if (server_supports("side-band-64k")) { + if (verbose) + fprintf(stderr, "Server supports side-band-64k\n"); + use_sideband = 2; + } + else if (server_supports("side-band")) { + if (verbose) + fprintf(stderr, "Server supports side-band\n"); + use_sideband = 1; + } + if (!ref) { + packet_flush(fd[1]); + die("no matching remote head"); + } + if (everything_local(&ref, nr_match, match)) { + packet_flush(fd[1]); + goto all_done; + } + if (find_common(fd, sha1, ref) < 0) + if (keep_pack != 1) + /* When cloning, it is not unusual to have + * no common commit. + */ + fprintf(stderr, "warning: no common commits\n"); + + if (get_pack(fd)) + die("git-fetch-pack: fetch failed."); + + all_done: + while (ref) { + printf("%s %s\n", + sha1_to_hex(ref->old_sha1), ref->name); + ref = ref->next; + } + return 0; +} + +static int remove_duplicates(int nr_heads, char **heads) +{ + int src, dst; + + for (src = dst = 0; src < nr_heads; src++) { + /* If heads[src] is different from any of + * heads[0..dst], push it in. + */ + int i; + for (i = 0; i < dst; i++) { + if (!strcmp(heads[i], heads[src])) + break; + } + if (i < dst) + continue; + if (src != dst) + heads[dst] = heads[src]; + dst++; + } + heads[dst] = 0; + return dst; +} + +static int fetch_pack_config(const char *var, const char *value) +{ + if (strcmp(var, "fetch.unpacklimit") == 0) { + fetch_unpack_limit = git_config_int(var, value); + return 0; + } + + if (strcmp(var, "transfer.unpacklimit") == 0) { + transfer_unpack_limit = git_config_int(var, value); + return 0; + } + + return git_default_config(var, value); +} + +static struct lock_file lock; + +int main(int argc, char **argv) +{ + int i, ret, nr_heads; + char *dest = NULL, **heads; + int fd[2]; + pid_t pid; + struct stat st; + + setup_git_directory(); + git_config(fetch_pack_config); + + if (0 <= transfer_unpack_limit) + unpack_limit = transfer_unpack_limit; + else if (0 <= fetch_unpack_limit) + unpack_limit = fetch_unpack_limit; + + nr_heads = 0; + heads = NULL; + for (i = 1; i < argc; i++) { + char *arg = argv[i]; + + if (*arg == '-') { + if (!strncmp("--upload-pack=", arg, 14)) { + uploadpack = arg + 14; + continue; + } + if (!strncmp("--exec=", arg, 7)) { + uploadpack = arg + 7; + continue; + } + if (!strcmp("--quiet", arg) || !strcmp("-q", arg)) { + quiet = 1; + continue; + } + if (!strcmp("--keep", arg) || !strcmp("-k", arg)) { + keep_pack++; + unpack_limit = 0; + continue; + } + if (!strcmp("--thin", arg)) { + use_thin_pack = 1; + continue; + } + if (!strcmp("--all", arg)) { + fetch_all = 1; + continue; + } + if (!strcmp("-v", arg)) { + verbose = 1; + continue; + } + if (!strncmp("--depth=", arg, 8)) { + depth = strtol(arg + 8, NULL, 0); + if (stat(git_path("shallow"), &st)) + st.st_mtime = 0; + continue; + } + usage(fetch_pack_usage); + } + dest = arg; + heads = argv + i + 1; + nr_heads = argc - i - 1; + break; + } + if (!dest) + usage(fetch_pack_usage); + pid = git_connect(fd, dest, uploadpack); + if (pid < 0) + return 1; + if (heads && nr_heads) + nr_heads = remove_duplicates(nr_heads, heads); + ret = fetch_pack(fd, nr_heads, heads); + close(fd[0]); + close(fd[1]); + ret |= finish_connect(pid); + + if (!ret && nr_heads) { + /* If the heads to pull were given, we should have + * consumed all of them by matching the remote. + * Otherwise, 'git-fetch remote no-such-ref' would + * silently succeed without issuing an error. + */ + for (i = 0; i < nr_heads; i++) + if (heads[i] && heads[i][0]) { + error("no such remote ref %s", heads[i]); + ret = 1; + } + } + + if (!ret && depth > 0) { + struct cache_time mtime; + char *shallow = git_path("shallow"); + int fd; + + mtime.sec = st.st_mtime; +#ifdef USE_NSEC + mtime.usec = st.st_mtim.usec; +#endif + if (stat(shallow, &st)) { + if (mtime.sec) + die("shallow file was removed during fetch"); + } else if (st.st_mtime != mtime.sec +#ifdef USE_NSEC + || st.st_mtim.usec != mtime.usec +#endif + ) + die("shallow file was changed during fetch"); + + fd = hold_lock_file_for_update(&lock, shallow, 1); + if (!write_shallow_commits(fd, 0)) { + unlink(shallow); + rollback_lock_file(&lock); + } else { + close(fd); + commit_lock_file(&lock); + } + } + + return !!ret; +} diff --git a/fetch.c b/fetch.c new file mode 100644 index 0000000000..f69be82f10 --- /dev/null +++ b/fetch.c @@ -0,0 +1,315 @@ +#include "cache.h" +#include "fetch.h" +#include "commit.h" +#include "tree.h" +#include "tree-walk.h" +#include "tag.h" +#include "blob.h" +#include "refs.h" +#include "strbuf.h" + +int get_tree = 0; +int get_history = 0; +int get_all = 0; +int get_verbosely = 0; +int get_recover = 0; +static unsigned char current_commit_sha1[20]; + +void pull_say(const char *fmt, const char *hex) +{ + if (get_verbosely) + fprintf(stderr, fmt, hex); +} + +static void report_missing(const struct object *obj) +{ + char missing_hex[41]; + strcpy(missing_hex, sha1_to_hex(obj->sha1));; + fprintf(stderr, "Cannot obtain needed %s %s\n", + obj->type ? typename(obj->type): "object", missing_hex); + if (!is_null_sha1(current_commit_sha1)) + fprintf(stderr, "while processing commit %s.\n", + sha1_to_hex(current_commit_sha1)); +} + +static int process(struct object *obj); + +static int process_tree(struct tree *tree) +{ + struct tree_desc desc; + struct name_entry entry; + + if (parse_tree(tree)) + return -1; + + 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(tree->buffer); + tree->buffer = NULL; + tree->size = 0; + return 0; +} + +#define COMPLETE (1U << 0) +#define SEEN (1U << 1) +#define TO_SCAN (1U << 2) + +static struct commit_list *complete = NULL; + +static int process_commit(struct commit *commit) +{ + if (parse_commit(commit)) + return -1; + + while (complete && complete->item->date >= commit->date) { + pop_most_recent_commit(&complete, COMPLETE); + } + + if (commit->object.flags & COMPLETE) + return 0; + + hashcpy(current_commit_sha1, commit->object.sha1); + + pull_say("walk %s\n", sha1_to_hex(commit->object.sha1)); + + if (get_tree) { + if (process(&commit->tree->object)) + return -1; + if (!get_all) + get_tree = 0; + } + if (get_history) { + struct commit_list *parents = commit->parents; + for (; parents; parents = parents->next) { + if (process(&parents->item->object)) + return -1; + } + } + return 0; +} + +static int process_tag(struct tag *tag) +{ + if (parse_tag(tag)) + return -1; + return process(tag->tagged); +} + +static struct object_list *process_queue = NULL; +static struct object_list **process_queue_end = &process_queue; + +static int process_object(struct object *obj) +{ + if (obj->type == OBJ_COMMIT) { + if (process_commit((struct commit *)obj)) + return -1; + return 0; + } + if (obj->type == OBJ_TREE) { + if (process_tree((struct tree *)obj)) + return -1; + return 0; + } + if (obj->type == OBJ_BLOB) { + return 0; + } + if (obj->type == OBJ_TAG) { + if (process_tag((struct tag *)obj)) + return -1; + return 0; + } + return error("Unable to determine requirements " + "of type %s for %s", + typename(obj->type), sha1_to_hex(obj->sha1)); +} + +static int process(struct object *obj) +{ + if (obj->flags & SEEN) + return 0; + obj->flags |= SEEN; + + if (has_sha1_file(obj->sha1)) { + /* We already have it, so we should scan it now. */ + obj->flags |= TO_SCAN; + } + else { + if (obj->flags & COMPLETE) + return 0; + prefetch(obj->sha1); + } + + object_list_insert(obj, process_queue_end); + process_queue_end = &(*process_queue_end)->next; + return 0; +} + +static int loop(void) +{ + struct object_list *elem; + + while (process_queue) { + struct object *obj = process_queue->item; + elem = process_queue; + process_queue = elem->next; + free(elem); + if (!process_queue) + process_queue_end = &process_queue; + + /* If we are not scanning this object, we placed it in + * the queue because we needed to fetch it first. + */ + if (! (obj->flags & TO_SCAN)) { + if (fetch(obj->sha1)) { + report_missing(obj); + return -1; + } + } + if (!obj->type) + parse_object(obj->sha1); + if (process_object(obj)) + return -1; + } + return 0; +} + +static int interpret_target(char *target, unsigned char *sha1) +{ + if (!get_sha1_hex(target, sha1)) + return 0; + if (!check_ref_format(target)) { + if (!fetch_ref(target, sha1)) { + return 0; + } + } + return -1; +} + +static int mark_complete(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + struct commit *commit = lookup_commit_reference_gently(sha1, 1); + if (commit) { + commit->object.flags |= COMPLETE; + insert_by_date(commit, &complete); + } + return 0; +} + +int pull_targets_stdin(char ***target, const char ***write_ref) +{ + int targets = 0, targets_alloc = 0; + struct strbuf buf; + *target = NULL; *write_ref = NULL; + strbuf_init(&buf); + while (1) { + char *rf_one = NULL; + char *tg_one; + + read_line(&buf, stdin, '\n'); + if (buf.eof) + break; + tg_one = buf.buf; + rf_one = strchr(tg_one, '\t'); + if (rf_one) + *rf_one++ = 0; + + if (targets >= targets_alloc) { + targets_alloc = targets_alloc ? targets_alloc * 2 : 64; + *target = xrealloc(*target, targets_alloc * sizeof(**target)); + *write_ref = xrealloc(*write_ref, targets_alloc * sizeof(**write_ref)); + } + (*target)[targets] = xstrdup(tg_one); + (*write_ref)[targets] = rf_one ? xstrdup(rf_one) : NULL; + targets++; + } + return targets; +} + +void pull_targets_free(int targets, char **target, const char **write_ref) +{ + while (targets--) { + free(target[targets]); + if (write_ref && write_ref[targets]) + free((char *) write_ref[targets]); + } +} + +int pull(int targets, char **target, const char **write_ref, + const char *write_ref_log_details) +{ + struct ref_lock **lock = xcalloc(targets, sizeof(struct ref_lock *)); + unsigned char *sha1 = xmalloc(targets * 20); + char *msg; + int ret; + int i; + + save_commit_buffer = 0; + track_object_refs = 0; + + for (i = 0; i < targets; i++) { + if (!write_ref || !write_ref[i]) + continue; + + lock[i] = lock_ref_sha1(write_ref[i], NULL); + if (!lock[i]) { + error("Can't lock ref %s", write_ref[i]); + goto unlock_and_fail; + } + } + + if (!get_recover) + for_each_ref(mark_complete, NULL); + + for (i = 0; i < targets; i++) { + if (interpret_target(target[i], &sha1[20 * i])) { + error("Could not interpret %s as something to pull", target[i]); + goto unlock_and_fail; + } + if (process(lookup_unknown_object(&sha1[20 * i]))) + goto unlock_and_fail; + } + + if (loop()) + goto unlock_and_fail; + + 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; + } + for (i = 0; i < targets; i++) { + if (!write_ref || !write_ref[i]) + continue; + ret = write_ref_sha1(lock[i], &sha1[20 * i], msg ? msg : "fetch (unknown)"); + lock[i] = NULL; + if (ret) + goto unlock_and_fail; + } + free(msg); + + return 0; + + +unlock_and_fail: + for (i = 0; i < targets; i++) + if (lock[i]) + unlock_ref(lock[i]); + return -1; +} diff --git a/fetch.h b/fetch.h new file mode 100644 index 0000000000..be48c6f190 --- /dev/null +++ b/fetch.h @@ -0,0 +1,54 @@ +#ifndef PULL_H +#define PULL_H + +/* + * Fetch object given SHA1 from the remote, and store it locally under + * GIT_OBJECT_DIRECTORY. Return 0 on success, -1 on failure. To be + * provided by the particular implementation. + */ +extern int fetch(unsigned char *sha1); + +/* + * Fetch the specified object and store it locally; fetch() will be + * called later to determine success. To be provided by the particular + * implementation. + */ +extern void prefetch(unsigned char *sha1); + +/* + * Fetch ref (relative to $GIT_DIR/refs) from the remote, and store + * the 20-byte SHA1 in sha1. Return 0 on success, -1 on failure. To + * be provided by the particular implementation. + */ +extern int fetch_ref(char *ref, unsigned char *sha1); + +/* Set to fetch the target tree. */ +extern int get_tree; + +/* Set to fetch the commit history. */ +extern int get_history; + +/* Set to fetch the trees in the commit history. */ +extern int get_all; + +/* Set to be verbose */ +extern int get_verbosely; + +/* Set to check on all reachable objects. */ +extern int get_recover; + +/* Report what we got under get_verbosely */ +extern void pull_say(const char *, const char *); + +/* Load pull targets from stdin */ +extern int pull_targets_stdin(char ***target, const char ***write_ref); + +/* Free up loaded targets */ +extern void pull_targets_free(int targets, char **target, const char **write_ref); + +/* If write_ref is set, the ref filename to write the target value to. */ +/* If write_ref_log_details is set, additional text will appear in the ref log. */ +extern int pull(int targets, char **target, const char **write_ref, + const char *write_ref_log_details); + +#endif /* PULL_H */ diff --git a/generate-cmdlist.sh b/generate-cmdlist.sh new file mode 100755 index 0000000000..975777f05e --- /dev/null +++ b/generate-cmdlist.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +echo "/* Automatically generated by $0 */ +struct cmdname_help +{ + char name[16]; + char help[80]; +}; + +struct cmdname_help common_cmds[] = {" + +sort <<\EOF | +add +apply +archive +bisect +branch +checkout +cherry-pick +clone +commit +diff +fetch +grep +init +log +merge +mv +prune +pull +push +rebase +reset +revert +rm +show +show-branch +status +tag +EOF +while read cmd +do + sed -n ' + /NAME/,/git-'"$cmd"'/H + ${ + x + s/.*git-'"$cmd"' - \(.*\)/ {"'"$cmd"'", "\1"},/ + p + }' "Documentation/git-$cmd.txt" +done +echo "};" diff --git a/git-add--interactive.perl b/git-add--interactive.perl new file mode 100755 index 0000000000..dc3038091d --- /dev/null +++ b/git-add--interactive.perl @@ -0,0 +1,803 @@ +#!/usr/bin/perl -w + +use strict; + +sub run_cmd_pipe { + my $fh = undef; + open($fh, '-|', @_) or die; + return <$fh>; +} + +my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir)); + +if (!defined $GIT_DIR) { + exit(1); # rev-parse would have already said "not a git repo" +} +chomp($GIT_DIR); + +sub refresh { + my $fh; + open $fh, '-|', qw(git update-index --refresh) + or die; + while (<$fh>) { + ;# ignore 'needs update' + } + close $fh; +} + +sub list_untracked { + map { + chomp $_; + $_; + } + run_cmd_pipe(qw(git ls-files --others + --exclude-per-directory=.gitignore), + "--exclude-from=$GIT_DIR/info/exclude", + '--', @_); +} + +my $status_fmt = '%12s %12s %s'; +my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path'); + +# Returns list of hashes, contents of each of which are: +# PRINT: print message +# VALUE: pathname +# BINARY: is a binary path +# INDEX: is index different from HEAD? +# FILE: is file different from index? +# INDEX_ADDDEL: is it add/delete between HEAD and index? +# FILE_ADDDEL: is it add/delete between index and file? + +sub list_modified { + my ($only) = @_; + my (%data, @return); + my ($add, $del, $adddel, $file); + + for (run_cmd_pipe(qw(git diff-index --cached + --numstat --summary HEAD))) { + if (($add, $del, $file) = + /^([-\d]+) ([-\d]+) (.*)/) { + my ($change, $bin); + if ($add eq '-' && $del eq '-') { + $change = 'binary'; + $bin = 1; + } + else { + $change = "+$add/-$del"; + } + $data{$file} = { + INDEX => $change, + BINARY => $bin, + FILE => 'nothing', + } + } + elsif (($adddel, $file) = + /^ (create|delete) mode [0-7]+ (.*)$/) { + $data{$file}{INDEX_ADDDEL} = $adddel; + } + } + + for (run_cmd_pipe(qw(git diff-files --numstat --summary))) { + if (($add, $del, $file) = + /^([-\d]+) ([-\d]+) (.*)/) { + if (!exists $data{$file}) { + $data{$file} = +{ + INDEX => 'unchanged', + BINARY => 0, + }; + } + my ($change, $bin); + if ($add eq '-' && $del eq '-') { + $change = 'binary'; + $bin = 1; + } + else { + $change = "+$add/-$del"; + } + $data{$file}{FILE} = $change; + if ($bin) { + $data{$file}{BINARY} = 1; + } + } + elsif (($adddel, $file) = + /^ (create|delete) mode [0-7]+ (.*)$/) { + $data{$file}{FILE_ADDDEL} = $adddel; + } + } + + for (sort keys %data) { + my $it = $data{$_}; + + if ($only) { + if ($only eq 'index-only') { + next if ($it->{INDEX} eq 'unchanged'); + } + if ($only eq 'file-only') { + next if ($it->{FILE} eq 'nothing'); + } + } + push @return, +{ + VALUE => $_, + PRINT => (sprintf $status_fmt, + $it->{INDEX}, $it->{FILE}, $_), + %$it, + }; + } + return @return; +} + +sub find_unique { + my ($string, @stuff) = @_; + my $found = undef; + for (my $i = 0; $i < @stuff; $i++) { + my $it = $stuff[$i]; + my $hit = undef; + if (ref $it) { + if ((ref $it) eq 'ARRAY') { + $it = $it->[0]; + } + else { + $it = $it->{VALUE}; + } + } + eval { + if ($it =~ /^$string/) { + $hit = 1; + }; + }; + if (defined $hit && defined $found) { + return undef; + } + if ($hit) { + $found = $i + 1; + } + } + return $found; +} + +sub list_and_choose { + my ($opts, @stuff) = @_; + my (@chosen, @return); + my $i; + + TOPLOOP: + while (1) { + my $last_lf = 0; + + if ($opts->{HEADER}) { + if (!$opts->{LIST_FLAT}) { + print " "; + } + print "$opts->{HEADER}\n"; + } + for ($i = 0; $i < @stuff; $i++) { + my $chosen = $chosen[$i] ? '*' : ' '; + my $print = $stuff[$i]; + if (ref $print) { + if ((ref $print) eq 'ARRAY') { + $print = $print->[0]; + } + else { + $print = $print->{PRINT}; + } + } + printf("%s%2d: %s", $chosen, $i+1, $print); + if (($opts->{LIST_FLAT}) && + (($i + 1) % ($opts->{LIST_FLAT}))) { + print "\t"; + $last_lf = 0; + } + else { + print "\n"; + $last_lf = 1; + } + } + if (!$last_lf) { + print "\n"; + } + + return if ($opts->{LIST_ONLY}); + + print $opts->{PROMPT}; + if ($opts->{SINGLETON}) { + print "> "; + } + else { + print ">> "; + } + my $line = <STDIN>; + last if (!$line); + chomp $line; + my $donesomething = 0; + for my $choice (split(/[\s,]+/, $line)) { + my $choose = 1; + my ($bottom, $top); + + # Input that begins with '-'; unchoose + if ($choice =~ s/^-//) { + $choose = 0; + } + # A range can be specified like 5-7 + if ($choice =~ /^(\d+)-(\d+)$/) { + ($bottom, $top) = ($1, $2); + } + elsif ($choice =~ /^\d+$/) { + $bottom = $top = $choice; + } + elsif ($choice eq '*') { + $bottom = 1; + $top = 1 + @stuff; + } + else { + $bottom = $top = find_unique($choice, @stuff); + if (!defined $bottom) { + print "Huh ($choice)?\n"; + next TOPLOOP; + } + } + if ($opts->{SINGLETON} && $bottom != $top) { + print "Huh ($choice)?\n"; + next TOPLOOP; + } + for ($i = $bottom-1; $i <= $top-1; $i++) { + next if (@stuff <= $i); + $chosen[$i] = $choose; + $donesomething++; + } + } + last if (!$donesomething || $opts->{IMMEDIATE}); + } + for ($i = 0; $i < @stuff; $i++) { + if ($chosen[$i]) { + push @return, $stuff[$i]; + } + } + return @return; +} + +sub status_cmd { + list_and_choose({ LIST_ONLY => 1, HEADER => $status_head }, + list_modified()); + print "\n"; +} + +sub say_n_paths { + my $did = shift @_; + my $cnt = scalar @_; + print "$did "; + if (1 < $cnt) { + print "$cnt paths\n"; + } + else { + print "one path\n"; + } +} + +sub update_cmd { + my @mods = list_modified('file-only'); + return if (!@mods); + + my @update = list_and_choose({ PROMPT => 'Update', + HEADER => $status_head, }, + @mods); + if (@update) { + system(qw(git update-index --add --remove --), + map { $_->{VALUE} } @update); + say_n_paths('updated', @update); + } + print "\n"; +} + +sub revert_cmd { + my @update = list_and_choose({ PROMPT => 'Revert', + HEADER => $status_head, }, + list_modified()); + if (@update) { + my @lines = run_cmd_pipe(qw(git ls-tree HEAD --), + map { $_->{VALUE} } @update); + my $fh; + open $fh, '|-', qw(git update-index --index-info) + or die; + for (@lines) { + print $fh $_; + } + close($fh); + for (@update) { + if ($_->{INDEX_ADDDEL} && + $_->{INDEX_ADDDEL} eq 'create') { + system(qw(git update-index --force-remove --), + $_->{VALUE}); + print "note: $_->{VALUE} is untracked now.\n"; + } + } + refresh(); + say_n_paths('reverted', @update); + } + print "\n"; +} + +sub add_untracked_cmd { + my @add = list_and_choose({ PROMPT => 'Add untracked' }, + list_untracked()); + if (@add) { + system(qw(git update-index --add --), @add); + say_n_paths('added', @add); + } + print "\n"; +} + +sub parse_diff { + my ($path) = @_; + my @diff = run_cmd_pipe(qw(git diff-files -p --), $path); + my (@hunk) = { TEXT => [] }; + + for (@diff) { + if (/^@@ /) { + push @hunk, { TEXT => [] }; + } + push @{$hunk[-1]{TEXT}}, $_; + } + return @hunk; +} + +sub hunk_splittable { + my ($text) = @_; + + my @s = split_hunk($text); + return (1 < @s); +} + +sub parse_hunk_header { + my ($line) = @_; + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = + $line =~ /^@@ -(\d+)(?:,(\d+)) \+(\d+)(?:,(\d+)) @@/; + return ($o_ofs, $o_cnt, $n_ofs, $n_cnt); +} + +sub split_hunk { + my ($text) = @_; + my @split = (); + + # If there are context lines in the middle of a hunk, + # it can be split, but we would need to take care of + # overlaps later. + + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = parse_hunk_header($text->[0]); + my $hunk_start = 1; + my $next_hunk_start; + + OUTER: + while (1) { + my $next_hunk_start = undef; + my $i = $hunk_start - 1; + my $this = +{ + TEXT => [], + OLD => $o_ofs, + NEW => $n_ofs, + OCNT => 0, + NCNT => 0, + ADDDEL => 0, + POSTCTX => 0, + }; + + while (++$i < @$text) { + my $line = $text->[$i]; + if ($line =~ /^ /) { + if ($this->{ADDDEL} && + !defined $next_hunk_start) { + # We have seen leading context and + # adds/dels and then here is another + # context, which is trailing for this + # split hunk and leading for the next + # one. + $next_hunk_start = $i; + } + push @{$this->{TEXT}}, $line; + $this->{OCNT}++; + $this->{NCNT}++; + if (defined $next_hunk_start) { + $this->{POSTCTX}++; + } + next; + } + + # add/del + if (defined $next_hunk_start) { + # We are done with the current hunk and + # this is the first real change for the + # next split one. + $hunk_start = $next_hunk_start; + $o_ofs = $this->{OLD} + $this->{OCNT}; + $n_ofs = $this->{NEW} + $this->{NCNT}; + $o_ofs -= $this->{POSTCTX}; + $n_ofs -= $this->{POSTCTX}; + push @split, $this; + redo OUTER; + } + push @{$this->{TEXT}}, $line; + $this->{ADDDEL}++; + if ($line =~ /^-/) { + $this->{OCNT}++; + } + else { + $this->{NCNT}++; + } + } + + push @split, $this; + last; + } + + for my $hunk (@split) { + $o_ofs = $hunk->{OLD}; + $n_ofs = $hunk->{NEW}; + $o_cnt = $hunk->{OCNT}; + $n_cnt = $hunk->{NCNT}; + + my $head = ("@@ -$o_ofs" . + (($o_cnt != 1) ? ",$o_cnt" : '') . + " +$n_ofs" . + (($n_cnt != 1) ? ",$n_cnt" : '') . + " @@\n"); + unshift @{$hunk->{TEXT}}, $head; + } + return map { $_->{TEXT} } @split; +} + +sub find_last_o_ctx { + my ($it) = @_; + my $text = $it->{TEXT}; + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = parse_hunk_header($text->[0]); + my $i = @{$text}; + my $last_o_ctx = $o_ofs + $o_cnt; + while (0 < --$i) { + my $line = $text->[$i]; + if ($line =~ /^ /) { + $last_o_ctx--; + next; + } + last; + } + return $last_o_ctx; +} + +sub merge_hunk { + my ($prev, $this) = @_; + my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) = + parse_hunk_header($prev->{TEXT}[0]); + my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) = + parse_hunk_header($this->{TEXT}[0]); + + my (@line, $i, $ofs, $o_cnt, $n_cnt); + $ofs = $o0_ofs; + $o_cnt = $n_cnt = 0; + for ($i = 1; $i < @{$prev->{TEXT}}; $i++) { + my $line = $prev->{TEXT}[$i]; + if ($line =~ /^\+/) { + $n_cnt++; + push @line, $line; + next; + } + + last if ($o1_ofs <= $ofs); + + $o_cnt++; + $ofs++; + if ($line =~ /^ /) { + $n_cnt++; + } + push @line, $line; + } + + for ($i = 1; $i < @{$this->{TEXT}}; $i++) { + my $line = $this->{TEXT}[$i]; + if ($line =~ /^\+/) { + $n_cnt++; + push @line, $line; + next; + } + $ofs++; + $o_cnt++; + if ($line =~ /^ /) { + $n_cnt++; + } + push @line, $line; + } + my $head = ("@@ -$o0_ofs" . + (($o_cnt != 1) ? ",$o_cnt" : '') . + " +$n0_ofs" . + (($n_cnt != 1) ? ",$n_cnt" : '') . + " @@\n"); + @{$prev->{TEXT}} = ($head, @line); +} + +sub coalesce_overlapping_hunks { + my (@in) = @_; + my @out = (); + + my ($last_o_ctx); + + for (grep { $_->{USE} } @in) { + my $text = $_->{TEXT}; + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = + parse_hunk_header($text->[0]); + if (defined $last_o_ctx && + $o_ofs <= $last_o_ctx) { + merge_hunk($out[-1], $_); + } + else { + push @out, $_; + } + $last_o_ctx = find_last_o_ctx($out[-1]); + } + return @out; +} + +sub help_patch_cmd { + print <<\EOF ; +y - stage this hunk +n - do not stage this hunk +a - stage this and all the remaining hunks +d - do not stage this hunk nor any of the remaining hunks +j - leave this hunk undecided, see next undecided hunk +J - leave this hunk undecided, see next hunk +k - leave this hunk undecided, see previous undecided hunk +K - leave this hunk undecided, see previous hunk +s - split the current hunk into smaller hunks +EOF +} + +sub patch_update_cmd { + my @mods = list_modified('file-only'); + @mods = grep { !($_->{BINARY}) } @mods; + return if (!@mods); + + my ($it) = list_and_choose({ PROMPT => 'Patch update', + SINGLETON => 1, + IMMEDIATE => 1, + HEADER => $status_head, }, + @mods); + return if (!$it); + + my ($ix, $num); + my $path = $it->{VALUE}; + my ($head, @hunk) = parse_diff($path); + for (@{$head->{TEXT}}) { + print; + } + $num = scalar @hunk; + $ix = 0; + + while (1) { + my ($prev, $next, $other, $undecided, $i); + $other = ''; + + if ($num <= $ix) { + $ix = 0; + } + for ($i = 0; $i < $ix; $i++) { + if (!defined $hunk[$i]{USE}) { + $prev = 1; + $other .= '/k'; + last; + } + } + if ($ix) { + $other .= '/K'; + } + for ($i = $ix + 1; $i < $num; $i++) { + if (!defined $hunk[$i]{USE}) { + $next = 1; + $other .= '/j'; + last; + } + } + if ($ix < $num - 1) { + $other .= '/J'; + } + for ($i = 0; $i < $num; $i++) { + if (!defined $hunk[$i]{USE}) { + $undecided = 1; + last; + } + } + last if (!$undecided); + + if (hunk_splittable($hunk[$ix]{TEXT})) { + $other .= '/s'; + } + for (@{$hunk[$ix]{TEXT}}) { + print; + } + print "Stage this hunk [y/n/a/d$other/?]? "; + my $line = <STDIN>; + if ($line) { + if ($line =~ /^y/i) { + $hunk[$ix]{USE} = 1; + } + elsif ($line =~ /^n/i) { + $hunk[$ix]{USE} = 0; + } + elsif ($line =~ /^a/i) { + while ($ix < $num) { + if (!defined $hunk[$ix]{USE}) { + $hunk[$ix]{USE} = 1; + } + $ix++; + } + next; + } + elsif ($line =~ /^d/i) { + while ($ix < $num) { + if (!defined $hunk[$ix]{USE}) { + $hunk[$ix]{USE} = 0; + } + $ix++; + } + next; + } + elsif ($other =~ /K/ && $line =~ /^K/) { + $ix--; + next; + } + elsif ($other =~ /J/ && $line =~ /^J/) { + $ix++; + next; + } + elsif ($other =~ /k/ && $line =~ /^k/) { + while (1) { + $ix--; + last if (!$ix || + !defined $hunk[$ix]{USE}); + } + next; + } + elsif ($other =~ /j/ && $line =~ /^j/) { + while (1) { + $ix++; + last if ($ix >= $num || + !defined $hunk[$ix]{USE}); + } + next; + } + elsif ($other =~ /s/ && $line =~ /^s/) { + my @split = split_hunk($hunk[$ix]{TEXT}); + if (1 < @split) { + print "Split into ", + scalar(@split), " hunks.\n"; + } + splice(@hunk, $ix, 1, + map { +{ TEXT => $_, USE => undef } } + @split); + $num = scalar @hunk; + next; + } + else { + help_patch_cmd($other); + next; + } + # soft increment + while (1) { + $ix++; + last if ($ix >= $num || + !defined $hunk[$ix]{USE}); + } + } + } + + @hunk = coalesce_overlapping_hunks(@hunk); + + my ($o_lofs, $n_lofs) = (0, 0); + my @result = (); + for (@hunk) { + my $text = $_->{TEXT}; + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = + parse_hunk_header($text->[0]); + + if (!$_->{USE}) { + if (!defined $o_cnt) { $o_cnt = 1; } + if (!defined $n_cnt) { $n_cnt = 1; } + + # We would have added ($n_cnt - $o_cnt) lines + # to the postimage if we were to use this hunk, + # but we didn't. So the line number that the next + # hunk starts at would be shifted by that much. + $n_lofs -= ($n_cnt - $o_cnt); + next; + } + else { + if ($n_lofs) { + $n_ofs += $n_lofs; + $text->[0] = ("@@ -$o_ofs" . + ((defined $o_cnt) + ? ",$o_cnt" : '') . + " +$n_ofs" . + ((defined $n_cnt) + ? ",$n_cnt" : '') . + " @@\n"); + } + for (@$text) { + push @result, $_; + } + } + } + + if (@result) { + my $fh; + + open $fh, '|-', qw(git apply --cached); + for (@{$head->{TEXT}}, @result) { + print $fh $_; + } + if (!close $fh) { + for (@{$head->{TEXT}}, @result) { + print STDERR $_; + } + } + refresh(); + } + + print "\n"; +} + +sub diff_cmd { + my @mods = list_modified('index-only'); + @mods = grep { !($_->{BINARY}) } @mods; + return if (!@mods); + my (@them) = list_and_choose({ PROMPT => 'Review diff', + IMMEDIATE => 1, + HEADER => $status_head, }, + @mods); + return if (!@them); + system(qw(git diff-index -p --cached HEAD --), + map { $_->{VALUE} } @them); +} + +sub quit_cmd { + print "Bye.\n"; + exit(0); +} + +sub help_cmd { + print <<\EOF ; +status - show paths with changes +update - add working tree state to the staged set of changes +revert - revert staged set of changes back to the HEAD version +patch - pick hunks and update selectively +diff - view diff between HEAD and index +add untracked - add contents of untracked files to the staged set of changes +EOF +} + +sub main_loop { + my @cmd = ([ 'status', \&status_cmd, ], + [ 'update', \&update_cmd, ], + [ 'revert', \&revert_cmd, ], + [ 'add untracked', \&add_untracked_cmd, ], + [ 'patch', \&patch_update_cmd, ], + [ 'diff', \&diff_cmd, ], + [ 'quit', \&quit_cmd, ], + [ 'help', \&help_cmd, ], + ); + while (1) { + my ($it) = list_and_choose({ PROMPT => 'What now', + SINGLETON => 1, + LIST_FLAT => 4, + HEADER => '*** Commands ***', + IMMEDIATE => 1 }, @cmd); + if ($it) { + eval { + $it->[1]->(); + }; + if ($@) { + print "$@"; + } + } + } +} + +my @z; + +refresh(); +status_cmd(); +main_loop(); diff --git a/git-am.sh b/git-am.sh new file mode 100755 index 0000000000..6db9cb503a --- /dev/null +++ b/git-am.sh @@ -0,0 +1,472 @@ +#!/bin/sh +# +# Copyright (c) 2005, 2006 Junio C Hamano + +USAGE='[--signoff] [--dotest=<dir>] [--utf8 | --no-utf8] [--binary] [--3way] + [--interactive] [--whitespace=<option>] [-C<n>] [-p<n>] <mbox>... + or, when resuming [--skip | --resolved]' +. git-sh-setup +set_reflog_action am +require_work_tree + +git var GIT_COMMITTER_IDENT >/dev/null || exit + +stop_here () { + echo "$1" >"$dotest/next" + exit 1 +} + +stop_here_user_resolve () { + if [ -n "$resolvemsg" ]; then + echo "$resolvemsg" + stop_here $1 + fi + cmdline=$(basename $0) + if test '' != "$interactive" + then + cmdline="$cmdline -i" + fi + if test '' != "$threeway" + then + cmdline="$cmdline -3" + fi + if test '.dotest' != "$dotest" + then + cmdline="$cmdline -d=$dotest" + fi + echo "When you have resolved this problem run \"$cmdline --resolved\"." + echo "If you would prefer to skip this patch, instead run \"$cmdline --skip\"." + + stop_here $1 +} + +go_next () { + rm -f "$dotest/$msgnum" "$dotest/msg" "$dotest/msg-clean" \ + "$dotest/patch" "$dotest/info" + echo "$next" >"$dotest/next" + this=$next +} + +cannot_fallback () { + echo "$1" + echo "Cannot fall back to three-way merge." + exit 1 +} + +fall_back_3way () { + O_OBJECT=`cd "$GIT_OBJECT_DIRECTORY" && pwd` + + rm -fr "$dotest"/patch-merge-* + mkdir "$dotest/patch-merge-tmp-dir" + + # First see if the patch records the index info that we can use. + git-apply -z --index-info "$dotest/patch" \ + >"$dotest/patch-merge-index-info" && + GIT_INDEX_FILE="$dotest/patch-merge-tmp-index" \ + git-update-index -z --index-info <"$dotest/patch-merge-index-info" && + GIT_INDEX_FILE="$dotest/patch-merge-tmp-index" \ + git-write-tree >"$dotest/patch-merge-base+" || + cannot_fallback "Patch does not record usable index information." + + echo Using index info to reconstruct a base tree... + if GIT_INDEX_FILE="$dotest/patch-merge-tmp-index" \ + git-apply $binary --cached <"$dotest/patch" + then + mv "$dotest/patch-merge-base+" "$dotest/patch-merge-base" + mv "$dotest/patch-merge-tmp-index" "$dotest/patch-merge-index" + else + cannot_fallback "Did you hand edit your patch? +It does not apply to blobs recorded in its index." + fi + + test -f "$dotest/patch-merge-index" && + his_tree=$(GIT_INDEX_FILE="$dotest/patch-merge-index" git-write-tree) && + orig_tree=$(cat "$dotest/patch-merge-base") && + rm -fr "$dotest"/patch-merge-* || exit 1 + + echo Falling back to patching base and 3-way merge... + + # This is not so wrong. Depending on which base we picked, + # orig_tree may be wildly different from ours, but his_tree + # has the same set of wildly different changes in parts the + # patch did not touch, so recursive ends up canceling them, + # saying that we reverted all those changes. + + eval GITHEAD_$his_tree='"$SUBJECT"' + export GITHEAD_$his_tree + git-merge-recursive $orig_tree -- HEAD $his_tree || { + if test -d "$GIT_DIR/rr-cache" + then + git-rerere + fi + echo Failed to merge in the changes. + exit 1 + } + unset GITHEAD_$his_tree +} + +prec=4 +dotest=.dotest sign= utf8=t keep= skip= interactive= resolved= binary= resolvemsg= +git_apply_opt= + +while case "$#" in 0) break;; esac +do + case "$1" in + -d=*|--d=*|--do=*|--dot=*|--dote=*|--dotes=*|--dotest=*) + dotest=`expr "z$1" : 'z-[^=]*=\(.*\)'`; shift ;; + -d|--d|--do|--dot|--dote|--dotes|--dotest) + case "$#" in 1) usage ;; esac; shift + dotest="$1"; shift;; + + -i|--i|--in|--int|--inte|--inter|--intera|--interac|--interact|\ + --interacti|--interactiv|--interactive) + interactive=t; shift ;; + + -b|--b|--bi|--bin|--bina|--binar|--binary) + binary=t; shift ;; + + -3|--3|--3w|--3wa|--3way) + threeway=t; shift ;; + -s|--s|--si|--sig|--sign|--signo|--signof|--signoff) + sign=t; shift ;; + -u|--u|--ut|--utf|--utf8) + utf8=t; shift ;; # this is now default + --no-u|--no-ut|--no-utf|--no-utf8) + utf8=; shift ;; + -k|--k|--ke|--kee|--keep) + keep=t; shift ;; + + -r|--r|--re|--res|--reso|--resol|--resolv|--resolve|--resolved) + resolved=t; shift ;; + + --sk|--ski|--skip) + skip=t; shift ;; + + --whitespace=*|-C*|-p*) + git_apply_opt="$git_apply_opt $1"; shift ;; + + --resolvemsg=*) + resolvemsg=$(echo "$1" | sed -e "s/^--resolvemsg=//"); shift ;; + + --) + shift; break ;; + -*) + usage ;; + *) + break ;; + esac +done + +# If the dotest directory exists, but we have finished applying all the +# patches in them, clear it out. +if test -d "$dotest" && + last=$(cat "$dotest/last") && + next=$(cat "$dotest/next") && + test $# != 0 && + test "$next" -gt "$last" +then + rm -fr "$dotest" +fi + +if test -d "$dotest" +then + case "$#,$skip$resolved" in + 0,*t*) + # Explicit resume command and we do not have file, so + # we are happy. + : ;; + 0,) + # No file input but without resume parameters; catch + # user error to feed us a patch from standard input + # when there is already .dotest. This is somewhat + # unreliable -- stdin could be /dev/null for example + # and the caller did not intend to feed us a patch but + # wanted to continue unattended. + tty -s + ;; + *) + false + ;; + esac || + die "previous dotest directory $dotest still exists but mbox given." + resume=yes +else + # Make sure we are not given --skip nor --resolved + test ",$skip,$resolved," = ,,, || + die "Resolve operation not in progress, we are not resuming." + + # Start afresh. + mkdir -p "$dotest" || exit + + git-mailsplit -d"$prec" -o"$dotest" -b -- "$@" > "$dotest/last" || { + rm -fr "$dotest" + exit 1 + } + + # -b, -s, -u, -k and --whitespace flags are kept for the + # resuming session after a patch failure. + # -3 and -i can and must be given when resuming. + echo "$binary" >"$dotest/binary" + echo " $ws" >"$dotest/whitespace" + echo "$sign" >"$dotest/sign" + echo "$utf8" >"$dotest/utf8" + echo "$keep" >"$dotest/keep" + echo 1 >"$dotest/next" +fi + +case "$resolved" in +'') + files=$(git-diff-index --cached --name-only HEAD) || exit + if [ "$files" ]; then + echo "Dirty index: cannot apply patches (dirty: $files)" >&2 + exit 1 + fi +esac + +if test "$(cat "$dotest/binary")" = t +then + binary=--allow-binary-replacement +fi +if test "$(cat "$dotest/utf8")" = t +then + utf8=-u +else + utf8=-n +fi +if test "$(cat "$dotest/keep")" = t +then + keep=-k +fi +ws=`cat "$dotest/whitespace"` +if test "$(cat "$dotest/sign")" = t +then + SIGNOFF=`git-var GIT_COMMITTER_IDENT | sed -e ' + s/>.*/>/ + s/^/Signed-off-by: /' + ` +else + SIGNOFF= +fi + +last=`cat "$dotest/last"` +this=`cat "$dotest/next"` +if test "$skip" = t +then + if test -d "$GIT_DIR/rr-cache" + then + git-rerere clear + fi + this=`expr "$this" + 1` + resume= +fi + +if test "$this" -gt "$last" +then + echo Nothing to do. + rm -fr "$dotest" + exit +fi + +while test "$this" -le "$last" +do + msgnum=`printf "%0${prec}d" $this` + next=`expr "$this" + 1` + test -f "$dotest/$msgnum" || { + resume= + go_next + continue + } + + # If we are not resuming, parse and extract the patch information + # into separate files: + # - info records the authorship and title + # - msg is the rest of commit log message + # - patch is the patch body. + # + # When we are resuming, these files are either already prepared + # by the user, or the user can tell us to do so by --resolved flag. + case "$resume" in + '') + git-mailinfo $keep $utf8 "$dotest/msg" "$dotest/patch" \ + <"$dotest/$msgnum" >"$dotest/info" || + stop_here $this + git-stripspace < "$dotest/msg" > "$dotest/msg-clean" + ;; + esac + + GIT_AUTHOR_NAME="$(sed -n '/^Author/ s/Author: //p' "$dotest/info")" + GIT_AUTHOR_EMAIL="$(sed -n '/^Email/ s/Email: //p' "$dotest/info")" + GIT_AUTHOR_DATE="$(sed -n '/^Date/ s/Date: //p' "$dotest/info")" + + if test -z "$GIT_AUTHOR_EMAIL" + then + echo "Patch does not have a valid e-mail address." + stop_here $this + fi + + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE + + SUBJECT="$(sed -n '/^Subject/ s/Subject: //p' "$dotest/info")" + case "$keep_subject" in -k) SUBJECT="[PATCH] $SUBJECT" ;; esac + + case "$resume" in + '') + if test '' != "$SIGNOFF" + then + LAST_SIGNED_OFF_BY=` + sed -ne '/^Signed-off-by: /p' \ + "$dotest/msg-clean" | + tail -n 1 + ` + ADD_SIGNOFF=` + test "$LAST_SIGNED_OFF_BY" = "$SIGNOFF" || { + test '' = "$LAST_SIGNED_OFF_BY" && echo + echo "$SIGNOFF" + }` + else + ADD_SIGNOFF= + fi + { + echo "$SUBJECT" + if test -s "$dotest/msg-clean" + then + echo + cat "$dotest/msg-clean" + fi + if test '' != "$ADD_SIGNOFF" + then + echo "$ADD_SIGNOFF" + fi + } >"$dotest/final-commit" + ;; + *) + case "$resolved$interactive" in + tt) + # This is used only for interactive view option. + git-diff-index -p --cached HEAD >"$dotest/patch" + ;; + esac + esac + + resume= + if test "$interactive" = t + then + test -t 0 || + die "cannot be interactive without stdin connected to a terminal." + action=again + while test "$action" = again + do + echo "Commit Body is:" + echo "--------------------------" + cat "$dotest/final-commit" + echo "--------------------------" + printf "Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all " + read reply + case "$reply" in + [yY]*) action=yes ;; + [aA]*) action=yes interactive= ;; + [nN]*) action=skip ;; + [eE]*) "${VISUAL:-${EDITOR:-vi}}" "$dotest/final-commit" + action=again ;; + [vV]*) action=again + LESS=-S ${PAGER:-less} "$dotest/patch" ;; + *) action=again ;; + esac + done + else + action=yes + fi + + if test $action = skip + then + go_next + continue + fi + + if test -x "$GIT_DIR"/hooks/applypatch-msg + then + "$GIT_DIR"/hooks/applypatch-msg "$dotest/final-commit" || + stop_here $this + fi + + echo + echo "Applying '$SUBJECT'" + echo + + case "$resolved" in + '') + git-apply $git_apply_opt $binary --index "$dotest/patch" + apply_status=$? + ;; + t) + # Resolved means the user did all the hard work, and + # we do not have to do any patch application. Just + # trust what the user has in the index file and the + # working tree. + resolved= + changed="$(git-diff-index --cached --name-only HEAD)" + if test '' = "$changed" + then + echo "No changes - did you forget to use 'git add'?" + stop_here_user_resolve $this + fi + unmerged=$(git-ls-files -u) + if test -n "$unmerged" + then + echo "You still have unmerged paths in your index" + echo "did you forget to use 'git add'?" + stop_here_user_resolve $this + fi + apply_status=0 + if test -d "$GIT_DIR/rr-cache" + then + git rerere + fi + ;; + esac + + if test $apply_status = 1 && test "$threeway" = t + then + if (fall_back_3way) + then + # Applying the patch to an earlier tree and merging the + # result may have produced the same tree as ours. + changed="$(git-diff-index --cached --name-only HEAD)" + if test '' = "$changed" + then + echo No changes -- Patch already applied. + go_next + continue + fi + # clear apply_status -- we have successfully merged. + apply_status=0 + fi + fi + if test $apply_status != 0 + then + echo Patch failed at $msgnum. + stop_here_user_resolve $this + fi + + if test -x "$GIT_DIR"/hooks/pre-applypatch + then + "$GIT_DIR"/hooks/pre-applypatch || stop_here $this + fi + + tree=$(git-write-tree) && + echo Wrote tree $tree && + parent=$(git-rev-parse --verify HEAD) && + commit=$(git-commit-tree $tree -p $parent <"$dotest/final-commit") && + echo Committed: $commit && + git-update-ref -m "$GIT_REFLOG_ACTION: $SUBJECT" HEAD $commit $parent || + stop_here $this + + if test -x "$GIT_DIR"/hooks/post-applypatch + then + "$GIT_DIR"/hooks/post-applypatch + fi + + go_next +done + +rm -fr "$dotest" diff --git a/git-applymbox.sh b/git-applymbox.sh new file mode 100755 index 0000000000..1f68599ae5 --- /dev/null +++ b/git-applymbox.sh @@ -0,0 +1,117 @@ +#!/bin/sh +## +## "dotest" is my stupid name for my patch-application script, which +## I never got around to renaming after I tested it. We're now on the +## second generation of scripts, still called "dotest". +## +## Update: Ryan Anderson finally shamed me into naming this "applymbox". +## +## You give it a mbox-format collection of emails, and it will try to +## apply them to the kernel using "applypatch" +## +## The patch application may fail in the middle. In which case: +## (1) look at .dotest/patch and fix it up to apply +## (2) re-run applymbox with -c .dotest/msg-number for the current one. +## Pay a special attention to the commit log message if you do this and +## use a Signoff_file, because applypatch wants to append the sign-off +## message to msg-clean every time it is run. +## +## git-am is supposed to be the newer and better tool for this job. + +USAGE='[-u] [-k] [-q] [-m] (-c .dotest/<num> | mbox) [signoff]' +. git-sh-setup + +git var GIT_COMMITTER_IDENT >/dev/null || exit + +keep_subject= query_apply= continue= utf8=-u resume=t +while case "$#" in 0) break ;; esac +do + case "$1" in + -u) utf8=-u ;; + -n) utf8=-n ;; + -k) keep_subject=-k ;; + -q) query_apply=t ;; + -c) continue="$2"; resume=f; shift ;; + -m) fall_back_3way=t ;; + -*) usage ;; + *) break ;; + esac + shift +done + +case "$continue" in +'') + rm -rf .dotest + mkdir .dotest + num_msgs=$(git-mailsplit "$1" .dotest) || exit 1 + echo "$num_msgs patch(es) to process." + shift +esac + +files=$(git-diff-index --cached --name-only HEAD) || exit +if [ "$files" ]; then + echo "Dirty index: cannot apply patches (dirty: $files)" >&2 + exit 1 +fi + +case "$query_apply" in +t) touch .dotest/.query_apply +esac +case "$fall_back_3way" in +t) : >.dotest/.3way +esac +case "$keep_subject" in +-k) : >.dotest/.keep_subject +esac + +signoff="$1" +set x .dotest/0* +shift +while case "$#" in 0) break;; esac +do + i="$1" + case "$resume,$continue" in + f,$i) resume=t;; + f,*) shift + continue;; + *) + git-mailinfo $keep_subject $utf8 \ + .dotest/msg .dotest/patch <$i >.dotest/info || exit 1 + git-stripspace < .dotest/msg > .dotest/msg-clean + ;; + esac + while :; # for fixing up and retry + do + git-applypatch .dotest/msg-clean .dotest/patch .dotest/info "$signoff" + case "$?" in + 0) + # Remove the cleanly applied one to reduce clutter. + rm -f .dotest/$i + ;; + 2) + # 2 is a special exit code from applypatch to indicate that + # the patch wasn't applied, but continue anyway + ;; + *) + ret=$? + if test -f .dotest/.query_apply + then + echo >&2 "* Patch failed." + echo >&2 "* You could fix it up in your editor and" + echo >&2 " retry. If you want to do so, say yes here" + echo >&2 " AFTER fixing .dotest/patch up." + echo >&2 -n "Retry [y/N]? " + read yesno + case "$yesno" in + [Yy]*) + continue ;; + esac + fi + exit $ret + esac + break + done + shift +done +# return to pristine +rm -fr .dotest diff --git a/git-applypatch.sh b/git-applypatch.sh new file mode 100755 index 0000000000..8df2aee4c2 --- /dev/null +++ b/git-applypatch.sh @@ -0,0 +1,212 @@ +#!/bin/sh +## +## applypatch takes four file arguments, and uses those to +## apply the unpacked patch (surprise surprise) that they +## represent to the current tree. +## +## The arguments are: +## $1 - file with commit message +## $2 - file with the actual patch +## $3 - "info" file with Author, email and subject +## $4 - optional file containing signoff to add +## + +USAGE='<msg> <patch> <info> [<signoff>]' +. git-sh-setup + +case "$#" in 3|4) ;; *) usage ;; esac + +final=.dotest/final-commit +## +## If this file exists, we ask before applying +## +query_apply=.dotest/.query_apply + +## We do not munge the first line of the commit message too much +## if this file exists. +keep_subject=.dotest/.keep_subject + +## We do not attempt the 3-way merge fallback unless this file exists. +fall_back_3way=.dotest/.3way + +MSGFILE=$1 +PATCHFILE=$2 +INFO=$3 +SIGNOFF=$4 +EDIT=${VISUAL:-${EDITOR:-vi}} + +export GIT_AUTHOR_NAME="$(sed -n '/^Author/ s/Author: //p' "$INFO")" +export GIT_AUTHOR_EMAIL="$(sed -n '/^Email/ s/Email: //p' "$INFO")" +export GIT_AUTHOR_DATE="$(sed -n '/^Date/ s/Date: //p' "$INFO")" +export SUBJECT="$(sed -n '/^Subject/ s/Subject: //p' "$INFO")" + +if test '' != "$SIGNOFF" +then + if test -f "$SIGNOFF" + then + SIGNOFF=`cat "$SIGNOFF"` || exit + elif case "$SIGNOFF" in yes | true | me | please) : ;; *) false ;; esac + then + SIGNOFF=`git-var GIT_COMMITTER_IDENT | sed -e ' + s/>.*/>/ + s/^/Signed-off-by: /' + ` + else + SIGNOFF= + fi + if test '' != "$SIGNOFF" + then + LAST_SIGNED_OFF_BY=` + sed -ne '/^Signed-off-by: /p' "$MSGFILE" | + tail -n 1 + ` + test "$LAST_SIGNED_OFF_BY" = "$SIGNOFF" || { + test '' = "$LAST_SIGNED_OFF_BY" && echo + echo "$SIGNOFF" + } >>"$MSGFILE" + fi +fi + +patch_header= +test -f "$keep_subject" || patch_header='[PATCH] ' + +{ + echo "$patch_header$SUBJECT" + if test -s "$MSGFILE" + then + echo + cat "$MSGFILE" + fi +} >"$final" + +interactive=yes +test -f "$query_apply" || interactive=no + +while [ "$interactive" = yes ]; do + echo "Commit Body is:" + echo "--------------------------" + cat "$final" + echo "--------------------------" + printf "Apply? [y]es/[n]o/[e]dit/[a]ccept all " + read reply + case "$reply" in + y|Y) interactive=no;; + n|N) exit 2;; # special value to tell dotest to keep going + e|E) "$EDIT" "$final";; + a|A) rm -f "$query_apply" + interactive=no ;; + esac +done + +if test -x "$GIT_DIR"/hooks/applypatch-msg +then + "$GIT_DIR"/hooks/applypatch-msg "$final" || exit +fi + +echo +echo Applying "'$SUBJECT'" +echo + +git-apply --index "$PATCHFILE" || { + + # git-apply exits with status 1 when the patch does not apply, + # but it die()s with other failures, most notably upon corrupt + # patch. In the latter case, there is no point to try applying + # it to another tree and do 3-way merge. + test $? = 1 || exit 1 + + test -f "$fall_back_3way" || exit 1 + + # Here if we know which revision the patch applies to, + # we create a temporary working tree and index, apply the + # patch, and attempt 3-way merge with the resulting tree. + + O_OBJECT=`cd "$GIT_OBJECT_DIRECTORY" && pwd` + rm -fr .patch-merge-* + + if git-apply -z --index-info "$PATCHFILE" \ + >.patch-merge-index-info 2>/dev/null && + GIT_INDEX_FILE=.patch-merge-tmp-index \ + git-update-index -z --index-info <.patch-merge-index-info && + GIT_INDEX_FILE=.patch-merge-tmp-index \ + git-write-tree >.patch-merge-tmp-base && + ( + mkdir .patch-merge-tmp-dir && + cd .patch-merge-tmp-dir && + GIT_INDEX_FILE="../.patch-merge-tmp-index" \ + GIT_OBJECT_DIRECTORY="$O_OBJECT" \ + git-apply $binary --index + ) <"$PATCHFILE" + then + echo Using index info to reconstruct a base tree... + mv .patch-merge-tmp-base .patch-merge-base + mv .patch-merge-tmp-index .patch-merge-index + else + ( + N=10 + + # Otherwise, try nearby trees that can be used to apply the + # patch. + git-rev-list --max-count=$N HEAD + + # or hoping the patch is against known tags... + git-ls-remote --tags . + ) | + while read base junk + do + # Try it if we have it as a tree. + git-cat-file tree "$base" >/dev/null 2>&1 || continue + + rm -fr .patch-merge-tmp-* && + mkdir .patch-merge-tmp-dir || break + ( + cd .patch-merge-tmp-dir && + GIT_INDEX_FILE=../.patch-merge-tmp-index && + GIT_OBJECT_DIRECTORY="$O_OBJECT" && + export GIT_INDEX_FILE GIT_OBJECT_DIRECTORY && + git-read-tree "$base" && + git-apply --index && + mv ../.patch-merge-tmp-index ../.patch-merge-index && + echo "$base" >../.patch-merge-base + ) <"$PATCHFILE" 2>/dev/null && break + done + fi + + test -f .patch-merge-index && + his_tree=$(GIT_INDEX_FILE=.patch-merge-index git-write-tree) && + orig_tree=$(cat .patch-merge-base) && + rm -fr .patch-merge-* || exit 1 + + echo Falling back to patching base and 3-way merge using $orig_tree... + + # This is not so wrong. Depending on which base we picked, + # orig_tree may be wildly different from ours, but his_tree + # has the same set of wildly different changes in parts the + # patch did not touch, so resolve ends up canceling them, + # saying that we reverted all those changes. + + if git-merge-resolve $orig_tree -- HEAD $his_tree + then + echo Done. + else + echo Failed to merge in the changes. + exit 1 + fi +} + +if test -x "$GIT_DIR"/hooks/pre-applypatch +then + "$GIT_DIR"/hooks/pre-applypatch || exit +fi + +tree=$(git-write-tree) || exit 1 +echo Wrote tree $tree +parent=$(git-rev-parse --verify HEAD) && +commit=$(git-commit-tree $tree -p $parent <"$final") || exit 1 +echo Committed: $commit +git-update-ref -m "applypatch: $SUBJECT" HEAD $commit $parent || exit + +if test -x "$GIT_DIR"/hooks/post-applypatch +then + "$GIT_DIR"/hooks/post-applypatch +fi diff --git a/git-archimport.perl b/git-archimport.perl new file mode 100755 index 0000000000..66aaeae102 --- /dev/null +++ b/git-archimport.perl @@ -0,0 +1,1077 @@ +#!/usr/bin/perl -w +# +# This tool is copyright (c) 2005, Martin Langhoff. +# It is released under the Gnu Public License, version 2. +# +# The basic idea is to walk the output of tla abrowse, +# fetch the changesets and apply them. +# + +=head1 Invocation + + git-archimport [ -h ] [ -v ] [ -o ] [ -a ] [ -f ] [ -T ] + [ -D depth] [ -t tempdir ] <archive>/<branch> [ <archive>/<branch> ] + +Imports a project from one or more Arch repositories. It will follow branches +and repositories within the namespaces defined by the <archive/branch> +parameters supplied. If it cannot find the remote branch a merge comes from +it will just import it as a regular commit. If it can find it, it will mark it +as a merge whenever possible. + +See man (1) git-archimport for more details. + +=head1 TODO + + - create tag objects instead of ref tags + - audit shell-escaping of filenames + - hide our private tags somewhere smarter + - find a way to make "cat *patches | patch" safe even when patchfiles are missing newlines + - sort and apply patches by graphing ancestry relations instead of just + relying in dates supplied in the changeset itself. + tla ancestry-graph -m could be helpful here... + +=head1 Devel tricks + +Add print in front of the shell commands invoked via backticks. + +=head1 Devel Notes + +There are several places where Arch and git terminology are intermixed +and potentially confused. + +The notion of a "branch" in git is approximately equivalent to +a "archive/category--branch--version" in Arch. Also, it should be noted +that the "--branch" portion of "archive/category--branch--version" is really +optional in Arch although not many people (nor tools!) seem to know this. +This means that "archive/category--version" is also a valid "branch" +in git terms. + +We always refer to Arch names by their fully qualified variant (which +means the "archive" name is prefixed. + +For people unfamiliar with Arch, an "archive" is the term for "repository", +and can contain multiple, unrelated branches. + +=cut + +use strict; +use warnings; +use Getopt::Std; +use File::Temp qw(tempdir); +use File::Path qw(mkpath rmtree); +use File::Basename qw(basename dirname); +use Data::Dumper qw/ Dumper /; +use IPC::Open2; + +$SIG{'PIPE'}="IGNORE"; +$ENV{'TZ'}="UTC"; + +my $git_dir = $ENV{"GIT_DIR"} || ".git"; +$ENV{"GIT_DIR"} = $git_dir; +my $ptag_dir = "$git_dir/archimport/tags"; + +our($opt_h,$opt_f,$opt_v,$opt_T,$opt_t,$opt_D,$opt_a,$opt_o); + +sub usage() { + print STDERR <<END; +Usage: ${\basename $0} # fetch/update GIT from Arch + [ -h ] [ -v ] [ -o ] [ -a ] [ -f ] [ -T ] [ -D depth ] [ -t tempdir ] + repository/arch-branch [ repository/arch-branch] ... +END + exit(1); +} + +getopts("fThvat:D:") or usage(); +usage if $opt_h; + +@ARGV >= 1 or usage(); +# $arch_branches: +# values associated with keys: +# =1 - Arch version / git 'branch' detected via abrowse on a limit +# >1 - Arch version / git 'branch' of an auxiliary branch we've merged +my %arch_branches = map { $_ => 1 } @ARGV; + +$ENV{'TMPDIR'} = $opt_t if $opt_t; # $ENV{TMPDIR} will affect tempdir() calls: +my $tmp = tempdir('git-archimport-XXXXXX', TMPDIR => 1, CLEANUP => 1); +$opt_v && print "+ Using $tmp as temporary directory\n"; + +unless (-d $git_dir) { # initial import needs empty directory + opendir DIR, '.' or die "Unable to open current directory: $!\n"; + while (my $entry = readdir DIR) { + $entry =~ /^\.\.?$/ or + die "Initial import needs an empty current working directory.\n" + } + closedir DIR +} + +my %reachable = (); # Arch repositories we can access +my %unreachable = (); # Arch repositories we can't access :< +my @psets = (); # the collection +my %psets = (); # the collection, by name +my %stats = ( # Track which strategy we used to import: + get_tag => 0, replay => 0, get_new => 0, get_delta => 0, + simple_changeset => 0, import_or_tag => 0 +); + +my %rptags = (); # my reverse private tags + # to map a SHA1 to a commitid +my $TLA = $ENV{'ARCH_CLIENT'} || 'tla'; + +sub do_abrowse { + my $stage = shift; + while (my ($limit, $level) = each %arch_branches) { + next unless $level == $stage; + + open ABROWSE, "$TLA abrowse -fkD --merges $limit |" + or die "Problems with tla abrowse: $!"; + + my %ps = (); # the current one + my $lastseen = ''; + + while (<ABROWSE>) { + chomp; + + # first record padded w 8 spaces + if (s/^\s{8}\b//) { + my ($id, $type) = split(m/\s+/, $_, 2); + + my %last_ps; + # store the record we just captured + if (%ps && !exists $psets{ $ps{id} }) { + %last_ps = %ps; # break references + push (@psets, \%last_ps); + $psets{ $last_ps{id} } = \%last_ps; + } + + my $branch = extract_versionname($id); + %ps = ( id => $id, branch => $branch ); + if (%last_ps && ($last_ps{branch} eq $branch)) { + $ps{parent_id} = $last_ps{id}; + } + + $arch_branches{$branch} = 1; + $lastseen = 'id'; + + # deal with types (should work with baz or tla): + if ($type =~ m/\(.*changeset\)/) { + $ps{type} = 's'; + } elsif ($type =~ /\(.*import\)/) { + $ps{type} = 'i'; + } elsif ($type =~ m/\(tag.*?(\S+\@\S+).*?\)/) { + $ps{type} = 't'; + # read which revision we've tagged when we parse the log + $ps{tag} = $1; + } else { + warn "Unknown type $type"; + } + + $arch_branches{$branch} = 1; + $lastseen = 'id'; + } elsif (s/^\s{10}//) { + # 10 leading spaces or more + # indicate commit metadata + + # date + if ($lastseen eq 'id' && m/^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)/){ + $ps{date} = $1; + $lastseen = 'date'; + } elsif ($_ eq 'merges in:') { + $ps{merges} = []; + $lastseen = 'merges'; + } elsif ($lastseen eq 'merges' && s/^\s{2}//) { + my $id = $_; + push (@{$ps{merges}}, $id); + + # aggressive branch finding: + if ($opt_D) { + my $branch = extract_versionname($id); + my $repo = extract_reponame($branch); + + if (archive_reachable($repo) && + !defined $arch_branches{$branch}) { + $arch_branches{$branch} = $stage + 1; + } + } + } else { + warn "more metadata after merges!?: $_\n" unless /^\s*$/; + } + } + } + + if (%ps && !exists $psets{ $ps{id} }) { + my %temp = %ps; # break references + if (@psets && $psets[$#psets]{branch} eq $ps{branch}) { + $temp{parent_id} = $psets[$#psets]{id}; + } + push (@psets, \%temp); + $psets{ $temp{id} } = \%temp; + } + + close ABROWSE or die "$TLA abrowse failed on $limit\n"; + } +} # end foreach $root + +do_abrowse(1); +my $depth = 2; +$opt_D ||= 0; +while ($depth <= $opt_D) { + do_abrowse($depth); + $depth++; +} + +## Order patches by time +# FIXME see if we can find a more optimal way to do this by graphing +# the ancestry data and walking it, that way we won't have to rely on +# client-supplied dates +@psets = sort {$a->{date}.$b->{id} cmp $b->{date}.$b->{id}} @psets; + +#print Dumper \@psets; + +## +## TODO cleanup irrelevant patches +## and put an initial import +## or a full tag +my $import = 0; +unless (-d $git_dir) { # initial import + if ($psets[0]{type} eq 'i' || $psets[0]{type} eq 't') { + print "Starting import from $psets[0]{id}\n"; + `git-init`; + die $! if $?; + $import = 1; + } else { + die "Need to start from an import or a tag -- cannot use $psets[0]{id}"; + } +} else { # progressing an import + # load the rptags + opendir(DIR, $ptag_dir) + || die "can't opendir: $!"; + while (my $file = readdir(DIR)) { + # skip non-interesting-files + next unless -f "$ptag_dir/$file"; + + # convert first '--' to '/' from old git-archimport to use + # as an archivename/c--b--v private tag + if ($file !~ m!,!) { + my $oldfile = $file; + $file =~ s!--!,!; + print STDERR "converting old tag $oldfile to $file\n"; + rename("$ptag_dir/$oldfile", "$ptag_dir/$file") or die $!; + } + my $sha = ptag($file); + chomp $sha; + $rptags{$sha} = $file; + } + closedir DIR; +} + +# process patchsets +# extract the Arch repository name (Arch "archive" in Arch-speak) +sub extract_reponame { + my $fq_cvbr = shift; # archivename/[[[[category]branch]version]revision] + return (split(/\//, $fq_cvbr))[0]; +} + +sub extract_versionname { + my $name = shift; + $name =~ s/--(?:patch|version(?:fix)?|base)-\d+$//; + return $name; +} + +# convert a fully-qualified revision or version to a unique dirname: +# normalperson@yhbt.net-05/mpd--uclinux--1--patch-2 +# becomes: normalperson@yhbt.net-05,mpd--uclinux--1 +# +# the git notion of a branch is closer to +# archive/category--branch--version than archive/category--branch, so we +# use this to convert to git branch names. +# Also, keep archive names but replace '/' with ',' since it won't require +# subdirectories, and is safer than swapping '--' which could confuse +# reverse-mapping when dealing with bastard branches that +# are just archive/category--version (no --branch) +sub tree_dirname { + my $revision = shift; + my $name = extract_versionname($revision); + $name =~ s#/#,#; + return $name; +} + +# old versions of git-archimport just use the <category--branch> part: +sub old_style_branchname { + my $id = shift; + my $ret = safe_pipe_capture($TLA,'parse-package-name','-p',$id); + chomp $ret; + return $ret; +} + +*git_branchname = $opt_o ? *old_style_branchname : *tree_dirname; + +sub process_patchset_accurate { + my $ps = shift; + + # switch to that branch if we're not already in that branch: + if (-e "$git_dir/refs/heads/$ps->{branch}") { + system('git-checkout','-f',$ps->{branch}) == 0 or die "$! $?\n"; + + # remove any old stuff that got leftover: + my $rm = safe_pipe_capture('git-ls-files','--others','-z'); + rmtree(split(/\0/,$rm)) if $rm; + } + + # Apply the import/changeset/merge into the working tree + my $dir = sync_to_ps($ps); + # read the new log entry: + my @commitlog = safe_pipe_capture($TLA,'cat-log','-d',$dir,$ps->{id}); + die "Error in cat-log: $!" if $?; + chomp @commitlog; + + # grab variables we want from the log, new fields get added to $ps: + # (author, date, email, summary, message body ...) + parselog($ps, \@commitlog); + + if ($ps->{id} =~ /--base-0$/ && $ps->{id} ne $psets[0]{id}) { + # this should work when importing continuations + if ($ps->{tag} && (my $branchpoint = eval { ptag($ps->{tag}) })) { + + # find where we are supposed to branch from + system('git-checkout','-f','-b',$ps->{branch}, + $branchpoint) == 0 or die "$! $?\n"; + + # remove any old stuff that got leftover: + my $rm = safe_pipe_capture('git-ls-files','--others','-z'); + rmtree(split(/\0/,$rm)) if $rm; + + # If we trust Arch with the fact that this is just + # a tag, and it does not affect the state of the tree + # then we just tag and move on + tag($ps->{id}, $branchpoint); + ptag($ps->{id}, $branchpoint); + print " * Tagged $ps->{id} at $branchpoint\n"; + return 0; + } else { + warn "Tagging from unknown id unsupported\n" if $ps->{tag}; + } + # allow multiple bases/imports here since Arch supports cherry-picks + # from unrelated trees + } + + # update the index with all the changes we got + system('git-diff-files --name-only -z | '. + 'git-update-index --remove -z --stdin') == 0 or die "$! $?\n"; + system('git-ls-files --others -z | '. + 'git-update-index --add -z --stdin') == 0 or die "$! $?\n"; + return 1; +} + +# the native changeset processing strategy. This is very fast, but +# does not handle permissions or any renames involving directories +sub process_patchset_fast { + my $ps = shift; + # + # create the branch if needed + # + if ($ps->{type} eq 'i' && !$import) { + die "Should not have more than one 'Initial import' per GIT import: $ps->{id}"; + } + + unless ($import) { # skip for import + if ( -e "$git_dir/refs/heads/$ps->{branch}") { + # we know about this branch + system('git-checkout',$ps->{branch}); + } else { + # new branch! we need to verify a few things + die "Branch on a non-tag!" unless $ps->{type} eq 't'; + my $branchpoint = ptag($ps->{tag}); + die "Tagging from unknown id unsupported: $ps->{tag}" + unless $branchpoint; + + # find where we are supposed to branch from + system('git-checkout','-b',$ps->{branch},$branchpoint); + + # If we trust Arch with the fact that this is just + # a tag, and it does not affect the state of the tree + # then we just tag and move on + tag($ps->{id}, $branchpoint); + ptag($ps->{id}, $branchpoint); + print " * Tagged $ps->{id} at $branchpoint\n"; + return 0; + } + die $! if $?; + } + + # + # Apply the import/changeset/merge into the working tree + # + if ($ps->{type} eq 'i' || $ps->{type} eq 't') { + apply_import($ps) or die $!; + $stats{import_or_tag}++; + $import=0; + } elsif ($ps->{type} eq 's') { + apply_cset($ps); + $stats{simple_changeset}++; + } + + # + # prepare update git's index, based on what arch knows + # about the pset, resolve parents, etc + # + + my @commitlog = safe_pipe_capture($TLA,'cat-archive-log',$ps->{id}); + die "Error in cat-archive-log: $!" if $?; + + parselog($ps,\@commitlog); + + # imports don't give us good info + # on added files. Shame on them + if ($ps->{type} eq 'i' || $ps->{type} eq 't') { + system('git-ls-files --deleted -z | '. + 'git-update-index --remove -z --stdin') == 0 or die "$! $?\n"; + system('git-ls-files --others -z | '. + 'git-update-index --add -z --stdin') == 0 or die "$! $?\n"; + } + + # TODO: handle removed_directories and renamed_directories: + + if (my $del = $ps->{removed_files}) { + unlink @$del; + while (@$del) { + my @slice = splice(@$del, 0, 100); + system('git-update-index','--remove','--',@slice) == 0 or + die "Error in git-update-index --remove: $! $?\n"; + } + } + + if (my $ren = $ps->{renamed_files}) { # renamed + if (@$ren % 2) { + die "Odd number of entries in rename!?"; + } + + while (@$ren) { + my $from = shift @$ren; + my $to = shift @$ren; + + unless (-d dirname($to)) { + mkpath(dirname($to)); # will die on err + } + # print "moving $from $to"; + rename($from, $to) or die "Error renaming '$from' '$to': $!\n"; + system('git-update-index','--remove','--',$from) == 0 or + die "Error in git-update-index --remove: $! $?\n"; + system('git-update-index','--add','--',$to) == 0 or + die "Error in git-update-index --add: $! $?\n"; + } + } + + if (my $add = $ps->{new_files}) { + while (@$add) { + my @slice = splice(@$add, 0, 100); + system('git-update-index','--add','--',@slice) == 0 or + die "Error in git-update-index --add: $! $?\n"; + } + } + + if (my $mod = $ps->{modified_files}) { + while (@$mod) { + my @slice = splice(@$mod, 0, 100); + system('git-update-index','--',@slice) == 0 or + die "Error in git-update-index: $! $?\n"; + } + } + return 1; # we successfully applied the changeset +} + +if ($opt_f) { + print "Will import patchsets using the fast strategy\n", + "Renamed directories and permission changes will be missed\n"; + *process_patchset = *process_patchset_fast; +} else { + print "Using the default (accurate) import strategy.\n", + "Things may be a bit slow\n"; + *process_patchset = *process_patchset_accurate; +} + +foreach my $ps (@psets) { + # process patchsets + $ps->{branch} = git_branchname($ps->{id}); + + # + # ensure we have a clean state + # + if (my $dirty = `git-diff-files`) { + die "Unclean tree when about to process $ps->{id} " . + " - did we fail to commit cleanly before?\n$dirty"; + } + die $! if $?; + + # + # skip commits already in repo + # + if (ptag($ps->{id})) { + $opt_v && print " * Skipping already imported: $ps->{id}\n"; + next; + } + + print " * Starting to work on $ps->{id}\n"; + + process_patchset($ps) or next; + + # warn "errors when running git-update-index! $!"; + my $tree = `git-write-tree`; + die "cannot write tree $!" if $?; + chomp $tree; + + # + # Who's your daddy? + # + my @par; + if ( -e "$git_dir/refs/heads/$ps->{branch}") { + if (open HEAD, "<","$git_dir/refs/heads/$ps->{branch}") { + my $p = <HEAD>; + close HEAD; + chomp $p; + push @par, '-p', $p; + } else { + if ($ps->{type} eq 's') { + warn "Could not find the right head for the branch $ps->{branch}"; + } + } + } + + if ($ps->{merges}) { + push @par, find_parents($ps); + } + + # + # Commit, tag and clean state + # + $ENV{TZ} = 'GMT'; + $ENV{GIT_AUTHOR_NAME} = $ps->{author}; + $ENV{GIT_AUTHOR_EMAIL} = $ps->{email}; + $ENV{GIT_AUTHOR_DATE} = $ps->{date}; + $ENV{GIT_COMMITTER_NAME} = $ps->{author}; + $ENV{GIT_COMMITTER_EMAIL} = $ps->{email}; + $ENV{GIT_COMMITTER_DATE} = $ps->{date}; + + my $pid = open2(*READER, *WRITER,'git-commit-tree',$tree,@par) + or die $!; + print WRITER $ps->{summary},"\n"; + print WRITER $ps->{message},"\n"; + + # make it easy to backtrack and figure out which Arch revision this was: + print WRITER 'git-archimport-id: ',$ps->{id},"\n"; + + close WRITER; + my $commitid = <READER>; # read + chomp $commitid; + close READER; + waitpid $pid,0; # close; + + if (length $commitid != 40) { + die "Something went wrong with the commit! $! $commitid"; + } + # + # Update the branch + # + open HEAD, ">","$git_dir/refs/heads/$ps->{branch}"; + print HEAD $commitid; + close HEAD; + system('git-update-ref', 'HEAD', "$ps->{branch}"); + + # tag accordingly + ptag($ps->{id}, $commitid); # private tag + if ($opt_T || $ps->{type} eq 't' || $ps->{type} eq 'i') { + tag($ps->{id}, $commitid); + } + print " * Committed $ps->{id}\n"; + print " + tree $tree\n"; + print " + commit $commitid\n"; + $opt_v && print " + commit date is $ps->{date} \n"; + $opt_v && print " + parents: ",join(' ',@par),"\n"; +} + +if ($opt_v) { + foreach (sort keys %stats) { + print" $_: $stats{$_}\n"; + } +} +exit 0; + +# used by the accurate strategy: +sub sync_to_ps { + my $ps = shift; + my $tree_dir = $tmp.'/'.tree_dirname($ps->{id}); + + $opt_v && print "sync_to_ps($ps->{id}) method: "; + + if (-d $tree_dir) { + if ($ps->{type} eq 't') { + $opt_v && print "get (tag)\n"; + # looks like a tag-only or (worse,) a mixed tags/changeset branch, + # can't rely on replay to work correctly on these + rmtree($tree_dir); + safe_pipe_capture($TLA,'get','--no-pristine',$ps->{id},$tree_dir); + $stats{get_tag}++; + } else { + my $tree_id = arch_tree_id($tree_dir); + if ($ps->{parent_id} && ($ps->{parent_id} eq $tree_id)) { + # the common case (hopefully) + $opt_v && print "replay\n"; + safe_pipe_capture($TLA,'replay','-d',$tree_dir,$ps->{id}); + $stats{replay}++; + } else { + # getting one tree is usually faster than getting two trees + # and applying the delta ... + rmtree($tree_dir); + $opt_v && print "apply-delta\n"; + safe_pipe_capture($TLA,'get','--no-pristine', + $ps->{id},$tree_dir); + $stats{get_delta}++; + } + } + } else { + # new branch work + $opt_v && print "get (new tree)\n"; + safe_pipe_capture($TLA,'get','--no-pristine',$ps->{id},$tree_dir); + $stats{get_new}++; + } + + # added -I flag to rsync since we're going to fast! AIEEEEE!!!! + system('rsync','-aI','--delete','--exclude',$git_dir, +# '--exclude','.arch-inventory', + '--exclude','.arch-ids','--exclude','{arch}', + '--exclude','+*','--exclude',',*', + "$tree_dir/",'./') == 0 or die "Cannot rsync $tree_dir: $! $?"; + return $tree_dir; +} + +sub apply_import { + my $ps = shift; + my $bname = git_branchname($ps->{id}); + + mkpath($tmp); + + safe_pipe_capture($TLA,'get','-s','--no-pristine',$ps->{id},"$tmp/import"); + die "Cannot get import: $!" if $?; + system('rsync','-aI','--delete', '--exclude',$git_dir, + '--exclude','.arch-ids','--exclude','{arch}', + "$tmp/import/", './'); + die "Cannot rsync import:$!" if $?; + + rmtree("$tmp/import"); + die "Cannot remove tempdir: $!" if $?; + + + return 1; +} + +sub apply_cset { + my $ps = shift; + + mkpath($tmp); + + # get the changeset + safe_pipe_capture($TLA,'get-changeset',$ps->{id},"$tmp/changeset"); + die "Cannot get changeset: $!" if $?; + + # apply patches + if (`find $tmp/changeset/patches -type f -name '*.patch'`) { + # this can be sped up considerably by doing + # (find | xargs cat) | patch + # but that can get mucked up by patches + # with missing trailing newlines or the standard + # 'missing newline' flag in the patch - possibly + # produced with an old/buggy diff. + # slow and safe, we invoke patch once per patchfile + `find $tmp/changeset/patches -type f -name '*.patch' -print0 | grep -zv '{arch}' | xargs -iFILE -0 --no-run-if-empty patch -p1 --forward -iFILE`; + die "Problem applying patches! $!" if $?; + } + + # apply changed binary files + if (my @modified = `find $tmp/changeset/patches -type f -name '*.modified'`) { + foreach my $mod (@modified) { + chomp $mod; + my $orig = $mod; + $orig =~ s/\.modified$//; # lazy + $orig =~ s!^\Q$tmp\E/changeset/patches/!!; + #print "rsync -p '$mod' '$orig'"; + system('rsync','-p',$mod,"./$orig"); + die "Problem applying binary changes! $!" if $?; + } + } + + # bring in new files + system('rsync','-aI','--exclude',$git_dir, + '--exclude','.arch-ids', + '--exclude', '{arch}', + "$tmp/changeset/new-files-archive/",'./'); + + # deleted files are hinted from the commitlog processing + + rmtree("$tmp/changeset"); +} + + +# =for reference +# notes: *-files/-directories keys cannot have spaces, they're always +# pika-escaped. Everything after the first newline +# A log entry looks like: +# Revision: moodle-org--moodle--1.3.3--patch-15 +# Archive: arch-eduforge@catalyst.net.nz--2004 +# Creator: Penny Leach <penny@catalyst.net.nz> +# Date: Wed May 25 14:15:34 NZST 2005 +# Standard-date: 2005-05-25 02:15:34 GMT +# New-files: lang/de/.arch-ids/block_glossary_random.php.id +# lang/de/.arch-ids/block_html.php.id +# New-directories: lang/de/help/questionnaire +# lang/de/help/questionnaire/.arch-ids +# Renamed-files: .arch-ids/db_sears.sql.id db/.arch-ids/db_sears.sql.id +# db_sears.sql db/db_sears.sql +# Removed-files: lang/be/docs/.arch-ids/release.html.id +# lang/be/docs/.arch-ids/releaseold.html.id +# Modified-files: admin/cron.php admin/delete.php +# admin/editor.html backup/lib.php backup/restore.php +# New-patches: arch-eduforge@catalyst.net.nz--2004/moodle-org--moodle--1.3.3--patch-15 +# Summary: Updating to latest from MOODLE_14_STABLE (1.4.5+) +# summary can be multiline with a leading space just like the above fields +# Keywords: +# +# Updating yadda tadda tadda madda +sub parselog { + my ($ps, $log) = @_; + my $key = undef; + + # headers we want that contain filenames: + my %want_headers = ( + new_files => 1, + modified_files => 1, + renamed_files => 1, + renamed_directories => 1, + removed_files => 1, + removed_directories => 1, + ); + + chomp (@$log); + while ($_ = shift @$log) { + if (/^Continuation-of:\s*(.*)/) { + $ps->{tag} = $1; + $key = undef; + } elsif (/^Summary:\s*(.*)$/ ) { + # summary can be multiline as long as it has a leading space + $ps->{summary} = [ $1 ]; + $key = 'summary'; + } elsif (/^Creator: (.*)\s*<([^\>]+)>/) { + $ps->{author} = $1; + $ps->{email} = $2; + $key = undef; + # any *-files or *-directories can be read here: + } elsif (/^([A-Z][a-z\-]+):\s*(.*)$/) { + my $val = $2; + $key = lc $1; + $key =~ tr/-/_/; # too lazy to quote :P + if ($want_headers{$key}) { + push @{$ps->{$key}}, split(/\s+/, $val); + } else { + $key = undef; + } + } elsif (/^$/) { + last; # remainder of @$log that didn't get shifted off is message + } elsif ($key) { + if (/^\s+(.*)$/) { + if ($key eq 'summary') { + push @{$ps->{$key}}, $1; + } else { # files/directories: + push @{$ps->{$key}}, split(/\s+/, $1); + } + } else { + $key = undef; + } + } + } + + # post-processing: + $ps->{summary} = join("\n",@{$ps->{summary}})."\n"; + $ps->{message} = join("\n",@$log); + + # skip Arch control files, unescape pika-escaped files + foreach my $k (keys %want_headers) { + next unless (defined $ps->{$k}); + my @tmp = (); + foreach my $t (@{$ps->{$k}}) { + next unless length ($t); + next if $t =~ m!\{arch\}/!; + next if $t =~ m!\.arch-ids/!; + # should we skip this? + next if $t =~ m!\.arch-inventory$!; + # tla cat-archive-log will give us filenames with spaces as file\(sp)name - why? + # we can assume that any filename with \ indicates some pika escaping that we want to get rid of. + if ($t =~ /\\/ ){ + $t = (safe_pipe_capture($TLA,'escape','--unescaped',$t))[0]; + } + push @tmp, $t; + } + $ps->{$k} = \@tmp; + } +} + +# write/read a tag +sub tag { + my ($tag, $commit) = @_; + + if ($opt_o) { + $tag =~ s|/|--|g; + } else { + # don't use subdirs for tags yet, it could screw up other porcelains + $tag =~ s|/|,|g; + } + + if ($commit) { + open(C,">","$git_dir/refs/tags/$tag") + or die "Cannot create tag $tag: $!\n"; + print C "$commit\n" + or die "Cannot write tag $tag: $!\n"; + close(C) + or die "Cannot write tag $tag: $!\n"; + print " * Created tag '$tag' on '$commit'\n" if $opt_v; + } else { # read + open(C,"<","$git_dir/refs/tags/$tag") + or die "Cannot read tag $tag: $!\n"; + $commit = <C>; + chomp $commit; + die "Error reading tag $tag: $!\n" unless length $commit == 40; + close(C) + or die "Cannot read tag $tag: $!\n"; + return $commit; + } +} + +# write/read a private tag +# reads fail softly if the tag isn't there +sub ptag { + my ($tag, $commit) = @_; + + # don't use subdirs for tags yet, it could screw up other porcelains + $tag =~ s|/|,|g; + + my $tag_file = "$ptag_dir/$tag"; + my $tag_branch_dir = dirname($tag_file); + mkpath($tag_branch_dir) unless (-d $tag_branch_dir); + + if ($commit) { # write + open(C,">",$tag_file) + or die "Cannot create tag $tag: $!\n"; + print C "$commit\n" + or die "Cannot write tag $tag: $!\n"; + close(C) + or die "Cannot write tag $tag: $!\n"; + $rptags{$commit} = $tag + unless $tag =~ m/--base-0$/; + } else { # read + # if the tag isn't there, return 0 + unless ( -s $tag_file) { + return 0; + } + open(C,"<",$tag_file) + or die "Cannot read tag $tag: $!\n"; + $commit = <C>; + chomp $commit; + die "Error reading tag $tag: $!\n" unless length $commit == 40; + close(C) + or die "Cannot read tag $tag: $!\n"; + unless (defined $rptags{$commit}) { + $rptags{$commit} = $tag; + } + return $commit; + } +} + +sub find_parents { + # + # Identify what branches are merging into me + # and whether we are fully merged + # git-merge-base <headsha> <headsha> should tell + # me what the base of the merge should be + # + my $ps = shift; + + my %branches; # holds an arrayref per branch + # the arrayref contains a list of + # merged patches between the base + # of the merge and the current head + + my @parents; # parents found for this commit + + # simple loop to split the merges + # per branch + foreach my $merge (@{$ps->{merges}}) { + my $branch = git_branchname($merge); + unless (defined $branches{$branch} ){ + $branches{$branch} = []; + } + push @{$branches{$branch}}, $merge; + } + + # + # foreach branch find a merge base and walk it to the + # head where we are, collecting the merged patchsets that + # Arch has recorded. Keep that in @have + # Compare that with the commits on the other branch + # between merge-base and the tip of the branch (@need) + # and see if we have a series of consecutive patches + # starting from the merge base. The tip of the series + # of consecutive patches merged is our new parent for + # that branch. + # + foreach my $branch (keys %branches) { + + # check that we actually know about the branch + next unless -e "$git_dir/refs/heads/$branch"; + + my $mergebase = `git-merge-base $branch $ps->{branch}`; + if ($?) { + # Don't die here, Arch supports one-way cherry-picking + # between branches with no common base (or any relationship + # at all beforehand) + warn "Cannot find merge base for $branch and $ps->{branch}"; + next; + } + chomp $mergebase; + + # now walk up to the mergepoint collecting what patches we have + my $branchtip = git_rev_parse($ps->{branch}); + my @ancestors = `git-rev-list --topo-order $branchtip ^$mergebase`; + my %have; # collected merges this branch has + foreach my $merge (@{$ps->{merges}}) { + $have{$merge} = 1; + } + my %ancestorshave; + foreach my $par (@ancestors) { + $par = commitid2pset($par); + if (defined $par->{merges}) { + foreach my $merge (@{$par->{merges}}) { + $ancestorshave{$merge}=1; + } + } + } + # print "++++ Merges in $ps->{id} are....\n"; + # my @have = sort keys %have; print Dumper(\@have); + + # merge what we have with what ancestors have + %have = (%have, %ancestorshave); + + # see what the remote branch has - these are the merges we + # will want to have in a consecutive series from the mergebase + my $otherbranchtip = git_rev_parse($branch); + my @needraw = `git-rev-list --topo-order $otherbranchtip ^$mergebase`; + my @need; + foreach my $needps (@needraw) { # get the psets + $needps = commitid2pset($needps); + # git-rev-list will also + # list commits merged in via earlier + # merges. we are only interested in commits + # from the branch we're looking at + if ($branch eq $needps->{branch}) { + push @need, $needps->{id}; + } + } + + # print "++++ Merges from $branch we want are....\n"; + # print Dumper(\@need); + + my $newparent; + while (my $needed_commit = pop @need) { + if ($have{$needed_commit}) { + $newparent = $needed_commit; + } else { + last; # break out of the while + } + } + if ($newparent) { + push @parents, $newparent; + } + + + } # end foreach branch + + # prune redundant parents + my %parents; + foreach my $p (@parents) { + $parents{$p} = 1; + } + foreach my $p (@parents) { + next unless exists $psets{$p}{merges}; + next unless ref $psets{$p}{merges}; + my @merges = @{$psets{$p}{merges}}; + foreach my $merge (@merges) { + if ($parents{$merge}) { + delete $parents{$merge}; + } + } + } + + @parents = (); + foreach (keys %parents) { + push @parents, '-p', ptag($_); + } + return @parents; +} + +sub git_rev_parse { + my $name = shift; + my $val = `git-rev-parse $name`; + die "Error: git-rev-parse $name" if $?; + chomp $val; + return $val; +} + +# resolve a SHA1 to a known patchset +sub commitid2pset { + my $commitid = shift; + chomp $commitid; + my $name = $rptags{$commitid} + || die "Cannot find reverse tag mapping for $commitid"; + $name =~ s|,|/|; + my $ps = $psets{$name} + || (print Dumper(sort keys %psets)) && die "Cannot find patchset for $name"; + return $ps; +} + + +# an alternative to `command` that allows input to be passed as an array +# to work around shell problems with weird characters in arguments +sub safe_pipe_capture { + my @output; + if (my $pid = open my $child, '-|') { + @output = (<$child>); + close $child or die join(' ',@_).": $! $?"; + } else { + exec(@_) or die "$! $?"; # exec() can fail the executable can't be found + } + return wantarray ? @output : join('',@output); +} + +# `tla logs -rf -d <dir> | head -n1` or `baz tree-id <dir>` +sub arch_tree_id { + my $dir = shift; + chomp( my $ret = (safe_pipe_capture($TLA,'logs','-rf','-d',$dir))[0] ); + return $ret; +} + +sub archive_reachable { + my $archive = shift; + return 1 if $reachable{$archive}; + return 0 if $unreachable{$archive}; + + if (system "$TLA whereis-archive $archive >/dev/null") { + if ($opt_a && (system($TLA,'register-archive', + "http://mirrors.sourcecontrol.net/$archive") == 0)) { + $reachable{$archive} = 1; + return 1; + } + print STDERR "Archive is unreachable: $archive\n"; + $unreachable{$archive} = 1; + return 0; + } else { + $reachable{$archive} = 1; + return 1; + } +} + diff --git a/git-bisect.sh b/git-bisect.sh new file mode 100755 index 0000000000..b1c3a6b1c1 --- /dev/null +++ b/git-bisect.sh @@ -0,0 +1,250 @@ +#!/bin/sh + +USAGE='[start|bad|good|next|reset|visualize|replay|log]' +LONG_USAGE='git bisect start [<pathspec>] reset bisect state and start bisection. +git bisect bad [<rev>] mark <rev> a known-bad revision. +git bisect good [<rev>...] mark <rev>... known-good revisions. +git bisect next find next bisection to test and check it out. +git bisect reset [<branch>] finish bisection search and go back to branch. +git bisect visualize show bisect status in gitk. +git bisect replay <logfile> replay bisection log +git bisect log show bisect log.' + +. git-sh-setup +require_work_tree + +sq() { + @@PERL@@ -e ' + for (@ARGV) { + s/'\''/'\'\\\\\'\''/g; + print " '\''$_'\''"; + } + print "\n"; + ' "$@" +} + +bisect_autostart() { + test -d "$GIT_DIR/refs/bisect" || { + echo >&2 'You need to start by "git bisect start"' + if test -t 0 + then + echo >&2 -n 'Do you want me to do it for you [Y/n]? ' + read yesno + case "$yesno" in + [Nn]*) + exit ;; + esac + bisect_start + else + exit 1 + fi + } +} + +bisect_start() { + # + # Verify HEAD. If we were bisecting before this, reset to the + # top-of-line master first! + # + head=$(GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD) || + die "Bad HEAD - I need a symbolic ref" + case "$head" in + refs/heads/bisect*) + if [ -s "$GIT_DIR/head-name" ]; then + branch=`cat "$GIT_DIR/head-name"` + else + branch=master + fi + git checkout $branch || exit + ;; + refs/heads/*) + [ -s "$GIT_DIR/head-name" ] && die "won't bisect on seeked tree" + echo "$head" | sed 's#^refs/heads/##' >"$GIT_DIR/head-name" + ;; + *) + die "Bad HEAD - strange symbolic ref" + ;; + esac + + # + # Get rid of any old bisect state + # + rm -f "$GIT_DIR/refs/heads/bisect" + rm -rf "$GIT_DIR/refs/bisect/" + mkdir "$GIT_DIR/refs/bisect" + { + printf "git-bisect start" + sq "$@" + } >"$GIT_DIR/BISECT_LOG" + sq "$@" >"$GIT_DIR/BISECT_NAMES" +} + +bisect_bad() { + bisect_autostart + case "$#" in + 0) + rev=$(git-rev-parse --verify HEAD) ;; + 1) + rev=$(git-rev-parse --verify "$1") ;; + *) + usage ;; + esac || exit + echo "$rev" >"$GIT_DIR/refs/bisect/bad" + echo "# bad: "$(git-show-branch $rev) >>"$GIT_DIR/BISECT_LOG" + echo "git-bisect bad $rev" >>"$GIT_DIR/BISECT_LOG" + bisect_auto_next +} + +bisect_good() { + bisect_autostart + case "$#" in + 0) revs=$(git-rev-parse --verify HEAD) || exit ;; + *) revs=$(git-rev-parse --revs-only --no-flags "$@") && + test '' != "$revs" || die "Bad rev input: $@" ;; + esac + for rev in $revs + do + rev=$(git-rev-parse --verify "$rev") || exit + echo "$rev" >"$GIT_DIR/refs/bisect/good-$rev" + echo "# good: "$(git-show-branch $rev) >>"$GIT_DIR/BISECT_LOG" + echo "git-bisect good $rev" >>"$GIT_DIR/BISECT_LOG" + done + bisect_auto_next +} + +bisect_next_check() { + next_ok=no + test -f "$GIT_DIR/refs/bisect/bad" && + case "$(cd "$GIT_DIR" && echo refs/bisect/good-*)" in + refs/bisect/good-\*) ;; + *) next_ok=yes ;; + esac + case "$next_ok,$1" in + no,) false ;; + no,fail) + echo >&2 'You need to give me at least one good and one bad revisions.' + exit 1 ;; + *) + true ;; + esac +} + +bisect_auto_next() { + bisect_next_check && bisect_next || : +} + +bisect_next() { + case "$#" in 0) ;; *) usage ;; esac + bisect_autostart + bisect_next_check fail + bad=$(git-rev-parse --verify refs/bisect/bad) && + good=$(git-rev-parse --sq --revs-only --not \ + $(cd "$GIT_DIR" && ls refs/bisect/good-*)) && + rev=$(eval "git-rev-list --bisect $good $bad -- $(cat $GIT_DIR/BISECT_NAMES)") || exit + if [ -z "$rev" ]; then + echo "$bad was both good and bad" + exit 1 + fi + if [ "$rev" = "$bad" ]; then + echo "$rev is first bad commit" + git-diff-tree --pretty $rev + exit 0 + fi + nr=$(eval "git-rev-list $rev $good -- $(cat $GIT_DIR/BISECT_NAMES)" | wc -l) || exit + echo "Bisecting: $nr revisions left to test after this" + echo "$rev" > "$GIT_DIR/refs/heads/new-bisect" + git checkout -q new-bisect || exit + mv "$GIT_DIR/refs/heads/new-bisect" "$GIT_DIR/refs/heads/bisect" && + GIT_DIR="$GIT_DIR" git-symbolic-ref HEAD refs/heads/bisect + git-show-branch "$rev" +} + +bisect_visualize() { + bisect_next_check fail + not=`cd "$GIT_DIR/refs" && echo bisect/good-*` + eval gitk bisect/bad --not $not -- $(cat "$GIT_DIR/BISECT_NAMES") +} + +bisect_reset() { + case "$#" in + 0) if [ -s "$GIT_DIR/head-name" ]; then + branch=`cat "$GIT_DIR/head-name"` + else + branch=master + fi ;; + 1) test -f "$GIT_DIR/refs/heads/$1" || { + echo >&2 "$1 does not seem to be a valid branch" + exit 1 + } + branch="$1" ;; + *) + usage ;; + esac + if git checkout "$branch"; then + rm -fr "$GIT_DIR/refs/bisect" + rm -f "$GIT_DIR/refs/heads/bisect" "$GIT_DIR/head-name" + rm -f "$GIT_DIR/BISECT_LOG" + rm -f "$GIT_DIR/BISECT_NAMES" + fi +} + +bisect_replay () { + test -r "$1" || { + echo >&2 "cannot read $1 for replaying" + exit 1 + } + bisect_reset + while read bisect command rev + do + test "$bisect" = "git-bisect" || continue + case "$command" in + start) + cmd="bisect_start $rev" + eval "$cmd" + ;; + good) + echo "$rev" >"$GIT_DIR/refs/bisect/good-$rev" + echo "# good: "$(git-show-branch $rev) >>"$GIT_DIR/BISECT_LOG" + echo "git-bisect good $rev" >>"$GIT_DIR/BISECT_LOG" + ;; + bad) + echo "$rev" >"$GIT_DIR/refs/bisect/bad" + echo "# bad: "$(git-show-branch $rev) >>"$GIT_DIR/BISECT_LOG" + echo "git-bisect bad $rev" >>"$GIT_DIR/BISECT_LOG" + ;; + *) + echo >&2 "?? what are you talking about?" + exit 1 ;; + esac + done <"$1" + bisect_auto_next +} + +case "$#" in +0) + usage ;; +*) + cmd="$1" + shift + case "$cmd" in + start) + bisect_start "$@" ;; + bad) + bisect_bad "$@" ;; + good) + bisect_good "$@" ;; + next) + # Not sure we want "next" at the UI level anymore. + bisect_next "$@" ;; + visualize) + bisect_visualize "$@" ;; + reset) + bisect_reset "$@" ;; + replay) + bisect_replay "$@" ;; + log) + cat "$GIT_DIR/BISECT_LOG" ;; + *) + usage ;; + esac +esac diff --git a/git-checkout.sh b/git-checkout.sh new file mode 100755 index 0000000000..14835a4aa9 --- /dev/null +++ b/git-checkout.sh @@ -0,0 +1,272 @@ +#!/bin/sh + +USAGE='[-q] [-f] [-b <new_branch>] [-m] [<branch>] [<paths>...]' +SUBDIRECTORY_OK=Sometimes +. git-sh-setup +require_work_tree + +old_name=HEAD +old=$(git-rev-parse --verify $old_name 2>/dev/null) +oldbranch=$(git-symbolic-ref $old_name 2>/dev/null) +new= +new_name= +force= +branch= +newbranch= +newbranch_log= +merge= +quiet= +LF=' +' +while [ "$#" != "0" ]; do + arg="$1" + shift + case "$arg" in + "-b") + newbranch="$1" + shift + [ -z "$newbranch" ] && + die "git checkout: -b needs a branch name" + git-show-ref --verify --quiet -- "refs/heads/$newbranch" && + die "git checkout: branch $newbranch already exists" + 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 + ;; + -m) + merge=1 + ;; + "-q") + quiet=1 + ;; + --) + break + ;; + -*) + usage + ;; + *) + if rev=$(git-rev-parse --verify "$arg^0" 2>/dev/null) + then + if [ -z "$rev" ]; then + echo "unknown flag $arg" + exit 1 + fi + new="$rev" + new_name="$arg" + if git-show-ref --verify --quiet -- "refs/heads/$arg" + then + branch="$arg" + fi + elif rev=$(git-rev-parse --verify "$arg^{tree}" 2>/dev/null) + 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 + fi + case "$1" in + --) + shift ;; + esac + break + ;; + esac +done + +case "$force$merge" in +11) + die "git checkout: -f and -m are incompatible" +esac + +# The behaviour of the command with and without explicit path +# parameters is quite different. +# +# Without paths, we are checking out everything in the work tree, +# possibly switching branches. This is the traditional behaviour. +# +# With paths, we are _never_ switching branch, but checking out +# the named paths from either index (when no rev is given), +# or the named tree-ish (when rev is given). + +if test "$#" -ge 1 +then + hint= + if test "$#" -eq 1 + then + hint=" +Did you intend to checkout '$@' which can not be resolved as commit?" + fi + if test '' != "$newbranch$force$merge" + then + die "git checkout: updating paths is incompatible with switching branches/forcing$hint" + fi + if test '' != "$new" + then + # from a specific tree-ish; note that this is for + # rescuing paths and is never meant to remove what + # is not in the named tree-ish. + git-ls-tree --full-name -r "$new" "$@" | + git-update-index --index-info || exit $? + fi + + # Make sure the request is about existing paths. + git-ls-files --error-unmatch -- "$@" >/dev/null || exit + git-ls-files -- "$@" | + git-checkout-index -f -u --stdin + exit $? +else + # Make sure we did not fall back on $arg^{tree} codepath + # since we are not checking out from an arbitrary tree-ish, + # but switching branches. + if test '' != "$new" + then + git-rev-parse --verify "$new^{commit}" >/dev/null 2>&1 || + die "Cannot switch branch to a non-commit." + fi +fi + +# We are switching branches and checking out trees, so +# we *NEED* to be at the toplevel. +cd_to_toplevel + +[ -z "$new" ] && new=$old && new_name="$old_name" + +# If we don't have an existing branch that we're switching to, +# and we don't have a new branch name for the target we +# are switching to, then we are detaching our HEAD from any +# branch. However, if "git checkout HEAD" detaches the HEAD +# from the current branch, even though that may be logically +# correct, it feels somewhat funny. More importantly, we do not +# want "git checkout" nor "git checkout -f" to detach HEAD. + +detached= +detach_warn= + +if test -z "$branch$newbranch" && test "$new" != "$old" +then + detached="$new" + if test -n "$oldbranch" && test -z "$quiet" + then + detach_warn="Note: moving to \"$new_name\" which isn't a local branch +If you want to create a new branch from this checkout, you may do so +(now or later) by using -b with the checkout command again. Example: + git checkout -b <new_branch_name>" + fi +elif test -z "$oldbranch" && test -z "$quiet" +then + echo >&2 "Previous HEAD position was $old" +fi + +if [ "X$old" = X ] +then + if test -z "$quiet" + then + echo >&2 "warning: You appear to be on a branch yet to be born." + echo >&2 "warning: Forcing checkout of $new_name." + fi + force=1 +fi + +if [ "$force" ] +then + git-read-tree --reset -u $new +else + git-update-index --refresh >/dev/null + merge_error=$(git-read-tree -m -u --exclude-per-directory=.gitignore $old $new 2>&1) || ( + case "$merge" in + '') + echo >&2 "$merge_error" + exit 1 ;; + esac + + # Match the index to the working tree, and do a three-way. + git diff-files --name-only | git update-index --remove --stdin && + work=`git write-tree` && + git read-tree --reset -u $new || exit + + eval GITHEAD_$new=${new_name:-${branch:-$new}} && + eval GITHEAD_$work=local && + export GITHEAD_$new GITHEAD_$work && + git merge-recursive $old -- $new $work + + # Do not register the cleanly merged paths in the index yet. + # this is not a real merge before committing, but just carrying + # the working tree changes along. + unmerged=`git ls-files -u` + git read-tree --reset $new + case "$unmerged" in + '') ;; + *) + ( + z40=0000000000000000000000000000000000000000 + echo "$unmerged" | + sed -e 's/^[0-7]* [0-9a-f]* /'"0 $z40 /" + echo "$unmerged" + ) | git update-index --index-info + ;; + esac + exit 0 + ) + saved_err=$? + if test "$saved_err" = 0 && test -z "$quiet" + then + git diff-index --name-status "$new" + fi + (exit $saved_err) +fi + +# +# Switch the HEAD pointer to the new branch if we +# checked out a branch head, and remove any potential +# old MERGE_HEAD's (subsequent commits will clearly not +# be based on them, since we re-set the index) +# +if [ "$?" -eq 0 ]; then + if [ "$newbranch" ]; then + 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 + if test -n "$branch" + then + GIT_DIR="$GIT_DIR" git-symbolic-ref -m "checkout: moving to $branch" HEAD "refs/heads/$branch" + if test -z "$quiet" + then + echo >&2 "Switched to${newbranch:+ a new} branch \"$branch\"" + fi + elif test -n "$detached" + then + # NEEDSWORK: we would want a command to detach the HEAD + # atomically, instead of this handcrafted command sequence. + # Perhaps: + # git update-ref --detach HEAD $new + # or something like that... + # + git-rev-parse HEAD >"$GIT_DIR/HEAD.new" && + mv "$GIT_DIR/HEAD.new" "$GIT_DIR/HEAD" && + git-update-ref -m "checkout: moving to $arg" HEAD "$detached" || + die "Cannot detach HEAD" + if test -n "$detach_warn" + then + echo >&2 "$detach_warn" + fi + fi + rm -f "$GIT_DIR/MERGE_HEAD" +else + exit 1 +fi diff --git a/git-clean.sh b/git-clean.sh new file mode 100755 index 0000000000..db177a7886 --- /dev/null +++ b/git-clean.sh @@ -0,0 +1,88 @@ +#!/bin/sh +# +# Copyright (c) 2005-2006 Pavel Roskin +# + +USAGE="[-d] [-n] [-q] [-x | -X] [--] <paths>..." +LONG_USAGE='Clean untracked files from the working directory + -d remove directories as well + -n don'\''t remove anything, just show what would be done + -q be quiet, only report errors + -x remove ignored files as well + -X remove only ignored files +When optional <paths>... arguments are given, the paths +affected are further limited to those that match them.' +SUBDIRECTORY_OK=Yes +. git-sh-setup +require_work_tree + +ignored= +ignoredonly= +cleandir= +rmf="rm -f --" +rmrf="rm -rf --" +rm_refuse="echo Not removing" +echo1="echo" + +while case "$#" in 0) break ;; esac +do + case "$1" in + -d) + cleandir=1 + ;; + -n) + rmf="echo Would remove" + rmrf="echo Would remove" + rm_refuse="echo Would not remove" + echo1=":" + ;; + -q) + echo1=":" + ;; + -x) + ignored=1 + ;; + -X) + ignoredonly=1 + ;; + --) + shift + break + ;; + -*) + usage + ;; + *) + break + esac + shift +done + +case "$ignored,$ignoredonly" in + 1,1) usage;; +esac + +if [ -z "$ignored" ]; then + excl="--exclude-per-directory=.gitignore" + if [ -f "$GIT_DIR/info/exclude" ]; then + excl_info="--exclude-from=$GIT_DIR/info/exclude" + fi + if [ "$ignoredonly" ]; then + excl="$excl --ignored" + fi +fi + +git-ls-files --others --directory $excl ${excl_info:+"$excl_info"} -- "$@" | +while read -r file; do + if [ -d "$file" -a ! -L "$file" ]; then + if [ -z "$cleandir" ]; then + $rm_refuse "$file" + continue + fi + $echo1 "Removing $file" + $rmrf "$file" + else + $echo1 "Removing $file" + $rmf "$file" + fi +done diff --git a/git-clone.sh b/git-clone.sh new file mode 100755 index 0000000000..1bd54ded3c --- /dev/null +++ b/git-clone.sh @@ -0,0 +1,403 @@ +#!/bin/sh +# +# Copyright (c) 2005, Linus Torvalds +# Copyright (c) 2005, Junio C Hamano +# +# Clone a repository into a different directory that does not yet exist. + +# See git-sh-setup why. +unset CDPATH + +die() { + echo >&2 "$@" + exit 1 +} + +usage() { + die "Usage: $0 [--template=<template_directory>] [--reference <reference-repo>] [--bare] [-l [-s]] [-q] [-u <upload-pack>] [--origin <name>] [--depth <n>] [-n] <repo> [<dir>]" +} + +get_repo_base() { + (cd "$1" && (cd .git ; pwd)) 2> /dev/null +} + +if [ -n "$GIT_SSL_NO_VERIFY" ]; then + curl_extra_args="-k" +fi + +http_fetch () { + # $1 = Remote, $2 = Local + curl -nsfL $curl_extra_args "$1" >"$2" +} + +clone_dumb_http () { + # $1 - remote, $2 - local + cd "$2" && + clone_tmp="$GIT_DIR/clone-tmp" && + mkdir -p "$clone_tmp" || exit 1 + if [ -n "$GIT_CURL_FTP_NO_EPSV" -o \ + "`git-config --bool http.noEPSV`" = true ]; then + curl_extra_args="${curl_extra_args} --disable-epsv" + fi + http_fetch "$1/info/refs" "$clone_tmp/refs" || + die "Cannot get remote repository information. +Perhaps git-update-server-info needs to be run there?" + while read sha1 refname + do + name=`expr "z$refname" : 'zrefs/\(.*\)'` && + case "$name" in + *^*) continue;; + esac + case "$bare,$name" in + yes,* | ,heads/* | ,tags/*) ;; + *) continue ;; + esac + if test -n "$use_separate_remote" && + branch_name=`expr "z$name" : 'zheads/\(.*\)'` + then + tname="remotes/$origin/$branch_name" + else + tname=$name + fi + git-http-fetch -v -a -w "$tname" "$name" "$1/" || exit 1 + done <"$clone_tmp/refs" + rm -fr "$clone_tmp" + http_fetch "$1/HEAD" "$GIT_DIR/REMOTE_HEAD" || + rm -f "$GIT_DIR/REMOTE_HEAD" +} + +quiet= +local=no +use_local=no +local_shared=no +unset template +no_checkout= +upload_pack= +bare= +reference= +origin= +origin_override= +use_separate_remote=t +depth= +while + case "$#,$1" in + 0,*) break ;; + *,-n|*,--no|*,--no-|*,--no-c|*,--no-ch|*,--no-che|*,--no-chec|\ + *,--no-check|*,--no-checko|*,--no-checkou|*,--no-checkout) + no_checkout=yes ;; + *,--na|*,--nak|*,--nake|*,--naked|\ + *,-b|*,--b|*,--ba|*,--bar|*,--bare) bare=yes ;; + *,-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) ;; + *,--no-separate-remote) + die "clones are always made with separate-remote layout" ;; + 1,--reference) usage ;; + *,--reference) + shift; reference="$1" ;; + *,--reference=*) + reference=`expr "z$1" : 'z--reference=\(.*\)'` ;; + *,-o|*,--or|*,--ori|*,--orig|*,--origi|*,--origin) + case "$2" in + '') + usage ;; + */*) + die "'$2' is not suitable for an origin name" + esac + git-check-ref-format "heads/$2" || + die "'$2' is not suitable for a branch name" + test -z "$origin_override" || + die "Do not give more than one --origin options." + origin_override=yes + origin="$2"; shift + ;; + 1,-u|1,--upload-pack) usage ;; + *,-u|*,--upload-pack) + shift + upload_pack="--upload-pack=$1" ;; + *,--upload-pack=*) + upload_pack=--upload-pack=$(expr "z$1" : 'z-[^=]*=\(.*\)') ;; + 1,--depth) usage;; + *,--depth) + shift + depth="--depth=$1";; + *,-*) usage ;; + *) break ;; + esac +do + shift +done + +repo="$1" +test -n "$repo" || + die 'you must specify a repository to clone.' + +# --bare implies --no-checkout and --no-separate-remote +if test yes = "$bare" +then + if test yes = "$origin_override" + then + die '--bare and --origin $origin options are incompatible.' + fi + no_checkout=yes + use_separate_remote= +fi + +if test -z "$origin" +then + origin=origin +fi + +# Turn the source into an absolute path if +# it is local +if base=$(get_repo_base "$repo"); then + repo="$base" + local=yes +fi + +dir="$2" +# Try using "humanish" part of source repo if user didn't specify one +[ -z "$dir" ] && dir=$(echo "$repo" | sed -e 's|/$||' -e 's|:*/*\.git$||' -e 's|.*[/:]||g') +[ -e "$dir" ] && die "destination directory '$dir' already exists." +mkdir -p "$dir" && +D=$(cd "$dir" && pwd) && +trap 'err=$?; cd ..; rm -rf "$D"; exit $err' 0 +case "$bare" in +yes) + GIT_DIR="$D" ;; +*) + GIT_DIR="$D/.git" ;; +esac && export GIT_DIR && git-init ${template+"$template"} || usage + +if test -n "$reference" +then + ref_git= + if test -d "$reference" + then + if test -d "$reference/.git/objects" + then + ref_git="$reference/.git" + elif test -d "$reference/objects" + then + ref_git="$reference" + fi + fi + if test -n "$ref_git" + then + ref_git=$(cd "$ref_git" && pwd) + echo "$ref_git/objects" >"$GIT_DIR/objects/info/alternates" + ( + GIT_DIR="$ref_git" git for-each-ref \ + --format='%(objectname) %(*objectname)' + ) | + while read a b + do + test -z "$a" || + git update-ref "refs/reference-tmp/$a" "$a" + test -z "$b" || + git update-ref "refs/reference-tmp/$b" "$b" + done + else + die "reference repository '$reference' is not a local directory." + fi +fi + +rm -f "$GIT_DIR/CLONE_HEAD" + +# We do local magic only when the user tells us to. +case "$local,$use_local" in +yes,yes) + ( cd "$repo/objects" ) || + die "-l flag seen but repository '$repo' is not local." + + case "$local_shared" in + no) + # See if we can hardlink and drop "l" if not. + sample_file=$(cd "$repo" && \ + find objects -type f -print | sed -e 1q) + + # objects directory should not be empty since we are cloning! + test -f "$repo/$sample_file" || exit + + l= + if ln "$repo/$sample_file" "$GIT_DIR/objects/sample" 2>/dev/null + then + l=l + fi && + rm -f "$GIT_DIR/objects/sample" && + cd "$repo" && + find objects -depth -print | cpio -pumd$l "$GIT_DIR/" || exit 1 + ;; + yes) + mkdir -p "$GIT_DIR/objects/info" + echo "$repo/objects" >> "$GIT_DIR/objects/info/alternates" + ;; + esac + git-ls-remote "$repo" >"$GIT_DIR/CLONE_HEAD" || exit 1 + ;; +*) + case "$repo" in + rsync://*) + case "$depth" in + "") ;; + *) die "shallow over rsync not supported" ;; + esac + rsync $quiet -av --ignore-existing \ + --exclude info "$repo/objects/" "$GIT_DIR/objects/" || + exit + # Look at objects/info/alternates for rsync -- http will + # support it natively and git native ones will do it on the + # remote end. Not having that file is not a crime. + rsync -q "$repo/objects/info/alternates" \ + "$GIT_DIR/TMP_ALT" 2>/dev/null || + rm -f "$GIT_DIR/TMP_ALT" + if test -f "$GIT_DIR/TMP_ALT" + then + ( cd "$D" && + . git-parse-remote && + resolve_alternates "$repo" <"$GIT_DIR/TMP_ALT" ) | + while read alt + do + case "$alt" in 'bad alternate: '*) die "$alt";; esac + case "$quiet" in + '') echo >&2 "Getting alternate: $alt" ;; + esac + rsync $quiet -av --ignore-existing \ + --exclude info "$alt" "$GIT_DIR/objects" || exit + done + rm -f "$GIT_DIR/TMP_ALT" + fi + git-ls-remote "$repo" >"$GIT_DIR/CLONE_HEAD" || exit 1 + ;; + https://*|http://*|ftp://*) + case "$depth" in + "") ;; + *) die "shallow over http or ftp not supported" ;; + esac + if test -z "@@NO_CURL@@" + then + clone_dumb_http "$repo" "$D" + else + die "http transport not supported, rebuild Git with curl support" + fi + ;; + *) + case "$upload_pack" in + '') git-fetch-pack --all -k $quiet $depth "$repo" ;; + *) git-fetch-pack --all -k $quiet "$upload_pack" $depth "$repo" ;; + esac >"$GIT_DIR/CLONE_HEAD" || + die "fetch-pack from '$repo' failed." + ;; + esac + ;; +esac +test -d "$GIT_DIR/refs/reference-tmp" && rm -fr "$GIT_DIR/refs/reference-tmp" + +if test -f "$GIT_DIR/CLONE_HEAD" +then + # Read git-fetch-pack -k output and store the remote branches. + if [ -n "$use_separate_remote" ] + then + branch_top="remotes/$origin" + else + branch_top="heads" + fi + tag_top="tags" + while read sha1 name + do + case "$name" in + *'^{}') + continue ;; + HEAD) + destname="REMOTE_HEAD" ;; + refs/heads/*) + destname="refs/$branch_top/${name#refs/heads/}" ;; + refs/tags/*) + destname="refs/$tag_top/${name#refs/tags/}" ;; + *) + continue ;; + esac + git-update-ref -m "clone: from $repo" "$destname" "$sha1" "" + done < "$GIT_DIR/CLONE_HEAD" +fi + +cd "$D" || exit + +if test -z "$bare" && test -f "$GIT_DIR/REMOTE_HEAD" +then + # a non-bare repository is always in separate-remote layout + remote_top="refs/remotes/$origin" + head_sha1=`cat "$GIT_DIR/REMOTE_HEAD"` + case "$head_sha1" in + 'ref: refs/'*) + # Uh-oh, the remote told us (http transport done against + # new style repository with a symref HEAD). + # Ideally we should skip the guesswork but for now + # opt for minimum change. + head_sha1=`expr "z$head_sha1" : 'zref: refs/heads/\(.*\)'` + head_sha1=`cat "$GIT_DIR/$remote_top/$head_sha1"` + ;; + esac + + # The name under $remote_top the remote HEAD seems to point at. + head_points_at=$( + ( + test -f "$GIT_DIR/$remote_top/master" && echo "master" + cd "$GIT_DIR/$remote_top" && + find . -type f -print | sed -e 's/^\.\///' + ) | ( + done=f + while read name + do + test t = $done && continue + branch_tip=`cat "$GIT_DIR/$remote_top/$name"` + if test "$head_sha1" = "$branch_tip" + then + echo "$name" + done=t + fi + done + ) + ) + + # Write out remote.$origin config, and update our "$head_points_at". + case "$head_points_at" in + ?*) + # Local default branch + git-symbolic-ref HEAD "refs/heads/$head_points_at" && + + # Tracking branch for the primary branch at the remote. + origin_track="$remote_top/$head_points_at" && + git-update-ref HEAD "$head_sha1" && + + # Upstream URL + git-config remote."$origin".url "$repo" && + + # Set up the mappings to track the remote branches. + git-config remote."$origin".fetch \ + "+refs/heads/*:$remote_top/*" '^$' && + rm -f "refs/remotes/$origin/HEAD" + git-symbolic-ref "refs/remotes/$origin/HEAD" \ + "refs/remotes/$origin/$head_points_at" && + + git-config branch."$head_points_at".remote "$origin" && + git-config branch."$head_points_at".merge "refs/heads/$head_points_at" + esac + + case "$no_checkout" in + '') + test "z$quiet" = z && v=-v || v= + git-read-tree -m -u $v HEAD HEAD + esac +fi +rm -f "$GIT_DIR/CLONE_HEAD" "$GIT_DIR/REMOTE_HEAD" + +trap - 0 + diff --git a/git-commit.sh b/git-commit.sh new file mode 100755 index 0000000000..ec506d956f --- /dev/null +++ b/git-commit.sh @@ -0,0 +1,638 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# Copyright (c) 2006 Junio C Hamano + +USAGE='[-a] [-s] [-v] [--no-verify] [-m <message> | -F <logfile> | (-C|-c) <commit> | --amend] [-u] [-e] [--author <author>] [[-i | -o] <path>...]' +SUBDIRECTORY_OK=Yes +. git-sh-setup +require_work_tree + +git-rev-parse --verify HEAD >/dev/null 2>&1 || initial_commit=t + +case "$0" in +*status) + status_only=t + unmerged_ok_if_status=--unmerged ;; +*commit) + status_only= + unmerged_ok_if_status= ;; +esac + +refuse_partial () { + echo >&2 "$1" + echo >&2 "You might have meant to say 'git commit -i paths...', perhaps?" + exit 1 +} + +THIS_INDEX="$GIT_DIR/index" +NEXT_INDEX="$GIT_DIR/next-index$$" +rm -f "$NEXT_INDEX" +save_index () { + cp -p "$THIS_INDEX" "$NEXT_INDEX" +} + +run_status () { + # If TMP_INDEX is defined, that means we are doing + # "--only" partial commit, and that index file is used + # to build the tree for the commit. Otherwise, if + # NEXT_INDEX exists, that is the index file used to + # make the commit. Otherwise we are using as-is commit + # so the regular index file is what we use to compare. + if test '' != "$TMP_INDEX" + then + GIT_INDEX_FILE="$TMP_INDEX" + export GIT_INDEX_FILE + elif test -f "$NEXT_INDEX" + then + GIT_INDEX_FILE="$NEXT_INDEX" + export GIT_INDEX_FILE + fi + + case "$status_only" in + t) color= ;; + *) color=--nocolor ;; + esac + git-runstatus ${color} \ + ${verbose:+--verbose} \ + ${amend:+--amend} \ + ${untracked_files:+--untracked} +} + +trap ' + test -z "$TMP_INDEX" || { + test -f "$TMP_INDEX" && rm -f "$TMP_INDEX" + } + rm -f "$NEXT_INDEX" +' 0 + +################################################################ +# Command line argument parsing and sanity checking + +all= +also= +only= +logfile= +use_commit= +amend= +edit_flag= +no_edit= +log_given= +log_message= +verify=t +quiet= +verbose= +signoff= +force_author= +only_include_assumed= +untracked_files= +while case "$#" in 0) break;; esac +do + case "$1" in + -F|--F|-f|--f|--fi|--fil|--file) + case "$#" in 1) usage ;; esac + shift + no_edit=t + log_given=t$log_given + logfile="$1" + shift + ;; + -F*|-f*) + no_edit=t + log_given=t$log_given + logfile=`expr "z$1" : 'z-[Ff]\(.*\)'` + shift + ;; + --F=*|--f=*|--fi=*|--fil=*|--file=*) + no_edit=t + log_given=t$log_given + logfile=`expr "z$1" : 'z-[^=]*=\(.*\)'` + shift + ;; + -a|--a|--al|--all) + all=t + shift + ;; + --au=*|--aut=*|--auth=*|--autho=*|--author=*) + force_author=`expr "z$1" : 'z-[^=]*=\(.*\)'` + shift + ;; + --au|--aut|--auth|--autho|--author) + case "$#" in 1) usage ;; esac + shift + force_author="$1" + shift + ;; + -e|--e|--ed|--edi|--edit) + edit_flag=t + shift + ;; + -i|--i|--in|--inc|--incl|--inclu|--includ|--include) + also=t + shift + ;; + -o|--o|--on|--onl|--only) + only=t + shift + ;; + -m|--m|--me|--mes|--mess|--messa|--messag|--message) + case "$#" in 1) usage ;; esac + shift + 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=m$log_given + if test "$log_message" = '' + then + log_message=`expr "z$1" : 'z-m\(.*\)'` + else + log_message="$log_message + +`expr "z$1" : 'z-m\(.*\)'`" + fi + no_edit=t + shift + ;; + --m=*|--me=*|--mes=*|--mess=*|--messa=*|--messag=*|--message=*) + log_given=m$log_given + if test "$log_message" = '' + then + log_message=`expr "z$1" : 'z-[^=]*=\(.*\)'` + else + log_message="$log_message + +`expr "z$1" : 'zq-[^=]*=\(.*\)'`" + fi + no_edit=t + shift + ;; + -n|--n|--no|--no-|--no-v|--no-ve|--no-ver|--no-veri|--no-verif|\ + --no-verify) + verify= + shift + ;; + --a|--am|--ame|--amen|--amend) + amend=t + log_given=t$log_given + use_commit=HEAD + shift + ;; + -c) + case "$#" in 1) usage ;; esac + shift + log_given=t$log_given + use_commit="$1" + no_edit= + shift + ;; + --ree=*|--reed=*|--reedi=*|--reedit=*|--reedit-=*|--reedit-m=*|\ + --reedit-me=*|--reedit-mes=*|--reedit-mess=*|--reedit-messa=*|\ + --reedit-messag=*|--reedit-message=*) + log_given=t$log_given + use_commit=`expr "z$1" : 'z-[^=]*=\(.*\)'` + no_edit= + shift + ;; + --ree|--reed|--reedi|--reedit|--reedit-|--reedit-m|--reedit-me|\ + --reedit-mes|--reedit-mess|--reedit-messa|--reedit-messag|\ + --reedit-message) + case "$#" in 1) usage ;; esac + shift + log_given=t$log_given + use_commit="$1" + no_edit= + shift + ;; + -C) + case "$#" in 1) usage ;; esac + shift + log_given=t$log_given + use_commit="$1" + no_edit=t + shift + ;; + --reu=*|--reus=*|--reuse=*|--reuse-=*|--reuse-m=*|--reuse-me=*|\ + --reuse-mes=*|--reuse-mess=*|--reuse-messa=*|--reuse-messag=*|\ + --reuse-message=*) + log_given=t$log_given + use_commit=`expr "z$1" : 'z-[^=]*=\(.*\)'` + no_edit=t + shift + ;; + --reu|--reus|--reuse|--reuse-|--reuse-m|--reuse-me|--reuse-mes|\ + --reuse-mess|--reuse-messa|--reuse-messag|--reuse-message) + case "$#" in 1) usage ;; esac + shift + log_given=t$log_given + use_commit="$1" + no_edit=t + shift + ;; + -s|--s|--si|--sig|--sign|--signo|--signof|--signoff) + signoff=t + shift + ;; + -q|--q|--qu|--qui|--quie|--quiet) + quiet=t + shift + ;; + -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) + verbose=t + shift + ;; + -u|--u|--un|--unt|--untr|--untra|--untrac|--untrack|--untracke|\ + --untracked|--untracked-|--untracked-f|--untracked-fi|--untracked-fil|\ + --untracked-file|--untracked-files) + untracked_files=t + shift + ;; + --) + shift + break + ;; + -*) + usage + ;; + *) + break + ;; + esac +done +case "$edit_flag" in t) no_edit= ;; esac + +################################################################ +# Sanity check options + +case "$amend,$initial_commit" in +t,t) + die "You do not have anything to amend." ;; +t,) + if [ -f "$GIT_DIR/MERGE_HEAD" ]; then + die "You are in the middle of a merge -- cannot amend." + fi ;; +esac + +case "$log_given" in +tt*) + die "Only one of -c/-C/-F/--amend can be used." ;; +*tm*|*mt*) + die "Option -m cannot be combined with -c/-C/-F/--amend." ;; +esac + +case "$#,$also,$only,$amend" in +*,t,t,*) + die "Only one of --include/--only can be used." ;; +0,t,,* | 0,,t,) + die "No paths with --include/--only does not make sense." ;; +0,,t,t) + only_include_assumed="# Clever... amending the last one with dirty index." ;; +0,,,*) + ;; +*,,,*) + only_include_assumed="# Explicit paths specified without -i nor -o; assuming --only paths..." + also= + ;; +esac +unset only +case "$all,$also,$#" in +t,t,*) + die "Cannot use -a and -i at the same time." ;; +t,,[1-9]*) + die "Paths with -a does not make sense." ;; +,t,0) + die "No paths with -i does not make sense." ;; +esac + +################################################################ +# Prepare index to have a tree to be committed + +case "$all,$also" in +t,) + save_index && + ( + cd_to_toplevel && + GIT_INDEX_FILE="$NEXT_INDEX" && + export GIT_INDEX_FILE && + git-diff-files --name-only -z | + git-update-index --remove -z --stdin + ) || exit + ;; +,t) + save_index && + git-ls-files --error-unmatch -- "$@" >/dev/null || exit + + git-diff-files --name-only -z -- "$@" | + ( + cd_to_toplevel && + GIT_INDEX_FILE="$NEXT_INDEX" && + export GIT_INDEX_FILE && + git-update-index --remove -z --stdin + ) || exit + ;; +,) + case "$#" in + 0) + ;; # commit as-is + *) + if test -f "$GIT_DIR/MERGE_HEAD" + then + refuse_partial "Cannot do a partial commit during a merge." + fi + TMP_INDEX="$GIT_DIR/tmp-index$$" + commit_only=`git-ls-files --error-unmatch -- "$@"` || exit + + # Build a temporary index and update the real index + # the same way. + if test -z "$initial_commit" + then + cp "$THIS_INDEX" "$TMP_INDEX" + GIT_INDEX_FILE="$TMP_INDEX" git-read-tree -m HEAD + else + rm -f "$TMP_INDEX" + fi || exit + + echo "$commit_only" | + GIT_INDEX_FILE="$TMP_INDEX" \ + git-update-index --add --remove --stdin && + + save_index && + echo "$commit_only" | + ( + GIT_INDEX_FILE="$NEXT_INDEX" + export GIT_INDEX_FILE + git-update-index --remove --stdin + ) || exit + ;; + esac + ;; +esac + +################################################################ +# If we do as-is commit, the index file will be THIS_INDEX, +# otherwise NEXT_INDEX after we make this commit. We leave +# the index as is if we abort. + +if test -f "$NEXT_INDEX" +then + USE_INDEX="$NEXT_INDEX" +else + USE_INDEX="$THIS_INDEX" +fi + +GIT_INDEX_FILE="$USE_INDEX" \ + git-update-index -q $unmerged_ok_if_status --refresh || exit + +################################################################ +# If the request is status, just show it and exit. + +case "$0" in +*status) + run_status + exit $? +esac + +################################################################ +# Grab commit message, write out tree and make commit. + +if test t = "$verify" && test -x "$GIT_DIR"/hooks/pre-commit +then + if test "$TMP_INDEX" + then + GIT_INDEX_FILE="$TMP_INDEX" "$GIT_DIR"/hooks/pre-commit + else + GIT_INDEX_FILE="$USE_INDEX" "$GIT_DIR"/hooks/pre-commit + fi || exit +fi + +if test "$log_message" != '' +then + echo "$log_message" +elif test "$logfile" != "" +then + if test "$logfile" = - + then + test -t 0 && + echo >&2 "(reading log message from standard input)" + cat + else + cat <"$logfile" + fi +elif test "$use_commit" != "" +then + encoding=$(git config i18n.commitencoding || echo UTF-8) + git show -s --pretty=raw --encoding="$encoding" "$use_commit" | + sed -e '1,/^$/d' -e 's/^ //' +elif test -f "$GIT_DIR/MERGE_MSG" +then + cat "$GIT_DIR/MERGE_MSG" +elif test -f "$GIT_DIR/SQUASH_MSG" +then + cat "$GIT_DIR/SQUASH_MSG" +fi | git-stripspace >"$GIT_DIR"/COMMIT_EDITMSG + +case "$signoff" in +t) + need_blank_before_signoff= + tail -n 1 "$GIT_DIR"/COMMIT_EDITMSG | + grep 'Signed-off-by:' >/dev/null || need_blank_before_signoff=yes + { + test -z "$need_blank_before_signoff" || echo + git-var GIT_COMMITTER_IDENT | sed -e ' + s/>.*/>/ + s/^/Signed-off-by: / + ' + } >>"$GIT_DIR"/COMMIT_EDITMSG + ;; +esac + +if test -f "$GIT_DIR/MERGE_HEAD" && test -z "$no_edit"; then + echo "#" + echo "# It looks like you may be committing a MERGE." + echo "# If this is not correct, please remove the file" + echo "# $GIT_DIR/MERGE_HEAD" + echo "# and try again" + echo "#" +fi >>"$GIT_DIR"/COMMIT_EDITMSG + +# Author +if test '' != "$use_commit" +then + pick_author_script=' + /^author /{ + s/'\''/'\''\\'\'\''/g + h + s/^author \([^<]*\) <[^>]*> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_NAME='\''&'\''/p + + g + s/^author [^<]* <\([^>]*\)> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p + + g + s/^author [^<]* <[^>]*> \(.*\)$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_DATE='\''&'\''/p + + q + } + ' + encoding=$(git config i18n.commitencoding || echo UTF-8) + set_author_env=`git show -s --pretty=raw --encoding="$encoding" "$use_commit" | + LANG=C LC_ALL=C sed -ne "$pick_author_script"` + eval "$set_author_env" + export GIT_AUTHOR_NAME + export GIT_AUTHOR_EMAIL + export GIT_AUTHOR_DATE +fi +if test '' != "$force_author" +then + GIT_AUTHOR_NAME=`expr "z$force_author" : 'z\(.*[^ ]\) *<.*'` && + GIT_AUTHOR_EMAIL=`expr "z$force_author" : '.*\(<.*\)'` && + test '' != "$GIT_AUTHOR_NAME" && + test '' != "$GIT_AUTHOR_EMAIL" || + die "malformed --author parameter" + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL +fi + +PARENTS="-p HEAD" +if test -z "$initial_commit" +then + rloga='commit' + if [ -f "$GIT_DIR/MERGE_HEAD" ]; then + rloga='commit (merge)' + PARENTS="-p HEAD "`sed -e 's/^/-p /' "$GIT_DIR/MERGE_HEAD"` + elif test -n "$amend"; then + rloga='commit (amend)' + PARENTS=$(git-cat-file commit HEAD | + sed -n -e '/^$/q' -e 's/^parent /-p /p') + fi + current="$(git-rev-parse --verify HEAD)" +else + if [ -z "$(git-ls-files)" ]; then + echo >&2 'nothing to commit (use "git add file1 file2" to include for commit)' + exit 1 + fi + PARENTS="" + rloga='commit (initial)' + current='' +fi +set_reflog_action "$rloga" + +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 +else + # we need to check if there is anything to commit + run_status >/dev/null +fi +if [ "$?" != "0" -a ! -f "$GIT_DIR/MERGE_HEAD" -a -z "$amend" ] +then + rm -f "$GIT_DIR/COMMIT_EDITMSG" "$GIT_DIR/SQUASH_MSG" + run_status + exit 1 +fi + +case "$no_edit" in +'') + case "${VISUAL:-$EDITOR},$TERM" in + ,dumb) + echo >&2 "Terminal is dumb but no VISUAL nor EDITOR defined." + echo >&2 "Please supply the commit log message using either" + echo >&2 "-m or -F option. A boilerplate log message has" + echo >&2 "been prepared in $GIT_DIR/COMMIT_EDITMSG" + exit 1 + ;; + esac + git-var GIT_AUTHOR_IDENT > /dev/null || die + git-var GIT_COMMITTER_IDENT > /dev/null || die + ${VISUAL:-${EDITOR:-vi}} "$GIT_DIR/COMMIT_EDITMSG" + ;; +esac + +case "$verify" in +t) + if test -x "$GIT_DIR"/hooks/commit-msg + then + "$GIT_DIR"/hooks/commit-msg "$GIT_DIR"/COMMIT_EDITMSG || exit + fi +esac + +if test -z "$no_edit" +then + sed -e ' + /^diff --git a\/.*/{ + s/// + q + } + /^#/d + ' "$GIT_DIR"/COMMIT_EDITMSG +else + cat "$GIT_DIR"/COMMIT_EDITMSG +fi | +git-stripspace >"$GIT_DIR"/COMMIT_MSG + +if cnt=`grep -v -i '^Signed-off-by' "$GIT_DIR"/COMMIT_MSG | + git-stripspace | + wc -l` && + test 0 -lt $cnt +then + if test -z "$TMP_INDEX" + then + tree=$(GIT_INDEX_FILE="$USE_INDEX" git-write-tree) + else + tree=$(GIT_INDEX_FILE="$TMP_INDEX" git-write-tree) && + rm -f "$TMP_INDEX" + fi && + commit=$(cat "$GIT_DIR"/COMMIT_MSG | git-commit-tree $tree $PARENTS) && + rlogm=$(sed -e 1q "$GIT_DIR"/COMMIT_MSG) && + git-update-ref -m "$GIT_REFLOG_ACTION: $rlogm" HEAD $commit "$current" && + rm -f -- "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/MERGE_MSG" && + if test -f "$NEXT_INDEX" + then + mv "$NEXT_INDEX" "$THIS_INDEX" + else + : ;# happy + fi +else + echo >&2 "* no commit message? aborting commit." + false +fi +ret="$?" +rm -f "$GIT_DIR/COMMIT_MSG" "$GIT_DIR/COMMIT_EDITMSG" "$GIT_DIR/SQUASH_MSG" +if test -d "$GIT_DIR/rr-cache" +then + git-rerere +fi + +if test "$ret" = 0 +then + if test -x "$GIT_DIR"/hooks/post-commit + then + "$GIT_DIR"/hooks/post-commit + fi + if test -z "$quiet" + then + echo "Created${initial_commit:+ initial} commit $commit" + git-diff-tree --shortstat --summary --root --no-commit-id HEAD -- + fi +fi + +exit "$ret" diff --git a/git-compat-util.h b/git-compat-util.h new file mode 100644 index 0000000000..c1bcb001a5 --- /dev/null +++ b/git-compat-util.h @@ -0,0 +1,274 @@ +#ifndef GIT_COMPAT_UTIL_H +#define GIT_COMPAT_UTIL_H + +#ifndef FLEX_ARRAY +#if defined(__GNUC__) && (__GNUC__ < 3) +#define FLEX_ARRAY 0 +#else +#define FLEX_ARRAY /* empty */ +#endif +#endif + +#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) + +#if !defined(__APPLE__) && !defined(__FreeBSD__) +#define _XOPEN_SOURCE 600 /* glibc2 and AIX 5.3L need 500, OpenBSD needs 600 for S_ISLNK() */ +#define _XOPEN_SOURCE_EXTENDED 1 /* AIX 5.3L needs this */ +#endif +#define _ALL_SOURCE 1 +#define _GNU_SOURCE 1 +#define _BSD_SOURCE 1 + +#include <unistd.h> +#include <stdio.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <stddef.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <errno.h> +#include <limits.h> +#include <sys/param.h> +#include <sys/types.h> +#include <dirent.h> +#include <sys/time.h> +#include <time.h> +#include <signal.h> +#include <sys/wait.h> +#include <fnmatch.h> +#include <sys/poll.h> +#include <sys/socket.h> +#include <assert.h> +#include <regex.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> +#include <netdb.h> +#include <pwd.h> +#include <inttypes.h> +#undef _ALL_SOURCE /* AIX 5.3L defines a struct list with _ALL_SOURCE. */ +#include <grp.h> +#define _ALL_SOURCE 1 + +#ifndef NO_ICONV +#include <iconv.h> +#endif + +/* On most systems <limits.h> would have given us this, but + * not on some systems (e.g. GNU/Hurd). + */ +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +#ifdef __GNUC__ +#define NORETURN __attribute__((__noreturn__)) +#else +#define NORETURN +#ifndef __attribute__ +#define __attribute__(x) +#endif +#endif + +/* General helper functions */ +extern void usage(const char *err) NORETURN; +extern void die(const char *err, ...) NORETURN __attribute__((format (printf, 1, 2))); +extern int error(const char *err, ...) __attribute__((format (printf, 1, 2))); +extern void warn(const char *err, ...) __attribute__((format (printf, 1, 2))); + +extern void set_usage_routine(void (*routine)(const char *err) NORETURN); +extern void set_die_routine(void (*routine)(const char *err, va_list params) NORETURN); +extern void set_error_routine(void (*routine)(const char *err, va_list params)); +extern void set_warn_routine(void (*routine)(const char *warn, va_list params)); + +#ifdef NO_MMAP + +#ifndef PROT_READ +#define PROT_READ 1 +#define PROT_WRITE 2 +#define MAP_PRIVATE 1 +#define MAP_FAILED ((void*)-1) +#endif + +#define mmap git_mmap +#define munmap git_munmap +extern void *git_mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); +extern int git_munmap(void *start, size_t length); + +#define DEFAULT_PACKED_GIT_WINDOW_SIZE (1 * 1024 * 1024) + +#else /* NO_MMAP */ + +#include <sys/mman.h> +#define DEFAULT_PACKED_GIT_WINDOW_SIZE \ + (sizeof(void*) >= 8 \ + ? 1 * 1024 * 1024 * 1024 \ + : 32 * 1024 * 1024) + +#endif /* NO_MMAP */ + +#define DEFAULT_PACKED_GIT_LIMIT \ + ((1024L * 1024L) * (sizeof(void*) >= 8 ? 8192 : 256)) + +#ifdef NO_PREAD +#define pread git_pread +extern ssize_t git_pread(int fd, void *buf, size_t count, off_t offset); +#endif + +#ifdef NO_SETENV +#define setenv gitsetenv +extern int gitsetenv(const char *, const char *, int); +#endif + +#ifdef NO_UNSETENV +#define unsetenv gitunsetenv +extern void gitunsetenv(const char *); +#endif + +#ifdef NO_STRCASESTR +#define strcasestr gitstrcasestr +extern char *gitstrcasestr(const char *haystack, const char *needle); +#endif + +#ifdef NO_STRLCPY +#define strlcpy gitstrlcpy +extern size_t gitstrlcpy(char *, const char *, size_t); +#endif + +extern void release_pack_memory(size_t); + +static inline char* xstrdup(const char *str) +{ + char *ret = strdup(str); + if (!ret) { + release_pack_memory(strlen(str) + 1); + ret = strdup(str); + if (!ret) + die("Out of memory, strdup failed"); + } + return ret; +} + +static inline void *xmalloc(size_t size) +{ + void *ret = malloc(size); + if (!ret && !size) + ret = malloc(1); + if (!ret) { + release_pack_memory(size); + ret = malloc(size); + if (!ret && !size) + ret = malloc(1); + if (!ret) + die("Out of memory, malloc failed"); + } +#ifdef XMALLOC_POISON + memset(ret, 0xA5, size); +#endif + return ret; +} + +static inline void *xrealloc(void *ptr, size_t size) +{ + void *ret = realloc(ptr, size); + if (!ret && !size) + ret = realloc(ptr, 1); + if (!ret) { + release_pack_memory(size); + ret = realloc(ptr, size); + if (!ret && !size) + ret = realloc(ptr, 1); + if (!ret) + die("Out of memory, realloc failed"); + } + return ret; +} + +static inline void *xcalloc(size_t nmemb, size_t size) +{ + void *ret = calloc(nmemb, size); + if (!ret && (!nmemb || !size)) + ret = calloc(1, 1); + if (!ret) { + release_pack_memory(nmemb * size); + ret = calloc(nmemb, size); + if (!ret && (!nmemb || !size)) + ret = calloc(1, 1); + if (!ret) + die("Out of memory, calloc failed"); + } + return ret; +} + +static inline void *xmmap(void *start, size_t length, + int prot, int flags, int fd, off_t offset) +{ + void *ret = mmap(start, length, prot, flags, fd, offset); + if (ret == MAP_FAILED) { + if (!length) + return NULL; + release_pack_memory(length); + ret = mmap(start, length, prot, flags, fd, offset); + if (ret == MAP_FAILED) + die("Out of memory? mmap failed: %s", strerror(errno)); + } + return ret; +} + +static inline ssize_t xread(int fd, void *buf, size_t len) +{ + ssize_t nr; + while (1) { + nr = read(fd, buf, len); + if ((nr < 0) && (errno == EAGAIN || errno == EINTR)) + continue; + return nr; + } +} + +static inline ssize_t xwrite(int fd, const void *buf, size_t len) +{ + ssize_t nr; + while (1) { + nr = write(fd, buf, len); + if ((nr < 0) && (errno == EAGAIN || errno == EINTR)) + continue; + return nr; + } +} + +static inline int has_extension(const char *filename, const char *ext) +{ + size_t len = strlen(filename); + size_t extlen = strlen(ext); + return len > extlen && !memcmp(filename + len - extlen, ext, extlen); +} + +/* Sane ctype - no locale, and works with signed chars */ +#undef isspace +#undef isdigit +#undef isalpha +#undef isalnum +#undef tolower +#undef toupper +extern unsigned char sane_ctype[256]; +#define GIT_SPACE 0x01 +#define GIT_DIGIT 0x02 +#define GIT_ALPHA 0x04 +#define sane_istest(x,mask) ((sane_ctype[(unsigned char)(x)] & (mask)) != 0) +#define isspace(x) sane_istest(x,GIT_SPACE) +#define isdigit(x) sane_istest(x,GIT_DIGIT) +#define isalpha(x) sane_istest(x,GIT_ALPHA) +#define isalnum(x) sane_istest(x,GIT_ALPHA | GIT_DIGIT) +#define tolower(x) sane_case((unsigned char)(x), 0x20) +#define toupper(x) sane_case((unsigned char)(x), 0) + +static inline int sane_case(int x, int high) +{ + if (sane_istest(x, GIT_ALPHA)) + x = (x & ~0x20) | high; + return x; +} + +#endif diff --git a/git-cvsexportcommit.perl b/git-cvsexportcommit.perl new file mode 100755 index 0000000000..870554eade --- /dev/null +++ b/git-cvsexportcommit.perl @@ -0,0 +1,284 @@ +#!/usr/bin/perl -w + +# Known limitations: +# - does not propagate permissions +# - error handling has not been extensively tested +# + +use strict; +use Getopt::Std; +use File::Temp qw(tempdir); +use Data::Dumper; +use File::Basename qw(basename dirname); + +unless ($ENV{GIT_DIR} && -r $ENV{GIT_DIR}){ + die "GIT_DIR is not defined or is unreadable"; +} + +our ($opt_h, $opt_P, $opt_p, $opt_v, $opt_c, $opt_f, $opt_a, $opt_m ); + +getopts('hPpvcfam:'); + +$opt_h && usage(); + +die "Need at least one commit identifier!" unless @ARGV; + +# setup a tempdir +our ($tmpdir, $tmpdirname) = tempdir('git-cvsapplycommit-XXXXXX', + TMPDIR => 1, + CLEANUP => 1); + +# resolve target commit +my $commit; +$commit = pop @ARGV; +$commit = safe_pipe_capture('git-rev-parse', '--verify', "$commit^0"); +chomp $commit; +if ($?) { + die "The commit reference $commit did not resolve!"; +} + +# resolve what parent we want +my $parent; +if (@ARGV) { + $parent = pop @ARGV; + $parent = safe_pipe_capture('git-rev-parse', '--verify', "$parent^0"); + chomp $parent; + if ($?) { + die "The parent reference did not resolve!"; + } +} + +# find parents from the commit itself +my @commit = safe_pipe_capture('git-cat-file', 'commit', $commit); +my @parents; +my $committer; +my $author; +my $stage = 'headers'; # headers, msg +my $title; +my $msg = ''; + +foreach my $line (@commit) { + chomp $line; + if ($stage eq 'headers' && $line eq '') { + $stage = 'msg'; + next; + } + + if ($stage eq 'headers') { + if ($line =~ m/^parent (\w{40})$/) { # found a parent + push @parents, $1; + } elsif ($line =~ m/^author (.+) \d+ [-+]\d+$/) { + $author = $1; + } elsif ($line =~ m/^committer (.+) \d+ [-+]\d+$/) { + $committer = $1; + } + } else { + $msg .= $line . "\n"; + unless ($title) { + $title = $line; + } + } +} + +if ($parent) { + my $found; + # double check that it's a valid parent + foreach my $p (@parents) { + if ($p eq $parent) { + $found = 1; + last; + }; # found it + } + die "Did not find $parent in the parents for this commit!" if !$found and !$opt_P; +} else { # we don't have a parent from the cmdline... + if (@parents == 1) { # it's safe to get it from the commit + $parent = $parents[0]; + } else { # or perhaps not! + die "This commit has more than one parent -- please name the parent you want to use explicitly"; + } +} + +$opt_v && print "Applying to CVS commit $commit from parent $parent\n"; + +# grab the commit message +open(MSG, ">.msg") or die "Cannot open .msg for writing"; +if ($opt_m) { + print MSG $opt_m; +} +print MSG $msg; +if ($opt_a) { + print MSG "\n\nAuthor: $author\n"; + if ($author ne $committer) { + print MSG "Committer: $committer\n"; + } +} +close MSG; + +`git-diff-tree --binary -p $parent $commit >.cvsexportcommit.diff`;# || die "Cannot diff"; + +## apply non-binary changes +my $fuzz = $opt_p ? 0 : 2; + +print "Checking if patch will apply\n"; + +my @stat; +open APPLY, "GIT_DIR= git-apply -C$fuzz --binary --summary --numstat<.cvsexportcommit.diff|" || die "cannot patch"; +@stat=<APPLY>; +close APPLY || die "Cannot patch"; +my (@bfiles,@files,@afiles,@dfiles); +chomp @stat; +foreach (@stat) { + push (@bfiles,$1) if m/^-\t-\t(.*)$/; + push (@files, $1) if m/^-\t-\t(.*)$/; + push (@files, $1) if m/^\d+\t\d+\t(.*)$/; + push (@afiles,$1) if m/^ create mode [0-7]+ (.*)$/; + push (@dfiles,$1) if m/^ delete mode [0-7]+ (.*)$/; +} +map { s/^"(.*)"$/$1/g } @bfiles,@files; +map { s/\\([0-7]{3})/sprintf('%c',oct $1)/eg } @bfiles,@files; + +# check that the files are clean and up to date according to cvs +my $dirty; +my @dirs; +foreach my $p (@afiles) { + my $path = dirname $p; + while (!-d $path and ! grep { $_ eq $path } @dirs) { + unshift @dirs, $path; + $path = dirname $path; + } +} + +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 + if ($f =~ m,(.*)/[^/]*$,) { + my $p = $1; + next if (grep { $_ eq $p } @dirs); + } + my @status = grep(m/^File/, safe_pipe_capture('cvs', '-q', 'status' ,$f)); + if (@status > 1) { warn 'Strange! cvs status returned more than one line?'}; + 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 (@files) { + next if grep { $_ eq $f } @afiles; + # TODO:we need to handle removed in cvs + 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: Up-to-date$/) { + $dirty = 1; + warn "File $f not up to date in your CVS checkout!\n"; + } +} +if ($dirty) { + if ($opt_f) { warn "The tree is not clean -- forced merge\n"; + $dirty = 0; + } else { + die "Exiting: your CVS tree is not clean for this merge."; + } +} + +print "Applying\n"; +`GIT_DIR= git-apply -C$fuzz --binary --summary --numstat --apply <.cvsexportcommit.diff` || die "cannot patch"; + +print "Patch applied successfully. Adding new files and directories to CVS\n"; +my $dirtypatch = 0; +foreach my $d (@dirs) { + if (system('cvs','add',$d)) { + $dirtypatch = 1; + warn "Failed to cvs add directory $d -- you may need to do it manually"; + } +} + +foreach my $f (@afiles) { + if (grep { $_ eq $f } @bfiles) { + system('cvs', 'add','-kb',$f); + } else { + system('cvs', 'add', $f); + } + if ($?) { + $dirtypatch = 1; + warn "Failed to cvs add $f -- you may need to do it manually"; + } +} + +foreach my $f (@dfiles) { + system('cvs', 'rm', '-f', $f); + if ($?) { + $dirtypatch = 1; + warn "Failed to cvs rm -f $f -- you may need to do it manually"; + } +} + +print "Commit to CVS\n"; +print "Patch title (first comment line): $title\n"; +my @commitfiles = map { unless (m/\s/) { '\''.$_.'\''; } else { $_; }; } (@files); +my $cmd = "cvs commit -F .msg @commitfiles"; + +if ($dirtypatch) { + print "NOTE: One or more hunks failed to apply cleanly.\n"; + print "You'll need to apply the patch in .cvsexportcommit.diff manually\n"; + print "using a patch program. After applying the patch and resolving the\n"; + print "problems you may commit using:"; + print "\n $cmd\n\n"; + exit(1); +} + +if ($opt_c) { + print "Autocommit\n $cmd\n"; + print safe_pipe_capture('cvs', 'commit', '-F', '.msg', @files); + if ($?) { + die "Exiting: The commit did not succeed"; + } + print "Committed successfully to CVS\n"; +} else { + print "Ready for you to commit, just run:\n\n $cmd\n"; +} + +# clean up +unlink(".cvsexportcommit.diff"); +unlink(".msg"); + +sub usage { + print STDERR <<END; +Usage: GIT_DIR=/path/to/.git ${\basename $0} [-h] [-p] [-v] [-c] [-f] [-m msgprefix] [ parent ] commit +END + exit(1); +} + +# An alternative to `command` that allows input to be passed as an array +# to work around shell problems with weird characters in arguments +# if the exec returns non-zero we die +sub safe_pipe_capture { + my @output; + if (my $pid = open my $child, '-|') { + @output = (<$child>); + close $child or die join(' ',@_).": $! $?"; + } else { + exec(@_) or die "$! $?"; # exec() can fail the executable can't be found + } + return wantarray ? @output : join('',@output); +} + +sub safe_pipe_capture_blob { + my $output; + if (my $pid = open my $child, '-|') { + local $/; + undef $/; + $output = (<$child>); + close $child or die join(' ',@_).": $! $?"; + } else { + exec(@_) or die "$! $?"; # exec() can fail the executable can't be found + } + return $output; +} diff --git a/git-cvsimport.perl b/git-cvsimport.perl new file mode 100755 index 0000000000..1a1ba7b1a6 --- /dev/null +++ b/git-cvsimport.perl @@ -0,0 +1,1023 @@ +#!/usr/bin/perl -w + +# This tool is copyright (c) 2005, Matthias Urlichs. +# It is released under the Gnu Public License, version 2. +# +# The basic idea is to aggregate CVS check-ins into related changes. +# Fortunately, "cvsps" does that for us; all we have to do is to parse +# its output. +# +# Checking out the files is done by a single long-running CVS connection +# / server process. +# +# The head revision is on branch "origin" by default. +# You can change that with the '-o' option. + +use strict; +use warnings; +use Getopt::Std; +use File::Spec; +use File::Temp qw(tempfile tmpnam); +use File::Path qw(mkpath); +use File::Basename qw(basename dirname); +use Time::Local; +use IO::Socket; +use IO::Pipe; +use POSIX qw(strftime dup2 ENOENT); +use IPC::Open2; + +$SIG{'PIPE'}="IGNORE"; +$ENV{'TZ'}="UTC"; + +our ($opt_h,$opt_o,$opt_v,$opt_k,$opt_u,$opt_d,$opt_p,$opt_C,$opt_z,$opt_i,$opt_P, $opt_s,$opt_m,$opt_M,$opt_A,$opt_S,$opt_L, $opt_a); +my (%conv_author_name, %conv_author_email); + +sub usage() { + print STDERR <<END; +Usage: ${\basename $0} # fetch/update GIT from CVS + [-o branch-for-HEAD] [-h] [-v] [-d CVSROOT] [-A author-conv-file] + [-p opts-for-cvsps] [-C GIT_repository] [-z fuzz] [-i] [-k] [-u] + [-s subst] [-a] [-m] [-M regex] [-S regex] [CVS_module] +END + exit(1); +} + +sub read_author_info($) { + my ($file) = @_; + my $user; + open my $f, '<', "$file" or die("Failed to open $file: $!\n"); + + while (<$f>) { + # Expected format is this: + # exon=Andreas Ericsson <ae@op5.se> + if (m/^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/) { + $user = $1; + $conv_author_name{$user} = $2; + $conv_author_email{$user} = $3; + } + # However, we also read from CVSROOT/users format + # to ease migration. + elsif (/^(\w+):(['"]?)(.+?)\2\s*$/) { + my $mapped; + ($user, $mapped) = ($1, $3); + if ($mapped =~ /^\s*(.*?)\s*<(.*)>\s*$/) { + $conv_author_name{$user} = $1; + $conv_author_email{$user} = $2; + } + elsif ($mapped =~ /^<?(.*)>?$/) { + $conv_author_name{$user} = $user; + $conv_author_email{$user} = $1; + } + } + # NEEDSWORK: Maybe warn on unrecognized lines? + } + close ($f); +} + +sub write_author_info($) { + my ($file) = @_; + open my $f, '>', $file or + die("Failed to open $file for writing: $!"); + + foreach (keys %conv_author_name) { + print $f "$_=$conv_author_name{$_} <$conv_author_email{$_}>\n"; + } + close ($f); +} + +# convert getopts specs for use by git-repo-config +sub read_repo_config { + # Split the string between characters, unless there is a ':' + # So "abc:de" becomes ["a", "b", "c:", "d", "e"] + my @opts = split(/ *(?!:)/, shift); + foreach my $o (@opts) { + my $key = $o; + $key =~ s/://g; + my $arg = 'git-repo-config'; + $arg .= ' --bool' if ($o !~ /:$/); + + chomp(my $tmp = `$arg --get cvsimport.$key`); + if ($tmp && !($arg =~ /--bool/ && $tmp eq 'false')) { + no strict 'refs'; + my $opt_name = "opt_" . $key; + if (!$$opt_name) { + $$opt_name = $tmp; + } + } + } + if (@ARGV == 0) { + chomp(my $module = `git-repo-config --get cvsimport.module`); + push(@ARGV, $module); + } +} + +my $opts = "haivmkuo:d:p:C:z:s:M:P:A:S:L:"; +read_repo_config($opts); +getopts($opts) or usage(); +usage if $opt_h; + +@ARGV <= 1 or usage(); + +if ($opt_d) { + $ENV{"CVSROOT"} = $opt_d; +} elsif (-f 'CVS/Root') { + open my $f, '<', 'CVS/Root' or die 'Failed to open CVS/Root'; + $opt_d = <$f>; + chomp $opt_d; + close $f; + $ENV{"CVSROOT"} = $opt_d; +} elsif ($ENV{"CVSROOT"}) { + $opt_d = $ENV{"CVSROOT"}; +} else { + die "CVSROOT needs to be set"; +} +$opt_o ||= "origin"; +$opt_s ||= "-"; +$opt_a ||= 0; + +my $git_tree = $opt_C; +$git_tree ||= "."; + +my $cvs_tree; +if ($#ARGV == 0) { + $cvs_tree = $ARGV[0]; +} elsif (-f 'CVS/Repository') { + open my $f, '<', 'CVS/Repository' or + die 'Failed to open CVS/Repository'; + $cvs_tree = <$f>; + chomp $cvs_tree; + close $f; +} else { + usage(); +} + +our @mergerx = (); +if ($opt_m) { + @mergerx = ( qr/\W(?:from|of|merge|merging|merged) (\w+)/i ); +} +if ($opt_M) { + push (@mergerx, qr/$opt_M/); +} + +# Remember UTC of our starting time +# we'll want to avoid importing commits +# that are too recent +our $starttime = time(); + +select(STDERR); $|=1; select(STDOUT); + + +package CVSconn; +# Basic CVS dialog. +# We're only interested in connecting and downloading, so ... + +use File::Spec; +use File::Temp qw(tempfile); +use POSIX qw(strftime dup2); + +sub new { + my ($what,$repo,$subdir) = @_; + $what=ref($what) if ref($what); + + my $self = {}; + $self->{'buffer'} = ""; + bless($self,$what); + + $repo =~ s#/+$##; + $self->{'fullrep'} = $repo; + $self->conn(); + + $self->{'subdir'} = $subdir; + $self->{'lines'} = undef; + + return $self; +} + +sub conn { + my $self = shift; + my $repo = $self->{'fullrep'}; + if ($repo =~ s/^:pserver(?:([^:]*)):(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?//) { + my ($param,$user,$pass,$serv,$port) = ($1,$2,$3,$4,$5); + + my ($proxyhost,$proxyport); + if ($param && ($param =~ m/proxy=([^;]+)/)) { + $proxyhost = $1; + # Default proxyport, if not specified, is 8080. + $proxyport = 8080; + if ($ENV{"CVS_PROXY_PORT"}) { + $proxyport = $ENV{"CVS_PROXY_PORT"}; + } + if ($param =~ m/proxyport=([^;]+)/) { + $proxyport = $1; + } + } + + $user="anonymous" unless defined $user; + my $rr2 = "-"; + unless ($port) { + $rr2 = ":pserver:$user\@$serv:$repo"; + $port=2401; + } + my $rr = ":pserver:$user\@$serv:$port$repo"; + + unless ($pass) { + open(H,$ENV{'HOME'}."/.cvspass") and do { + # :pserver:cvs@mea.tmt.tele.fi:/cvsroot/zmailer Ah<Z + while (<H>) { + chomp; + s/^\/\d+\s+//; + my ($w,$p) = split(/\s/,$_,2); + if ($w eq $rr or $w eq $rr2) { + $pass = $p; + last; + } + } + }; + } + $pass="A" unless $pass; + + my ($s, $rep); + if ($proxyhost) { + + # Use a HTTP Proxy. Only works for HTTP proxies that + # don't require user authentication + # + # See: http://www.ietf.org/rfc/rfc2817.txt + + $s = IO::Socket::INET->new(PeerHost => $proxyhost, PeerPort => $proxyport); + die "Socket to $proxyhost: $!\n" unless defined $s; + $s->write("CONNECT $serv:$port HTTP/1.1\r\nHost: $serv:$port\r\n\r\n") + or die "Write to $proxyhost: $!\n"; + $s->flush(); + + $rep = <$s>; + + # The answer should look like 'HTTP/1.x 2yy ....' + if (!($rep =~ m#^HTTP/1\.. 2[0-9][0-9]#)) { + die "Proxy connect: $rep\n"; + } + # Skip up to the empty line of the proxy server output + # including the response headers. + while ($rep = <$s>) { + last if (!defined $rep || + $rep eq "\n" || + $rep eq "\r\n"); + } + } else { + $s = IO::Socket::INET->new(PeerHost => $serv, PeerPort => $port); + die "Socket to $serv: $!\n" unless defined $s; + } + + $s->write("BEGIN AUTH REQUEST\n$repo\n$user\n$pass\nEND AUTH REQUEST\n") + or die "Write to $serv: $!\n"; + $s->flush(); + + $rep = <$s>; + + if ($rep ne "I LOVE YOU\n") { + $rep="<unknown>" unless $rep; + die "AuthReply: $rep\n"; + } + $self->{'socketo'} = $s; + $self->{'socketi'} = $s; + } else { # local or ext: Fork off our own cvs server. + my $pr = IO::Pipe->new(); + my $pw = IO::Pipe->new(); + my $pid = fork(); + die "Fork: $!\n" unless defined $pid; + my $cvs = 'cvs'; + $cvs = $ENV{CVS_SERVER} if exists $ENV{CVS_SERVER}; + my $rsh = 'rsh'; + $rsh = $ENV{CVS_RSH} if exists $ENV{CVS_RSH}; + + my @cvs = ($cvs, 'server'); + my ($local, $user, $host); + $local = $repo =~ s/:local://; + if (!$local) { + $repo =~ s/:ext://; + $local = !($repo =~ s/^(?:([^\@:]+)\@)?([^:]+)://); + ($user, $host) = ($1, $2); + } + if (!$local) { + if ($user) { + unshift @cvs, $rsh, '-l', $user, $host; + } else { + unshift @cvs, $rsh, $host; + } + } + + unless ($pid) { + $pr->writer(); + $pw->reader(); + dup2($pw->fileno(),0); + dup2($pr->fileno(),1); + $pr->close(); + $pw->close(); + exec(@cvs); + } + $pw->writer(); + $pr->reader(); + $self->{'socketo'} = $pw; + $self->{'socketi'} = $pr; + } + $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 Checked-in Created Updated Merged Removed\n"); + + $self->{'socketo'}->write("valid-requests\n"); + $self->{'socketo'}->flush(); + + chomp(my $rep=$self->readline()); + if ($rep !~ s/^Valid-requests\s*//) { + $rep="<unknown>" unless $rep; + die "Expected Valid-requests from server, but got: $rep\n"; + } + chomp(my $res=$self->readline()); + die "validReply: $res\n" if $res ne "ok"; + + $self->{'socketo'}->write("UseUnchanged\n") if $rep =~ /\bUseUnchanged\b/; + $self->{'repo'} = $repo; +} + +sub readline { + my ($self) = @_; + return $self->{'socketi'}->getline(); +} + +sub _file { + # Request a file with a given revision. + # Trial and error says this is a good way to do it. :-/ + my ($self,$fn,$rev) = @_; + $self->{'socketo'}->write("Argument -N\n") or return undef; + $self->{'socketo'}->write("Argument -P\n") or return undef; + # -kk: Linus' version doesn't use it - defaults to off + if ($opt_k) { + $self->{'socketo'}->write("Argument -kk\n") or return undef; + } + $self->{'socketo'}->write("Argument -r\n") or return undef; + $self->{'socketo'}->write("Argument $rev\n") or return undef; + $self->{'socketo'}->write("Argument --\n") or return undef; + $self->{'socketo'}->write("Argument $self->{'subdir'}/$fn\n") or return undef; + $self->{'socketo'}->write("Directory .\n") or return undef; + $self->{'socketo'}->write("$self->{'repo'}\n") or return undef; + # $self->{'socketo'}->write("Sticky T1.0\n") or return undef; + $self->{'socketo'}->write("co\n") or return undef; + $self->{'socketo'}->flush() or return undef; + $self->{'lines'} = 0; + return 1; +} +sub _line { + # Read a line from the server. + # ... except that 'line' may be an entire file. ;-) + my ($self, $fh) = @_; + die "Not in lines" unless defined $self->{'lines'}; + + my $line; + my $res=0; + while (defined($line = $self->readline())) { + # M U gnupg-cvs-rep/AUTHORS + # Updated gnupg-cvs-rep/ + # /daten/src/rsync/gnupg-cvs-rep/AUTHORS + # /AUTHORS/1.1///T1.1 + # u=rw,g=rw,o=rw + # 0 + # ok + + if ($line =~ s/^(?:Created|Updated) //) { + $line = $self->readline(); # path + $line = $self->readline(); # Entries line + my $mode = $self->readline(); chomp $mode; + $self->{'mode'} = $mode; + defined (my $cnt = $self->readline()) + or die "EOF from server after 'Changed'\n"; + chomp $cnt; + die "Duh: Filesize $cnt" if $cnt !~ /^\d+$/; + $line=""; + $res = $self->_fetchfile($fh, $cnt); + } elsif ($line =~ s/^ //) { + print $fh $line; + $res += length($line); + } elsif ($line =~ /^M\b/) { + # output, do nothing + } elsif ($line =~ /^Mbinary\b/) { + my $cnt; + die "EOF from server after 'Mbinary'" unless defined ($cnt = $self->readline()); + chomp $cnt; + die "Duh: Mbinary $cnt" if $cnt !~ /^\d+$/ or $cnt<1; + $line=""; + $res += $self->_fetchfile($fh, $cnt); + } else { + chomp $line; + if ($line eq "ok") { + # print STDERR "S: ok (".length($res).")\n"; + return $res; + } elsif ($line =~ s/^E //) { + # print STDERR "S: $line\n"; + } elsif ($line =~ /^(Remove-entry|Removed) /i) { + $line = $self->readline(); # filename + $line = $self->readline(); # OK + chomp $line; + die "Unknown: $line" if $line ne "ok"; + return -1; + } else { + die "Unknown: $line\n"; + } + } + } + return undef; +} +sub file { + my ($self,$fn,$rev) = @_; + my $res; + + my ($fh, $name) = tempfile('gitcvs.XXXXXX', + DIR => File::Spec->tmpdir(), UNLINK => 1); + + $self->_file($fn,$rev) and $res = $self->_line($fh); + + if (!defined $res) { + print STDERR "Server has gone away while fetching $fn $rev, retrying...\n"; + truncate $fh, 0; + $self->conn(); + $self->_file($fn,$rev) or die "No file command send"; + $res = $self->_line($fh); + die "Retry failed" unless defined $res; + } + close ($fh); + + return ($name, $res); +} +sub _fetchfile { + my ($self, $fh, $cnt) = @_; + my $res = 0; + my $bufsize = 1024 * 1024; + while ($cnt) { + if ($bufsize > $cnt) { + $bufsize = $cnt; + } + my $buf; + my $num = $self->{'socketi'}->read($buf,$bufsize); + die "Server: Filesize $cnt: $num: $!\n" if not defined $num or $num<=0; + print $fh $buf; + $res += $num; + $cnt -= $num; + } + return $res; +} + + +package main; + +my $cvs = CVSconn->new($opt_d, $cvs_tree); + + +sub pdate($) { + my ($d) = @_; + m#(\d{2,4})/(\d\d)/(\d\d)\s(\d\d):(\d\d)(?::(\d\d))?# + or die "Unparseable date: $d\n"; + my $y=$1; $y-=1900 if $y>1900; + return timegm($6||0,$5,$4,$3,$2-1,$y); +} + +sub pmode($) { + my ($mode) = @_; + my $m = 0; + my $mm = 0; + my $um = 0; + for my $x(split(//,$mode)) { + if ($x eq ",") { + $m |= $mm&$um; + $mm = 0; + $um = 0; + } elsif ($x eq "u") { $um |= 0700; + } elsif ($x eq "g") { $um |= 0070; + } elsif ($x eq "o") { $um |= 0007; + } elsif ($x eq "r") { $mm |= 0444; + } elsif ($x eq "w") { $mm |= 0222; + } elsif ($x eq "x") { $mm |= 0111; + } elsif ($x eq "=") { # do nothing + } else { die "Unknown mode: $mode\n"; + } + } + $m |= $mm&$um; + return $m; +} + +sub getwd() { + my $pwd = `pwd`; + chomp $pwd; + return $pwd; +} + +sub is_sha1 { + my $s = shift; + return $s =~ /^[a-f0-9]{40}$/; +} + +sub get_headref ($$) { + my $name = shift; + my $git_dir = shift; + + my $f = "$git_dir/refs/heads/$name"; + if (open(my $fh, $f)) { + chomp(my $r = <$fh>); + is_sha1($r) or die "Cannot get head id for $name ($r): $!"; + return $r; + } + die "unable to open $f: $!" unless $! == POSIX::ENOENT; + return undef; +} + +-d $git_tree + or mkdir($git_tree,0777) + or die "Could not create $git_tree: $!"; +chdir($git_tree); + +my $last_branch = ""; +my $orig_branch = ""; +my %branch_date; +my $tip_at_start = undef; + +my $git_dir = $ENV{"GIT_DIR"} || ".git"; +$git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#; +$ENV{"GIT_DIR"} = $git_dir; +my $orig_git_index; +$orig_git_index = $ENV{GIT_INDEX_FILE} if exists $ENV{GIT_INDEX_FILE}; + +my %index; # holds filenames of one index per branch + +unless (-d $git_dir) { + system("git-init"); + die "Cannot init the GIT db at $git_tree: $?\n" if $?; + system("git-read-tree"); + die "Cannot init an empty tree: $?\n" if $?; + + $last_branch = $opt_o; + $orig_branch = ""; +} else { + -f "$git_dir/refs/heads/$opt_o" + or die "Branch '$opt_o' does not exist.\n". + "Either use the correct '-o branch' option,\n". + "or import to a new repository.\n"; + + open(F, "git-symbolic-ref HEAD |") or + die "Cannot run git-symbolic-ref: $!\n"; + chomp ($last_branch = <F>); + $last_branch = basename($last_branch); + close(F); + unless ($last_branch) { + warn "Cannot read the last branch name: $! -- assuming 'master'\n"; + $last_branch = "master"; + } + $orig_branch = $last_branch; + $tip_at_start = `git-rev-parse --verify HEAD`; + + # Get the last import timestamps + my $fmt = '($ref, $author) = (%(refname), %(author));'; + open(H, "git-for-each-ref --perl --format='$fmt' refs/heads |") or + die "Cannot run git-for-each-ref: $!\n"; + while (defined(my $entry = <H>)) { + my ($ref, $author); + eval($entry) || die "cannot eval refs list: $@"; + my ($head) = ($ref =~ m|^refs/heads/(.*)|); + $author =~ /^.*\s(\d+)\s[-+]\d{4}$/; + $branch_date{$head} = $1; + } + close(H); +} + +-d $git_dir + or die "Could not create git subdir ($git_dir).\n"; + +# now we read (and possibly save) author-info as well +-f "$git_dir/cvs-authors" and + read_author_info("$git_dir/cvs-authors"); +if ($opt_A) { + read_author_info($opt_A); + write_author_info("$git_dir/cvs-authors"); +} + + +# +# run cvsps into a file unless we are getting +# it passed as a file via $opt_P +# +my $cvspsfile; +unless ($opt_P) { + print "Running cvsps...\n" if $opt_v; + my $pid = open(CVSPS,"-|"); + my $cvspsfh; + 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"; + } + ($cvspsfh, $cvspsfile) = tempfile('gitXXXXXX', SUFFIX => '.cvsps', + DIR => File::Spec->tmpdir()); + while (<CVSPS>) { + print $cvspsfh $_; + } + close CVSPS; + close $cvspsfh; +} else { + $cvspsfile = $opt_P; +} + +open(CVS, "<$cvspsfile") or die $!; + +## cvsps output: +#--------------------- +#PatchSet 314 +#Date: 1999/09/18 13:03:59 +#Author: wkoch +#Branch: STABLE-BRANCH-1-0 +#Ancestor branch: HEAD +#Tag: (none) +#Log: +# See ChangeLog: Sat Sep 18 13:03:28 CEST 1999 Werner Koch +#Members: +# README:1.57->1.57.2.1 +# VERSION:1.96->1.96.2.1 +# +#--------------------- + +my $state = 0; + +sub update_index (\@\@) { + my $old = shift; + my $new = shift; + open(my $fh, '|-', qw(git-update-index -z --index-info)) + or die "unable to open git-update-index: $!"; + print $fh + (map { "0 0000000000000000000000000000000000000000\t$_\0" } + @$old), + (map { '100' . sprintf('%o', $_->[0]) . " $_->[1]\t$_->[2]\0" } + @$new) + or die "unable to write to git-update-index: $!"; + close $fh + or die "unable to write to git-update-index: $!"; + $? and die "git-update-index reported error: $?"; +} + +sub write_tree () { + open(my $fh, '-|', qw(git-write-tree)) + or die "unable to open git-write-tree: $!"; + chomp(my $tree = <$fh>); + is_sha1($tree) + or die "Cannot get tree id ($tree): $!"; + close($fh) + or die "Error running git-write-tree: $?\n"; + print "Tree ID $tree\n" if $opt_v; + return $tree; +} + +my ($patchset,$date,$author_name,$author_email,$branch,$ancestor,$tag,$logmsg); +my (@old,@new,@skipped,%ignorebranch); + +# commits that cvsps cannot place anywhere... +$ignorebranch{'#CVSPS_NO_BRANCH'} = 1; + +sub commit { + if ($branch eq $opt_o && !$index{branch} && !get_headref($branch, $git_dir)) { + # looks like an initial commit + # use the index primed by git-init + $ENV{GIT_INDEX_FILE} = '.git/index'; + $index{$branch} = '.git/index'; + } else { + # use an index per branch to speed up + # imports of projects with many branches + unless ($index{$branch}) { + $index{$branch} = tmpnam(); + $ENV{GIT_INDEX_FILE} = $index{$branch}; + if ($ancestor) { + system("git-read-tree", $ancestor); + } else { + system("git-read-tree", $branch); + } + die "read-tree failed: $?\n" if $?; + } + } + $ENV{GIT_INDEX_FILE} = $index{$branch}; + + update_index(@old, @new); + @old = @new = (); + my $tree = write_tree(); + my $parent = get_headref($last_branch, $git_dir); + print "Parent ID " . ($parent ? $parent : "(empty)") . "\n" if $opt_v; + + my @commit_args; + push @commit_args, ("-p", $parent) if $parent; + + # loose detection of merges + # based on the commit msg + foreach my $rx (@mergerx) { + next unless $logmsg =~ $rx && $1; + my $mparent = $1 eq 'HEAD' ? $opt_o : $1; + if (my $sha1 = get_headref($mparent, $git_dir)) { + push @commit_args, '-p', $mparent; + print "Merge parent branch: $mparent\n" if $opt_v; + } + } + + my $commit_date = strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)); + $ENV{GIT_AUTHOR_NAME} = $author_name; + $ENV{GIT_AUTHOR_EMAIL} = $author_email; + $ENV{GIT_AUTHOR_DATE} = $commit_date; + $ENV{GIT_COMMITTER_NAME} = $author_name; + $ENV{GIT_COMMITTER_EMAIL} = $author_email; + $ENV{GIT_COMMITTER_DATE} = $commit_date; + my $pid = open2(my $commit_read, my $commit_write, + 'git-commit-tree', $tree, @commit_args); + + # compatibility with git2cvs + substr($logmsg,32767) = "" if length($logmsg) > 32767; + $logmsg =~ s/[\s\n]+\z//; + + if (@skipped) { + $logmsg .= "\n\n\nSKIPPED:\n\t"; + $logmsg .= join("\n\t", @skipped) . "\n"; + @skipped = (); + } + + print($commit_write "$logmsg\n") && close($commit_write) + or die "Error writing to git-commit-tree: $!\n"; + + print "Committed patch $patchset ($branch $commit_date)\n" if $opt_v; + chomp(my $cid = <$commit_read>); + is_sha1($cid) or die "Cannot get commit id ($cid): $!\n"; + print "Commit ID $cid\n" if $opt_v; + close($commit_read); + + waitpid($pid,0); + die "Error running git-commit-tree: $?\n" if $?; + + system("git-update-ref refs/heads/$branch $cid") == 0 + or die "Cannot write branch $branch for update: $!\n"; + + if ($tag) { + my ($in, $out) = ('',''); + my ($xtag) = $tag; + $xtag =~ s/\s+\*\*.*$//; # Remove stuff like ** INVALID ** and ** FUNKY ** + $xtag =~ tr/_/\./ if ( $opt_u ); + $xtag =~ s/[\/]/$opt_s/g; + + my $pid = open2($in, $out, 'git-mktag'); + print $out "object $cid\n". + "type commit\n". + "tag $xtag\n". + "tagger $author_name <$author_email>\n" + or die "Cannot create tag object $xtag: $!\n"; + close($out) + or die "Cannot create tag object $xtag: $!\n"; + + my $tagobj = <$in>; + chomp $tagobj; + + if ( !close($in) or waitpid($pid, 0) != $pid or + $? != 0 or $tagobj !~ /^[0123456789abcdef]{40}$/ ) { + die "Cannot create tag object $xtag: $!\n"; + } + + + open(C,">$git_dir/refs/tags/$xtag") + or die "Cannot create tag $xtag: $!\n"; + print C "$tagobj\n" + or die "Cannot write tag $xtag: $!\n"; + close(C) + or die "Cannot write tag $xtag: $!\n"; + + print "Created tag '$xtag' on '$branch'\n" if $opt_v; + } +}; + +my $commitcount = 1; +while (<CVS>) { + chomp; + if ($state == 0 and /^-+$/) { + $state = 1; + } elsif ($state == 0) { + $state = 1; + redo; + } elsif (($state==0 or $state==1) and s/^PatchSet\s+//) { + $patchset = 0+$_; + $state=2; + } elsif ($state == 2 and s/^Date:\s+//) { + $date = pdate($_); + unless ($date) { + print STDERR "Could not parse date: $_\n"; + $state=0; + next; + } + $state=3; + } elsif ($state == 3 and s/^Author:\s+//) { + s/\s+$//; + if (/^(.*?)\s+<(.*)>/) { + ($author_name, $author_email) = ($1, $2); + } elsif ($conv_author_name{$_}) { + $author_name = $conv_author_name{$_}; + $author_email = $conv_author_email{$_}; + } else { + $author_name = $author_email = $_; + } + $state = 4; + } elsif ($state == 4 and s/^Branch:\s+//) { + s/\s+$//; + s/[\/]/$opt_s/g; + $branch = $_; + $state = 5; + } elsif ($state == 5 and s/^Ancestor branch:\s+//) { + s/\s+$//; + $ancestor = $_; + $ancestor = $opt_o if $ancestor eq "HEAD"; + $state = 6; + } elsif ($state == 5) { + $ancestor = undef; + $state = 6; + redo; + } elsif ($state == 6 and s/^Tag:\s+//) { + s/\s+$//; + if ($_ eq "(none)") { + $tag = undef; + } else { + $tag = $_; + } + $state = 7; + } elsif ($state == 7 and /^Log:/) { + $logmsg = ""; + $state = 8; + } elsif ($state == 8 and /^Members:/) { + $branch = $opt_o if $branch eq "HEAD"; + if (defined $branch_date{$branch} and $branch_date{$branch} >= $date) { + # skip + print "skip patchset $patchset: $date before $branch_date{$branch}\n" if $opt_v; + $state = 11; + next; + } + if (!$opt_a && $starttime - 300 - (defined $opt_z ? $opt_z : 300) <= $date) { + # skip if the commit is too recent + # that the cvsps default fuzz is 300s, we give ourselves another + # 300s just in case -- this also prevents skipping commits + # due to server clock drift + print "skip patchset $patchset: $date too recent\n" if $opt_v; + $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; + next; + } + unless (open(H,"$git_dir/refs/heads/$ancestor")) { + print STDERR "Branch $ancestor does not exist!\n"; + $ignorebranch{$branch} = 1; + $state=11; + next; + } + chomp(my $id = <H>); + close(H); + unless (open(H,"> $git_dir/refs/heads/$branch")) { + print STDERR "Could not create branch $branch: $!\n"; + $ignorebranch{$branch} = 1; + $state=11; + next; + } + print H "$id\n" + or die "Could not write branch $branch: $!"; + close(H) + or die "Could not write branch $branch: $!"; + } + $last_branch = $branch if $branch ne $last_branch; + $state = 9; + } elsif ($state == 8) { + $logmsg .= "$_\n"; + } elsif ($state == 9 and /^\s+(.+?):(INITIAL|\d+(?:\.\d+)+)->(\d+(?:\.\d+)+)\s*$/) { +# VERSION:1.96->1.96.2.1 + my $init = ($2 eq "INITIAL"); + my $fn = $1; + my $rev = $3; + $fn =~ s#^/+##; + if ($opt_S && $fn =~ m/$opt_S/) { + print "SKIPPING $fn v $rev\n"; + push(@skipped, $fn); + next; + } + print "Fetching $fn v $rev\n" if $opt_v; + my ($tmpname, $size) = $cvs->file($fn,$rev); + if ($size == -1) { + push(@old,$fn); + print "Drop $fn\n" if $opt_v; + } else { + print "".($init ? "New" : "Update")." $fn: $size bytes\n" if $opt_v; + my $pid = open(my $F, '-|'); + die $! unless defined $pid; + if (!$pid) { + exec("git-hash-object", "-w", $tmpname) + or die "Cannot create object: $!\n"; + } + my $sha = <$F>; + chomp $sha; + close $F; + my $mode = pmode($cvs->{'mode'}); + push(@new,[$mode, $sha, $fn]); # may be resurrected! + } + unlink($tmpname); + } elsif ($state == 9 and /^\s+(.+?):\d+(?:\.\d+)+->(\d+(?:\.\d+)+)\(DEAD\)\s*$/) { + my $fn = $1; + $fn =~ s#^/+##; + push(@old,$fn); + print "Delete $fn\n" if $opt_v; + } elsif ($state == 9 and /^\s*$/) { + $state = 10; + } elsif (($state == 9 or $state == 10) and /^-+$/) { + $commitcount++; + if ($opt_L && $commitcount > $opt_L) { + last; + } + commit(); + if (($commitcount & 1023) == 0) { + system("git repack -a -d"); + } + $state = 1; + } elsif ($state == 11 and /^-+$/) { + $state = 1; + } elsif (/^-+$/) { # end of unknown-line processing + $state = 1; + } elsif ($state != 11) { # ignore stuff when skipping + print "* UNKNOWN LINE * $_\n"; + } +} +commit() if $branch and $state != 11; + +unless ($opt_P) { + unlink($cvspsfile); +} + +# The heuristic of repacking every 1024 commits can leave a +# lot of unpacked data. If there is more than 1MB worth of +# not-packed objects, repack once more. +my $line = `git-count-objects`; +if ($line =~ /^(\d+) objects, (\d+) kilobytes$/) { + my ($n_objects, $kb) = ($1, $2); + 1024 < $kb + and system("git repack -a -d"); +} + +foreach my $git_index (values %index) { + if ($git_index ne '.git/index') { + unlink($git_index); + } +} + +if (defined $orig_git_index) { + $ENV{GIT_INDEX_FILE} = $orig_git_index; +} else { + delete $ENV{GIT_INDEX_FILE}; +} + +# Now switch back to the branch we were in before all of this happened +if ($orig_branch) { + print "DONE.\n" if $opt_v; + if ($opt_i) { + exit 0; + } + my $tip_at_end = `git-rev-parse --verify HEAD`; + if ($tip_at_start ne $tip_at_end) { + for ($tip_at_start, $tip_at_end) { chomp; } + print "Fetched into the current branch.\n" if $opt_v; + system(qw(git-read-tree -u -m), + $tip_at_start, $tip_at_end); + die "Fast-forward update failed: $?\n" if $?; + } + else { + system(qw(git-merge cvsimport HEAD), "refs/heads/$opt_o"); + die "Could not merge $opt_o into the current branch.\n" if $?; + } +} else { + $orig_branch = "master"; + print "DONE; creating $orig_branch branch\n" if $opt_v; + system("git-update-ref", "refs/heads/master", "refs/heads/$opt_o") + unless -f "$git_dir/refs/heads/master"; + system('git-update-ref', 'HEAD', "$orig_branch"); + unless ($opt_i) { + system('git checkout'); + die "checkout failed: $?\n" if $?; + } +} diff --git a/git-cvsserver.perl b/git-cvsserver.perl new file mode 100755 index 0000000000..9371788fab --- /dev/null +++ b/git-cvsserver.perl @@ -0,0 +1,2806 @@ +#!/usr/bin/perl + +#### +#### This application is a CVS emulation layer for git. +#### It is intended for clients to connect over SSH. +#### See the documentation for more details. +#### +#### Copyright The Open University UK - 2006. +#### +#### Authors: Martyn Smith <martyn@catalyst.net.nz> +#### Martin Langhoff <martin@catalyst.net.nz> +#### +#### +#### Released under the GNU Public License, version 2. +#### +#### + +use strict; +use warnings; +use bytes; + +use Fcntl; +use File::Temp qw/tempdir tempfile/; +use File::Basename; + +my $log = GITCVS::log->new(); +my $cfg; + +my $DATE_LIST = { + Jan => "01", + Feb => "02", + Mar => "03", + Apr => "04", + May => "05", + Jun => "06", + Jul => "07", + Aug => "08", + Sep => "09", + Oct => "10", + Nov => "11", + Dec => "12", +}; + +# Enable autoflush for STDOUT (otherwise the whole thing falls apart) +$| = 1; + +#### Definition and mappings of functions #### + +my $methods = { + 'Root' => \&req_Root, + 'Valid-responses' => \&req_Validresponses, + 'valid-requests' => \&req_validrequests, + 'Directory' => \&req_Directory, + 'Entry' => \&req_Entry, + 'Modified' => \&req_Modified, + 'Unchanged' => \&req_Unchanged, + 'Questionable' => \&req_Questionable, + 'Argument' => \&req_Argument, + 'Argumentx' => \&req_Argument, + 'expand-modules' => \&req_expandmodules, + 'add' => \&req_add, + 'remove' => \&req_remove, + 'co' => \&req_co, + 'update' => \&req_update, + 'ci' => \&req_ci, + 'diff' => \&req_diff, + 'log' => \&req_log, + 'rlog' => \&req_log, + 'tag' => \&req_CATCHALL, + 'status' => \&req_status, + 'admin' => \&req_CATCHALL, + 'history' => \&req_CATCHALL, + 'watchers' => \&req_CATCHALL, + 'editors' => \&req_CATCHALL, + 'annotate' => \&req_annotate, + 'Global_option' => \&req_Globaloption, + #'annotate' => \&req_CATCHALL, +}; + +############################################## + + +# $state holds all the bits of information the clients sends us that could +# potentially be useful when it comes to actually _doing_ something. +my $state = { prependdir => '' }; +$log->info("--------------- STARTING -----------------"); + +my $TEMP_DIR = tempdir( CLEANUP => 1 ); +$log->debug("Temporary directory is '$TEMP_DIR'"); + +# if we are called with a pserver argument, +# deal with the authentication cat before entering the +# main loop +if (@ARGV && $ARGV[0] eq 'pserver') { + my $line = <STDIN>; chomp $line; + unless( $line eq 'BEGIN AUTH REQUEST') { + die "E Do not understand $line - expecting BEGIN AUTH REQUEST\n"; + } + $line = <STDIN>; chomp $line; + req_Root('root', $line) # reuse Root + or die "E Invalid root $line \n"; + $line = <STDIN>; chomp $line; + unless ($line eq 'anonymous') { + print "E Only anonymous user allowed via pserver\n"; + print "I HATE YOU\n"; + } + $line = <STDIN>; chomp $line; # validate the password? + $line = <STDIN>; chomp $line; + unless ($line eq 'END AUTH REQUEST') { + die "E Do not understand $line -- expecting END AUTH REQUEST\n"; + } + print "I LOVE YOU\n"; + # and now back to our regular programme... +} + +# Keep going until the client closes the connection +while (<STDIN>) +{ + chomp; + + # Check to see if we've seen this method, and call appropriate function. + if ( /^([\w-]+)(?:\s+(.*))?$/ and defined($methods->{$1}) ) + { + # use the $methods hash to call the appropriate sub for this command + #$log->info("Method : $1"); + &{$methods->{$1}}($1,$2); + } else { + # log fatal because we don't understand this function. If this happens + # we're fairly screwed because we don't know if the client is expecting + # a response. If it is, the client will hang, we'll hang, and the whole + # thing will be custard. + $log->fatal("Don't understand command $_\n"); + die("Unknown command $_"); + } +} + +$log->debug("Processing time : user=" . (times)[0] . " system=" . (times)[1]); +$log->info("--------------- FINISH -----------------"); + +# Magic catchall method. +# This is the method that will handle all commands we haven't yet +# implemented. It simply sends a warning to the log file indicating a +# command that hasn't been implemented has been invoked. +sub req_CATCHALL +{ + my ( $cmd, $data ) = @_; + $log->warn("Unhandled command : req_$cmd : $data"); +} + + +# Root pathname \n +# Response expected: no. Tell the server which CVSROOT to use. Note that +# pathname is a local directory and not a fully qualified CVSROOT variable. +# pathname must already exist; if creating a new root, use the init +# request, not Root. pathname does not include the hostname of the server, +# how to access the server, etc.; by the time the CVS protocol is in use, +# connection, authentication, etc., are already taken care of. The Root +# request must be sent only once, and it must be sent before any requests +# other than Valid-responses, valid-requests, UseUnchanged, Set or init. +sub req_Root +{ + my ( $cmd, $data ) = @_; + $log->debug("req_Root : $data"); + + $state->{CVSROOT} = $data; + + $ENV{GIT_DIR} = $state->{CVSROOT} . "/"; + unless (-d $ENV{GIT_DIR} && -e $ENV{GIT_DIR}.'HEAD') { + print "E $ENV{GIT_DIR} does not seem to be a valid GIT repository\n"; + print "E \n"; + print "error 1 $ENV{GIT_DIR} is not a valid repository\n"; + return 0; + } + + my @gitvars = `git-config -l`; + if ($?) { + print "E problems executing git-config on the server -- this is not a git repository or the PATH is not set correctly.\n"; + print "E \n"; + print "error 1 - problem executing git-config\n"; + return 0; + } + foreach my $line ( @gitvars ) + { + next unless ( $line =~ /^(.*?)\.(.*?)=(.*)$/ ); + $cfg->{$1}{$2} = $3; + } + + unless ( defined ( $cfg->{gitcvs}{enabled} ) and $cfg->{gitcvs}{enabled} =~ /^\s*(1|true|yes)\s*$/i ) + { + print "E GITCVS emulation needs to be enabled on this repo\n"; + print "E the repo config file needs a [gitcvs] section added, and the parameter 'enabled' set to 1\n"; + print "E \n"; + print "error 1 GITCVS emulation disabled\n"; + return 0; + } + + if ( defined ( $cfg->{gitcvs}{logfile} ) ) + { + $log->setfile($cfg->{gitcvs}{logfile}); + } else { + $log->nofile(); + } + + return 1; +} + +# Global_option option \n +# Response expected: no. Transmit one of the global options `-q', `-Q', +# `-l', `-t', `-r', or `-n'. option must be one of those strings, no +# variations (such as combining of options) are allowed. For graceful +# handling of valid-requests, it is probably better to make new global +# options separate requests, rather than trying to add them to this +# request. +sub req_Globaloption +{ + my ( $cmd, $data ) = @_; + $log->debug("req_Globaloption : $data"); + $state->{globaloptions}{$data} = 1; +} + +# Valid-responses request-list \n +# Response expected: no. Tell the server what responses the client will +# accept. request-list is a space separated list of tokens. +sub req_Validresponses +{ + my ( $cmd, $data ) = @_; + $log->debug("req_Validresponses : $data"); + + # TODO : re-enable this, currently it's not particularly useful + #$state->{validresponses} = [ split /\s+/, $data ]; +} + +# valid-requests \n +# Response expected: yes. Ask the server to send back a Valid-requests +# response. +sub req_validrequests +{ + my ( $cmd, $data ) = @_; + + $log->debug("req_validrequests"); + + $log->debug("SEND : Valid-requests " . join(" ",keys %$methods)); + $log->debug("SEND : ok"); + + print "Valid-requests " . join(" ",keys %$methods) . "\n"; + print "ok\n"; +} + +# Directory local-directory \n +# Additional data: repository \n. Response expected: no. Tell the server +# what directory to use. The repository should be a directory name from a +# previous server response. Note that this both gives a default for Entry +# and Modified and also for ci and the other commands; normal usage is to +# send Directory for each directory in which there will be an Entry or +# Modified, and then a final Directory for the original directory, then the +# command. The local-directory is relative to the top level at which the +# command is occurring (i.e. the last Directory which is sent before the +# command); to indicate that top level, `.' should be sent for +# local-directory. +sub req_Directory +{ + my ( $cmd, $data ) = @_; + + my $repository = <STDIN>; + chomp $repository; + + + $state->{localdir} = $data; + $state->{repository} = $repository; + $state->{path} = $repository; + $state->{path} =~ s/^$state->{CVSROOT}\///; + $state->{module} = $1 if ($state->{path} =~ s/^(.*?)(\/|$)//); + $state->{path} .= "/" if ( $state->{path} =~ /\S/ ); + + $state->{directory} = $state->{localdir}; + $state->{directory} = "" if ( $state->{directory} eq "." ); + $state->{directory} .= "/" if ( $state->{directory} =~ /\S/ ); + + if ( (not defined($state->{prependdir}) or $state->{prependdir} eq '') and $state->{localdir} eq "." and $state->{path} =~ /\S/ ) + { + $log->info("Setting prepend to '$state->{path}'"); + $state->{prependdir} = $state->{path}; + foreach my $entry ( keys %{$state->{entries}} ) + { + $state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry}; + delete $state->{entries}{$entry}; + } + } + + if ( defined ( $state->{prependdir} ) ) + { + $log->debug("Prepending '$state->{prependdir}' to state|directory"); + $state->{directory} = $state->{prependdir} . $state->{directory} + } + $log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}"); +} + +# Entry entry-line \n +# Response expected: no. Tell the server what version of a file is on the +# local machine. The name in entry-line is a name relative to the directory +# most recently specified with Directory. If the user is operating on only +# some files in a directory, Entry requests for only those files need be +# included. If an Entry request is sent without Modified, Is-modified, or +# Unchanged, it means the file is lost (does not exist in the working +# directory). If both Entry and one of Modified, Is-modified, or Unchanged +# are sent for the same file, Entry must be sent first. For a given file, +# one can send Modified, Is-modified, or Unchanged, but not more than one +# of these three. +sub req_Entry +{ + my ( $cmd, $data ) = @_; + + #$log->debug("req_Entry : $data"); + + my @data = split(/\//, $data); + + $state->{entries}{$state->{directory}.$data[1]} = { + revision => $data[2], + conflict => $data[3], + options => $data[4], + tag_or_date => $data[5], + }; + + $log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'"); +} + +# Questionable filename \n +# Response expected: no. Additional data: no. Tell the server to check +# whether filename should be ignored, and if not, next time the server +# sends responses, send (in a M response) `?' followed by the directory and +# filename. filename must not contain `/'; it needs to be a file in the +# directory named by the most recent Directory request. +sub req_Questionable +{ + my ( $cmd, $data ) = @_; + + $log->debug("req_Questionable : $data"); + $state->{entries}{$state->{directory}.$data}{questionable} = 1; +} + +# add \n +# Response expected: yes. Add a file or directory. This uses any previous +# Argument, Directory, Entry, or Modified requests, if they have been sent. +# The last Directory sent specifies the working directory at the time of +# the operation. To add a directory, send the directory to be added using +# Directory and Argument requests. +sub req_add +{ + my ( $cmd, $data ) = @_; + + argsplit("add"); + + my $addcount = 0; + + foreach my $filename ( @{$state->{args}} ) + { + $filename = filecleanup($filename); + + unless ( defined ( $state->{entries}{$filename}{modified_filename} ) ) + { + print "E cvs add: nothing known about `$filename'\n"; + next; + } + # TODO : check we're not squashing an already existing file + if ( defined ( $state->{entries}{$filename}{revision} ) ) + { + print "E cvs add: `$filename' has already been entered\n"; + next; + } + + my ( $filepart, $dirpart ) = filenamesplit($filename, 1); + + print "E cvs add: scheduling file `$filename' for addition\n"; + + print "Checked-in $dirpart\n"; + print "$filename\n"; + print "/$filepart/0///\n"; + + $addcount++; + } + + if ( $addcount == 1 ) + { + print "E cvs add: use `cvs commit' to add this file permanently\n"; + } + elsif ( $addcount > 1 ) + { + print "E cvs add: use `cvs commit' to add these files permanently\n"; + } + + print "ok\n"; +} + +# remove \n +# Response expected: yes. Remove a file. This uses any previous Argument, +# Directory, Entry, or Modified requests, if they have been sent. The last +# Directory sent specifies the working directory at the time of the +# operation. Note that this request does not actually do anything to the +# repository; the only effect of a successful remove request is to supply +# the client with a new entries line containing `-' to indicate a removed +# file. In fact, the client probably could perform this operation without +# contacting the server, although using remove may cause the server to +# perform a few more checks. The client sends a subsequent ci request to +# actually record the removal in the repository. +sub req_remove +{ + my ( $cmd, $data ) = @_; + + argsplit("remove"); + + # Grab a handle to the SQLite db and do any necessary updates + my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log); + $updater->update(); + + #$log->debug("add state : " . Dumper($state)); + + my $rmcount = 0; + + foreach my $filename ( @{$state->{args}} ) + { + $filename = filecleanup($filename); + + if ( defined ( $state->{entries}{$filename}{unchanged} ) or defined ( $state->{entries}{$filename}{modified_filename} ) ) + { + print "E cvs remove: file `$filename' still in working directory\n"; + next; + } + + my $meta = $updater->getmeta($filename); + my $wrev = revparse($filename); + + unless ( defined ( $wrev ) ) + { + print "E cvs remove: nothing known about `$filename'\n"; + next; + } + + if ( defined($wrev) and $wrev < 0 ) + { + print "E cvs remove: file `$filename' already scheduled for removal\n"; + next; + } + + unless ( $wrev == $meta->{revision} ) + { + # TODO : not sure if the format of this message is quite correct. + print "E cvs remove: Up to date check failed for `$filename'\n"; + next; + } + + + my ( $filepart, $dirpart ) = filenamesplit($filename, 1); + + print "E cvs remove: scheduling `$filename' for removal\n"; + + print "Checked-in $dirpart\n"; + print "$filename\n"; + print "/$filepart/-1.$wrev///\n"; + + $rmcount++; + } + + if ( $rmcount == 1 ) + { + print "E cvs remove: use `cvs commit' to remove this file permanently\n"; + } + elsif ( $rmcount > 1 ) + { + print "E cvs remove: use `cvs commit' to remove these files permanently\n"; + } + + print "ok\n"; +} + +# Modified filename \n +# Response expected: no. Additional data: mode, \n, file transmission. Send +# the server a copy of one locally modified file. filename is a file within +# the most recent directory sent with Directory; it must not contain `/'. +# If the user is operating on only some files in a directory, only those +# files need to be included. This can also be sent without Entry, if there +# is no entry for the file. +sub req_Modified +{ + my ( $cmd, $data ) = @_; + + my $mode = <STDIN>; + chomp $mode; + my $size = <STDIN>; + chomp $size; + + # Grab config information + my $blocksize = 8192; + my $bytesleft = $size; + my $tmp; + + # Get a filehandle/name to write it to + my ( $fh, $filename ) = tempfile( DIR => $TEMP_DIR ); + + # Loop over file data writing out to temporary file. + while ( $bytesleft ) + { + $blocksize = $bytesleft if ( $bytesleft < $blocksize ); + read STDIN, $tmp, $blocksize; + print $fh $tmp; + $bytesleft -= $blocksize; + } + + close $fh; + + # Ensure we have something sensible for the file mode + if ( $mode =~ /u=(\w+)/ ) + { + $mode = $1; + } else { + $mode = "rw"; + } + + # Save the file data in $state + $state->{entries}{$state->{directory}.$data}{modified_filename} = $filename; + $state->{entries}{$state->{directory}.$data}{modified_mode} = $mode; + $state->{entries}{$state->{directory}.$data}{modified_hash} = `git-hash-object $filename`; + $state->{entries}{$state->{directory}.$data}{modified_hash} =~ s/\s.*$//s; + + #$log->debug("req_Modified : file=$data mode=$mode size=$size"); +} + +# Unchanged filename \n +# Response expected: no. Tell the server that filename has not been +# modified in the checked out directory. The filename is a file within the +# most recent directory sent with Directory; it must not contain `/'. +sub req_Unchanged +{ + my ( $cmd, $data ) = @_; + + $state->{entries}{$state->{directory}.$data}{unchanged} = 1; + + #$log->debug("req_Unchanged : $data"); +} + +# Argument text \n +# Response expected: no. Save argument for use in a subsequent command. +# Arguments accumulate until an argument-using command is given, at which +# point they are forgotten. +# Argumentx text \n +# Response expected: no. Append \n followed by text to the current argument +# being saved. +sub req_Argument +{ + my ( $cmd, $data ) = @_; + + # Argumentx means: append to last Argument (with a newline in front) + + $log->debug("$cmd : $data"); + + if ( $cmd eq 'Argumentx') { + ${$state->{arguments}}[$#{$state->{arguments}}] .= "\n" . $data; + } else { + push @{$state->{arguments}}, $data; + } +} + +# expand-modules \n +# Response expected: yes. Expand the modules which are specified in the +# arguments. Returns the data in Module-expansion responses. Note that the +# server can assume that this is checkout or export, not rtag or rdiff; the +# latter do not access the working directory and thus have no need to +# expand modules on the client side. Expand may not be the best word for +# what this request does. It does not necessarily tell you all the files +# contained in a module, for example. Basically it is a way of telling you +# which working directories the server needs to know about in order to +# handle a checkout of the specified modules. For example, suppose that the +# server has a module defined by +# aliasmodule -a 1dir +# That is, one can check out aliasmodule and it will take 1dir in the +# repository and check it out to 1dir in the working directory. Now suppose +# the client already has this module checked out and is planning on using +# the co request to update it. Without using expand-modules, the client +# would have two bad choices: it could either send information about all +# working directories under the current directory, which could be +# unnecessarily slow, or it could be ignorant of the fact that aliasmodule +# stands for 1dir, and neglect to send information for 1dir, which would +# lead to incorrect operation. With expand-modules, the client would first +# ask for the module to be expanded: +sub req_expandmodules +{ + my ( $cmd, $data ) = @_; + + argsplit(); + + $log->debug("req_expandmodules : " . ( defined($data) ? $data : "[NULL]" ) ); + + unless ( ref $state->{arguments} eq "ARRAY" ) + { + print "ok\n"; + return; + } + + foreach my $module ( @{$state->{arguments}} ) + { + $log->debug("SEND : Module-expansion $module"); + print "Module-expansion $module\n"; + } + + print "ok\n"; + statecleanup(); +} + +# co \n +# Response expected: yes. Get files from the repository. This uses any +# previous Argument, Directory, Entry, or Modified requests, if they have +# been sent. Arguments to this command are module names; the client cannot +# know what directories they correspond to except by (1) just sending the +# co request, and then seeing what directory names the server sends back in +# its responses, and (2) the expand-modules request. +sub req_co +{ + my ( $cmd, $data ) = @_; + + argsplit("co"); + + my $module = $state->{args}[0]; + my $checkout_path = $module; + + # use the user specified directory if we're given it + $checkout_path = $state->{opt}{d} if ( exists ( $state->{opt}{d} ) ); + + $log->debug("req_co : " . ( defined($data) ? $data : "[NULL]" ) ); + + $log->info("Checking out module '$module' ($state->{CVSROOT}) to '$checkout_path'"); + + $ENV{GIT_DIR} = $state->{CVSROOT} . "/"; + + # Grab a handle to the SQLite db and do any necessary updates + my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log); + $updater->update(); + + $checkout_path =~ s|/$||; # get rid of trailing slashes + + # Eclipse seems to need the Clear-sticky command + # to prepare the 'Entries' file for the new directory. + print "Clear-sticky $checkout_path/\n"; + print $state->{CVSROOT} . "/$module/\n"; + print "Clear-static-directory $checkout_path/\n"; + print $state->{CVSROOT} . "/$module/\n"; + print "Clear-sticky $checkout_path/\n"; # yes, twice + print $state->{CVSROOT} . "/$module/\n"; + print "Template $checkout_path/\n"; + print $state->{CVSROOT} . "/$module/\n"; + print "0\n"; + + # instruct the client that we're checking out to $checkout_path + print "E cvs checkout: Updating $checkout_path\n"; + + my %seendirs = (); + my $lastdir =''; + + # recursive + sub prepdir { + my ($dir, $repodir, $remotedir, $seendirs) = @_; + my $parent = dirname($dir); + $dir =~ s|/+$||; + $repodir =~ s|/+$||; + $remotedir =~ s|/+$||; + $parent =~ s|/+$||; + $log->debug("announcedir $dir, $repodir, $remotedir" ); + + if ($parent eq '.' || $parent eq './') { + $parent = ''; + } + # recurse to announce unseen parents first + if (length($parent) && !exists($seendirs->{$parent})) { + prepdir($parent, $repodir, $remotedir, $seendirs); + } + # Announce that we are going to modify at the parent level + if ($parent) { + print "E cvs checkout: Updating $remotedir/$parent\n"; + } else { + print "E cvs checkout: Updating $remotedir\n"; + } + print "Clear-sticky $remotedir/$parent/\n"; + print "$repodir/$parent/\n"; + + print "Clear-static-directory $remotedir/$dir/\n"; + print "$repodir/$dir/\n"; + print "Clear-sticky $remotedir/$parent/\n"; # yes, twice + print "$repodir/$parent/\n"; + print "Template $remotedir/$dir/\n"; + print "$repodir/$dir/\n"; + print "0\n"; + + $seendirs->{$dir} = 1; + } + + foreach my $git ( @{$updater->gethead} ) + { + # Don't want to check out deleted files + next if ( $git->{filehash} eq "deleted" ); + + ( $git->{name}, $git->{dir} ) = filenamesplit($git->{name}); + + if (length($git->{dir}) && $git->{dir} ne './' + && $git->{dir} ne $lastdir ) { + unless (exists($seendirs{$git->{dir}})) { + prepdir($git->{dir}, $state->{CVSROOT} . "/$module/", + $checkout_path, \%seendirs); + $lastdir = $git->{dir}; + $seendirs{$git->{dir}} = 1; + } + print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n"; + } + + # modification time of this file + print "Mod-time $git->{modified}\n"; + + # print some information to the client + if ( defined ( $git->{dir} ) and $git->{dir} ne "./" ) + { + print "M U $checkout_path/$git->{dir}$git->{name}\n"; + } else { + print "M U $checkout_path/$git->{name}\n"; + } + + # instruct client we're sending a file to put in this path + print "Created $checkout_path/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "\n"; + + print $state->{CVSROOT} . "/$module/" . ( defined ( $git->{dir} ) and $git->{dir} ne "./" ? $git->{dir} . "/" : "" ) . "$git->{name}\n"; + + # this is an "entries" line + print "/$git->{name}/1.$git->{revision}///\n"; + # permissions + print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n"; + + # transmit file + transmitfile($git->{filehash}); + } + + print "ok\n"; + + statecleanup(); +} + +# update \n +# Response expected: yes. Actually do a cvs update command. This uses any +# previous Argument, Directory, Entry, or Modified requests, if they have +# been sent. The last Directory sent specifies the working directory at the +# time of the operation. The -I option is not used--files which the client +# can decide whether to ignore are not mentioned and the client sends the +# Questionable request for others. +sub req_update +{ + my ( $cmd, $data ) = @_; + + $log->debug("req_update : " . ( defined($data) ? $data : "[NULL]" )); + + argsplit("update"); + + # + # It may just be a client exploring the available heads/modules + # in that case, list them as top level directories and leave it + # at that. Eclipse uses this technique to offer you a list of + # projects (heads in this case) to checkout. + # + if ($state->{module} eq '') { + print "E cvs update: Updating .\n"; + opendir HEADS, $state->{CVSROOT} . '/refs/heads'; + while (my $head = readdir(HEADS)) { + if (-f $state->{CVSROOT} . '/refs/heads/' . $head) { + print "E cvs update: New directory `$head'\n"; + } + } + closedir HEADS; + print "ok\n"; + return 1; + } + + + # Grab a handle to the SQLite db and do any necessary updates + my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log); + + $updater->update(); + + argsfromdir($updater); + + #$log->debug("update state : " . Dumper($state)); + + # foreach file specified on the command line ... + foreach my $filename ( @{$state->{args}} ) + { + $filename = filecleanup($filename); + + $log->debug("Processing file $filename"); + + # if we have a -C we should pretend we never saw modified stuff + if ( exists ( $state->{opt}{C} ) ) + { + delete $state->{entries}{$filename}{modified_hash}; + delete $state->{entries}{$filename}{modified_filename}; + $state->{entries}{$filename}{unchanged} = 1; + } + + my $meta; + if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^1\.(\d+)/ ) + { + $meta = $updater->getmeta($filename, $1); + } else { + $meta = $updater->getmeta($filename); + } + + if ( ! defined $meta ) + { + $meta = { + name => $filename, + revision => 0, + filehash => 'added' + }; + } + + my $oldmeta = $meta; + + my $wrev = revparse($filename); + + # If the working copy is an old revision, lets get that version too for comparison. + if ( defined($wrev) and $wrev != $meta->{revision} ) + { + $oldmeta = $updater->getmeta($filename, $wrev); + } + + #$log->debug("Target revision is $meta->{revision}, current working revision is $wrev"); + + # Files are up to date if the working copy and repo copy have the same revision, + # and the working copy is unmodified _and_ the user hasn't specified -C + next if ( defined ( $wrev ) + and defined($meta->{revision}) + and $wrev == $meta->{revision} + and $state->{entries}{$filename}{unchanged} + and not exists ( $state->{opt}{C} ) ); + + # If the working copy and repo copy have the same revision, + # but the working copy is modified, tell the client it's modified + if ( defined ( $wrev ) + and defined($meta->{revision}) + and $wrev == $meta->{revision} + and not exists ( $state->{opt}{C} ) ) + { + $log->info("Tell the client the file is modified"); + print "MT text M \n"; + print "MT fname $filename\n"; + print "MT newline\n"; + next; + } + + if ( $meta->{filehash} eq "deleted" ) + { + my ( $filepart, $dirpart ) = filenamesplit($filename,1); + + $log->info("Removing '$filename' from working copy (no longer in the repo)"); + + print "E cvs update: `$filename' is no longer in the repository\n"; + # Don't want to actually _DO_ the update if -n specified + unless ( $state->{globaloptions}{-n} ) { + print "Removed $dirpart\n"; + print "$filepart\n"; + } + } + elsif ( not defined ( $state->{entries}{$filename}{modified_hash} ) + or $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} + or $meta->{filehash} eq 'added' ) + { + # normal update, just send the new revision (either U=Update, + # or A=Add, or R=Remove) + if ( defined($wrev) && $wrev < 0 ) + { + $log->info("Tell the client the file is scheduled for removal"); + print "MT text R \n"; + print "MT fname $filename\n"; + print "MT newline\n"; + next; + } + elsif ( (!defined($wrev) || $wrev == 0) && (!defined($meta->{revision}) || $meta->{revision} == 0) ) + { + $log->info("Tell the client the file is scheduled for addition"); + print "MT text A \n"; + print "MT fname $filename\n"; + print "MT newline\n"; + next; + + } + else { + $log->info("Updating '$filename' to ".$meta->{revision}); + print "MT +updated\n"; + print "MT text U \n"; + print "MT fname $filename\n"; + print "MT newline\n"; + print "MT -updated\n"; + } + + my ( $filepart, $dirpart ) = filenamesplit($filename,1); + + # Don't want to actually _DO_ the update if -n specified + unless ( $state->{globaloptions}{-n} ) + { + if ( defined ( $wrev ) ) + { + # instruct client we're sending a file to put in this path as a replacement + print "Update-existing $dirpart\n"; + $log->debug("Updating existing file 'Update-existing $dirpart'"); + } else { + # instruct client we're sending a file to put in this path as a new file + print "Clear-static-directory $dirpart\n"; + print $state->{CVSROOT} . "/$state->{module}/$dirpart\n"; + print "Clear-sticky $dirpart\n"; + print $state->{CVSROOT} . "/$state->{module}/$dirpart\n"; + + $log->debug("Creating new file 'Created $dirpart'"); + print "Created $dirpart\n"; + } + print $state->{CVSROOT} . "/$state->{module}/$filename\n"; + + # this is an "entries" line + $log->debug("/$filepart/1.$meta->{revision}///"); + print "/$filepart/1.$meta->{revision}///\n"; + + # permissions + $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); + print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; + + # transmit file + transmitfile($meta->{filehash}); + } + } else { + $log->info("Updating '$filename'"); + my ( $filepart, $dirpart ) = filenamesplit($meta->{name},1); + + my $dir = tempdir( DIR => $TEMP_DIR, CLEANUP => 1 ) . "/"; + + chdir $dir; + my $file_local = $filepart . ".mine"; + system("ln","-s",$state->{entries}{$filename}{modified_filename}, $file_local); + my $file_old = $filepart . "." . $oldmeta->{revision}; + transmitfile($oldmeta->{filehash}, $file_old); + my $file_new = $filepart . "." . $meta->{revision}; + transmitfile($meta->{filehash}, $file_new); + + # we need to merge with the local changes ( M=successful merge, C=conflict merge ) + $log->info("Merging $file_local, $file_old, $file_new"); + + $log->debug("Temporary directory for merge is $dir"); + + my $return = system("git", "merge-file", $file_local, $file_old, $file_new); + $return >>= 8; + + if ( $return == 0 ) + { + $log->info("Merged successfully"); + print "M M $filename\n"; + $log->debug("Update-existing $dirpart"); + + # Don't want to actually _DO_ the update if -n specified + unless ( $state->{globaloptions}{-n} ) + { + print "Update-existing $dirpart\n"; + $log->debug($state->{CVSROOT} . "/$state->{module}/$filename"); + print $state->{CVSROOT} . "/$state->{module}/$filename\n"; + $log->debug("/$filepart/1.$meta->{revision}///"); + print "/$filepart/1.$meta->{revision}///\n"; + } + } + elsif ( $return == 1 ) + { + $log->info("Merged with conflicts"); + print "M C $filename\n"; + + # Don't want to actually _DO_ the update if -n specified + unless ( $state->{globaloptions}{-n} ) + { + print "Update-existing $dirpart\n"; + print $state->{CVSROOT} . "/$state->{module}/$filename\n"; + print "/$filepart/1.$meta->{revision}/+//\n"; + } + } + else + { + $log->warn("Merge failed"); + next; + } + + # Don't want to actually _DO_ the update if -n specified + unless ( $state->{globaloptions}{-n} ) + { + # permissions + $log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}"); + print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n"; + + # transmit file, format is single integer on a line by itself (file + # size) followed by the file contents + # TODO : we should copy files in blocks + my $data = `cat $file_local`; + $log->debug("File size : " . length($data)); + print length($data) . "\n"; + print $data; + } + + chdir "/"; + } + + } + + print "ok\n"; +} + +sub req_ci +{ + my ( $cmd, $data ) = @_; + + argsplit("ci"); + + #$log->debug("State : " . Dumper($state)); + + $log->info("req_ci : " . ( defined($data) ? $data : "[NULL]" )); + + if ( @ARGV && $ARGV[0] eq 'pserver') + { + print "error 1 pserver access cannot commit\n"; + exit; + } + + if ( -e $state->{CVSROOT} . "/index" ) + { + $log->warn("file 'index' already exists in the git repository"); + print "error 1 Index already exists in git repo\n"; + exit; + } + + my $lockfile = "$state->{CVSROOT}/refs/heads/$state->{module}.lock"; + unless ( sysopen(LOCKFILE,$lockfile,O_EXCL|O_CREAT|O_WRONLY) ) + { + $log->warn("lockfile '$lockfile' already exists, please try again"); + print "error 1 Lock file '$lockfile' already exists, please try again\n"; + exit; + } + + # Grab a handle to the SQLite db and do any necessary updates + my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log); + $updater->update(); + + my $tmpdir = tempdir ( DIR => $TEMP_DIR ); + my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 ); + $log->info("Lock successful, basing commit on '$tmpdir', index file is '$file_index'"); + + $ENV{GIT_DIR} = $state->{CVSROOT} . "/"; + $ENV{GIT_INDEX_FILE} = $file_index; + + chdir $tmpdir; + + # populate the temporary index based + system("git-read-tree", $state->{module}); + unless ($? == 0) + { + die "Error running git-read-tree $state->{module} $file_index $!"; + } + $log->info("Created index '$file_index' with for head $state->{module} - exit status $?"); + + + my @committedfiles = (); + + # foreach file specified on the command line ... + foreach my $filename ( @{$state->{args}} ) + { + my $committedfile = $filename; + $filename = filecleanup($filename); + + next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} ); + + my $meta = $updater->getmeta($filename); + + my $wrev = revparse($filename); + + my ( $filepart, $dirpart ) = filenamesplit($filename); + + # do a checkout of the file if it part of this tree + if ($wrev) { + system('git-checkout-index', '-f', '-u', $filename); + unless ($? == 0) { + die "Error running git-checkout-index -f -u $filename : $!"; + } + } + + my $addflag = 0; + my $rmflag = 0; + $rmflag = 1 if ( defined($wrev) and $wrev < 0 ); + $addflag = 1 unless ( -e $filename ); + + # Do up to date checking + unless ( $addflag or $wrev == $meta->{revision} or ( $rmflag and -$wrev == $meta->{revision} ) ) + { + # fail everything if an up to date check fails + print "error 1 Up to date check failed for $filename\n"; + close LOCKFILE; + unlink($lockfile); + chdir "/"; + exit; + } + + push @committedfiles, $committedfile; + $log->info("Committing $filename"); + + system("mkdir","-p",$dirpart) unless ( -d $dirpart ); + + unless ( $rmflag ) + { + $log->debug("rename $state->{entries}{$filename}{modified_filename} $filename"); + rename $state->{entries}{$filename}{modified_filename},$filename; + + # Calculate modes to remove + my $invmode = ""; + foreach ( qw (r w x) ) { $invmode .= $_ unless ( $state->{entries}{$filename}{modified_mode} =~ /$_/ ); } + + $log->debug("chmod u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode . " $filename"); + system("chmod","u+" . $state->{entries}{$filename}{modified_mode} . "-" . $invmode, $filename); + } + + if ( $rmflag ) + { + $log->info("Removing file '$filename'"); + unlink($filename); + system("git-update-index", "--remove", $filename); + } + elsif ( $addflag ) + { + $log->info("Adding file '$filename'"); + system("git-update-index", "--add", $filename); + } else { + $log->info("Updating file '$filename'"); + system("git-update-index", $filename); + } + } + + unless ( scalar(@committedfiles) > 0 ) + { + print "E No files to commit\n"; + print "ok\n"; + close LOCKFILE; + unlink($lockfile); + chdir "/"; + return; + } + + my $treehash = `git-write-tree`; + my $parenthash = `cat $ENV{GIT_DIR}refs/heads/$state->{module}`; + chomp $treehash; + chomp $parenthash; + + $log->debug("Treehash : $treehash, Parenthash : $parenthash"); + + # write our commit message out if we have one ... + my ( $msg_fh, $msg_filename ) = tempfile( DIR => $TEMP_DIR ); + print $msg_fh $state->{opt}{m};# if ( exists ( $state->{opt}{m} ) ); + print $msg_fh "\n\nvia git-CVS emulator\n"; + close $msg_fh; + + my $commithash = `git-commit-tree $treehash -p $parenthash < $msg_filename`; + $log->info("Commit hash : $commithash"); + + unless ( $commithash =~ /[a-zA-Z0-9]{40}/ ) + { + $log->warn("Commit failed (Invalid commit hash)"); + print "error 1 Commit failed (unknown reason)\n"; + close LOCKFILE; + unlink($lockfile); + chdir "/"; + exit; + } + + print LOCKFILE $commithash; + + $updater->update(); + + # foreach file specified on the command line ... + foreach my $filename ( @committedfiles ) + { + $filename = filecleanup($filename); + + my $meta = $updater->getmeta($filename); + unless (defined $meta->{revision}) { + $meta->{revision} = 1; + } + + my ( $filepart, $dirpart ) = filenamesplit($filename, 1); + + $log->debug("Checked-in $dirpart : $filename"); + + if ( defined $meta->{filehash} && $meta->{filehash} eq "deleted" ) + { + print "Remove-entry $dirpart\n"; + print "$filename\n"; + } else { + print "Checked-in $dirpart\n"; + print "$filename\n"; + print "/$filepart/1.$meta->{revision}///\n"; + } + } + + close LOCKFILE; + my $reffile = "$ENV{GIT_DIR}refs/heads/$state->{module}"; + unlink($reffile); + rename($lockfile, $reffile); + chdir "/"; + + print "ok\n"; +} + +sub req_status +{ + my ( $cmd, $data ) = @_; + + argsplit("status"); + + $log->info("req_status : " . ( defined($data) ? $data : "[NULL]" )); + #$log->debug("status state : " . Dumper($state)); + + # Grab a handle to the SQLite db and do any necessary updates + my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log); + $updater->update(); + + # if no files were specified, we need to work out what files we should be providing status on ... + argsfromdir($updater); + + # foreach file specified on the command line ... + foreach my $filename ( @{$state->{args}} ) + { + $filename = filecleanup($filename); + + my $meta = $updater->getmeta($filename); + my $oldmeta = $meta; + + my $wrev = revparse($filename); + + # If the working copy is an old revision, lets get that version too for comparison. + if ( defined($wrev) and $wrev != $meta->{revision} ) + { + $oldmeta = $updater->getmeta($filename, $wrev); + } + + # TODO : All possible statuses aren't yet implemented + my $status; + # Files are up to date if the working copy and repo copy have the same revision, and the working copy is unmodified + $status = "Up-to-date" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} + and + ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) + or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta->{filehash} ) ) + ); + + # Need checkout if the working copy has an older revision than the repo copy, and the working copy is unmodified + $status ||= "Needs Checkout" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev + and + ( $state->{entries}{$filename}{unchanged} + or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $oldmeta->{filehash} ) ) + ); + + # Need checkout if it exists in the repo but doesn't have a working copy + $status ||= "Needs Checkout" if ( not defined ( $wrev ) and defined ( $meta->{revision} ) ); + + # Locally modified if working copy and repo copy have the same revision but there are local changes + $status ||= "Locally Modified" if ( defined ( $wrev ) and defined($meta->{revision}) and $wrev == $meta->{revision} and $state->{entries}{$filename}{modified_filename} ); + + # Needs Merge if working copy revision is less than repo copy and there are local changes + $status ||= "Needs Merge" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and $meta->{revision} > $wrev and $state->{entries}{$filename}{modified_filename} ); + + $status ||= "Locally Added" if ( defined ( $state->{entries}{$filename}{revision} ) and not defined ( $meta->{revision} ) ); + $status ||= "Locally Removed" if ( defined ( $wrev ) and defined ( $meta->{revision} ) and -$wrev == $meta->{revision} ); + $status ||= "Unresolved Conflict" if ( defined ( $state->{entries}{$filename}{conflict} ) and $state->{entries}{$filename}{conflict} =~ /^\+=/ ); + $status ||= "File had conflicts on merge" if ( 0 ); + + $status ||= "Unknown"; + + print "M ===================================================================\n"; + print "M File: $filename\tStatus: $status\n"; + if ( defined($state->{entries}{$filename}{revision}) ) + { + print "M Working revision:\t" . $state->{entries}{$filename}{revision} . "\n"; + } else { + print "M Working revision:\tNo entry for $filename\n"; + } + if ( defined($meta->{revision}) ) + { + print "M Repository revision:\t1." . $meta->{revision} . "\t$state->{repository}/$filename,v\n"; + print "M Sticky Tag:\t\t(none)\n"; + print "M Sticky Date:\t\t(none)\n"; + print "M Sticky Options:\t\t(none)\n"; + } else { + print "M Repository revision:\tNo revision control file\n"; + } + print "M\n"; + } + + print "ok\n"; +} + +sub req_diff +{ + my ( $cmd, $data ) = @_; + + argsplit("diff"); + + $log->debug("req_diff : " . ( defined($data) ? $data : "[NULL]" )); + #$log->debug("status state : " . Dumper($state)); + + my ($revision1, $revision2); + if ( defined ( $state->{opt}{r} ) and ref $state->{opt}{r} eq "ARRAY" ) + { + $revision1 = $state->{opt}{r}[0]; + $revision2 = $state->{opt}{r}[1]; + } else { + $revision1 = $state->{opt}{r}; + } + + $revision1 =~ s/^1\.// if ( defined ( $revision1 ) ); + $revision2 =~ s/^1\.// if ( defined ( $revision2 ) ); + + $log->debug("Diffing revisions " . ( defined($revision1) ? $revision1 : "[NULL]" ) . " and " . ( defined($revision2) ? $revision2 : "[NULL]" ) ); + + # Grab a handle to the SQLite db and do any necessary updates + my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log); + $updater->update(); + + # if no files were specified, we need to work out what files we should be providing status on ... + argsfromdir($updater); + + # foreach file specified on the command line ... + foreach my $filename ( @{$state->{args}} ) + { + $filename = filecleanup($filename); + + my ( $fh, $file1, $file2, $meta1, $meta2, $filediff ); + + my $wrev = revparse($filename); + + # We need _something_ to diff against + next unless ( defined ( $wrev ) ); + + # if we have a -r switch, use it + if ( defined ( $revision1 ) ) + { + ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 ); + $meta1 = $updater->getmeta($filename, $revision1); + unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" ) + { + print "E File $filename at revision 1.$revision1 doesn't exist\n"; + next; + } + transmitfile($meta1->{filehash}, $file1); + } + # otherwise we just use the working copy revision + else + { + ( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 ); + $meta1 = $updater->getmeta($filename, $wrev); + transmitfile($meta1->{filehash}, $file1); + } + + # if we have a second -r switch, use it too + if ( defined ( $revision2 ) ) + { + ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 ); + $meta2 = $updater->getmeta($filename, $revision2); + + unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" ) + { + print "E File $filename at revision 1.$revision2 doesn't exist\n"; + next; + } + + transmitfile($meta2->{filehash}, $file2); + } + # otherwise we just use the working copy + else + { + $file2 = $state->{entries}{$filename}{modified_filename}; + } + + # if we have been given -r, and we don't have a $file2 yet, lets get one + if ( defined ( $revision1 ) and not defined ( $file2 ) ) + { + ( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 ); + $meta2 = $updater->getmeta($filename, $wrev); + transmitfile($meta2->{filehash}, $file2); + } + + # We need to have retrieved something useful + next unless ( defined ( $meta1 ) ); + + # Files to date if the working copy and repo copy have the same revision, and the working copy is unmodified + next if ( not defined ( $meta2 ) and $wrev == $meta1->{revision} + and + ( ( $state->{entries}{$filename}{unchanged} and ( not defined ( $state->{entries}{$filename}{conflict} ) or $state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) + or ( defined($state->{entries}{$filename}{modified_hash}) and $state->{entries}{$filename}{modified_hash} eq $meta1->{filehash} ) ) + ); + + # Apparently we only show diffs for locally modified files + next unless ( defined($meta2) or defined ( $state->{entries}{$filename}{modified_filename} ) ); + + print "M Index: $filename\n"; + print "M ===================================================================\n"; + print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n"; + print "M retrieving revision 1.$meta1->{revision}\n" if ( defined ( $meta1 ) ); + print "M retrieving revision 1.$meta2->{revision}\n" if ( defined ( $meta2 ) ); + print "M diff "; + foreach my $opt ( keys %{$state->{opt}} ) + { + if ( ref $state->{opt}{$opt} eq "ARRAY" ) + { + foreach my $value ( @{$state->{opt}{$opt}} ) + { + print "-$opt $value "; + } + } else { + print "-$opt "; + print "$state->{opt}{$opt} " if ( defined ( $state->{opt}{$opt} ) ); + } + } + print "$filename\n"; + + $log->info("Diffing $filename -r $meta1->{revision} -r " . ( $meta2->{revision} or "workingcopy" )); + + ( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR ); + + if ( exists $state->{opt}{u} ) + { + system("diff -u -L '$filename revision 1.$meta1->{revision}' -L '$filename " . ( defined($meta2->{revision}) ? "revision 1.$meta2->{revision}" : "working copy" ) . "' $file1 $file2 > $filediff"); + } else { + system("diff $file1 $file2 > $filediff"); + } + + while ( <$fh> ) + { + print "M $_"; + } + close $fh; + } + + print "ok\n"; +} + +sub req_log +{ + my ( $cmd, $data ) = @_; + + argsplit("log"); + + $log->debug("req_log : " . ( defined($data) ? $data : "[NULL]" )); + #$log->debug("log state : " . Dumper($state)); + + my ( $minrev, $maxrev ); + if ( defined ( $state->{opt}{r} ) and $state->{opt}{r} =~ /([\d.]+)?(::?)([\d.]+)?/ ) + { + my $control = $2; + $minrev = $1; + $maxrev = $3; + $minrev =~ s/^1\.// if ( defined ( $minrev ) ); + $maxrev =~ s/^1\.// if ( defined ( $maxrev ) ); + $minrev++ if ( defined($minrev) and $control eq "::" ); + } + + # Grab a handle to the SQLite db and do any necessary updates + my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log); + $updater->update(); + + # if no files were specified, we need to work out what files we should be providing status on ... + argsfromdir($updater); + + # foreach file specified on the command line ... + foreach my $filename ( @{$state->{args}} ) + { + $filename = filecleanup($filename); + + my $headmeta = $updater->getmeta($filename); + + my $revisions = $updater->getlog($filename); + my $totalrevisions = scalar(@$revisions); + + if ( defined ( $minrev ) ) + { + $log->debug("Removing revisions less than $minrev"); + while ( scalar(@$revisions) > 0 and $revisions->[-1]{revision} < $minrev ) + { + pop @$revisions; + } + } + if ( defined ( $maxrev ) ) + { + $log->debug("Removing revisions greater than $maxrev"); + while ( scalar(@$revisions) > 0 and $revisions->[0]{revision} > $maxrev ) + { + shift @$revisions; + } + } + + next unless ( scalar(@$revisions) ); + + print "M \n"; + print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n"; + print "M Working file: $filename\n"; + print "M head: 1.$headmeta->{revision}\n"; + print "M branch:\n"; + print "M locks: strict\n"; + print "M access list:\n"; + print "M symbolic names:\n"; + print "M keyword substitution: kv\n"; + print "M total revisions: $totalrevisions;\tselected revisions: " . scalar(@$revisions) . "\n"; + print "M description:\n"; + + foreach my $revision ( @$revisions ) + { + print "M ----------------------------\n"; + print "M revision 1.$revision->{revision}\n"; + # reformat the date for log output + $revision->{modified} = sprintf('%04d/%02d/%02d %s', $3, $DATE_LIST->{$2}, $1, $4 ) if ( $revision->{modified} =~ /(\d+)\s+(\w+)\s+(\d+)\s+(\S+)/ and defined($DATE_LIST->{$2}) ); + $revision->{author} =~ s/\s+.*//; + $revision->{author} =~ s/^(.{8}).*/$1/; + print "M date: $revision->{modified}; author: $revision->{author}; state: " . ( $revision->{filehash} eq "deleted" ? "dead" : "Exp" ) . "; lines: +2 -3\n"; + my $commitmessage = $updater->commitmessage($revision->{commithash}); + $commitmessage =~ s/^/M /mg; + print $commitmessage . "\n"; + } + print "M =============================================================================\n"; + } + + print "ok\n"; +} + +sub req_annotate +{ + my ( $cmd, $data ) = @_; + + argsplit("annotate"); + + $log->info("req_annotate : " . ( defined($data) ? $data : "[NULL]" )); + #$log->debug("status state : " . Dumper($state)); + + # Grab a handle to the SQLite db and do any necessary updates + my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log); + $updater->update(); + + # if no files were specified, we need to work out what files we should be providing annotate on ... + argsfromdir($updater); + + # we'll need a temporary checkout dir + my $tmpdir = tempdir ( DIR => $TEMP_DIR ); + my ( undef, $file_index ) = tempfile ( DIR => $TEMP_DIR, OPEN => 0 ); + $log->info("Temp checkoutdir creation successful, basing annotate session work on '$tmpdir', index file is '$file_index'"); + + $ENV{GIT_DIR} = $state->{CVSROOT} . "/"; + $ENV{GIT_INDEX_FILE} = $file_index; + + chdir $tmpdir; + + # foreach file specified on the command line ... + foreach my $filename ( @{$state->{args}} ) + { + $filename = filecleanup($filename); + + my $meta = $updater->getmeta($filename); + + next unless ( $meta->{revision} ); + + # get all the commits that this file was in + # in dense format -- aka skip dead revisions + my $revisions = $updater->gethistorydense($filename); + my $lastseenin = $revisions->[0][2]; + + # populate the temporary index based on the latest commit were we saw + # the file -- but do it cheaply without checking out any files + # TODO: if we got a revision from the client, use that instead + # to look up the commithash in sqlite (still good to default to + # the current head as we do now) + system("git-read-tree", $lastseenin); + unless ($? == 0) + { + die "Error running git-read-tree $lastseenin $file_index $!"; + } + $log->info("Created index '$file_index' with commit $lastseenin - exit status $?"); + + # do a checkout of the file + system('git-checkout-index', '-f', '-u', $filename); + unless ($? == 0) { + die "Error running git-checkout-index -f -u $filename : $!"; + } + + $log->info("Annotate $filename"); + + # Prepare a file with the commits from the linearized + # history that annotate should know about. This prevents + # git-jsannotate telling us about commits we are hiding + # from the client. + + open(ANNOTATEHINTS, ">$tmpdir/.annotate_hints") or die "Error opening > $tmpdir/.annotate_hints $!"; + for (my $i=0; $i < @$revisions; $i++) + { + print ANNOTATEHINTS $revisions->[$i][2]; + if ($i+1 < @$revisions) { # have we got a parent? + print ANNOTATEHINTS ' ' . $revisions->[$i+1][2]; + } + print ANNOTATEHINTS "\n"; + } + + print ANNOTATEHINTS "\n"; + close ANNOTATEHINTS; + + my $annotatecmd = 'git-annotate'; + open(ANNOTATE, "-|", $annotatecmd, '-l', '-S', "$tmpdir/.annotate_hints", $filename) + or die "Error invoking $annotatecmd -l -S $tmpdir/.annotate_hints $filename : $!"; + my $metadata = {}; + print "E Annotations for $filename\n"; + print "E ***************\n"; + while ( <ANNOTATE> ) + { + if (m/^([a-zA-Z0-9]{40})\t\([^\)]*\)(.*)$/i) + { + my $commithash = $1; + my $data = $2; + unless ( defined ( $metadata->{$commithash} ) ) + { + $metadata->{$commithash} = $updater->getmeta($filename, $commithash); + $metadata->{$commithash}{author} =~ s/\s+.*//; + $metadata->{$commithash}{author} =~ s/^(.{8}).*/$1/; + $metadata->{$commithash}{modified} = sprintf("%02d-%s-%02d", $1, $2, $3) if ( $metadata->{$commithash}{modified} =~ /^(\d+)\s(\w+)\s\d\d(\d\d)/ ); + } + printf("M 1.%-5d (%-8s %10s): %s\n", + $metadata->{$commithash}{revision}, + $metadata->{$commithash}{author}, + $metadata->{$commithash}{modified}, + $data + ); + } else { + $log->warn("Error in annotate output! LINE: $_"); + print "E Annotate error \n"; + next; + } + } + close ANNOTATE; + } + + # done; get out of the tempdir + chdir "/"; + + print "ok\n"; + +} + +# This method takes the state->{arguments} array and produces two new arrays. +# The first is $state->{args} which is everything before the '--' argument, and +# the second is $state->{files} which is everything after it. +sub argsplit +{ + return unless( defined($state->{arguments}) and ref $state->{arguments} eq "ARRAY" ); + + my $type = shift; + + $state->{args} = []; + $state->{files} = []; + $state->{opt} = {}; + + if ( defined($type) ) + { + my $opt = {}; + $opt = { A => 0, N => 0, P => 0, R => 0, c => 0, f => 0, l => 0, n => 0, p => 0, s => 0, r => 1, D => 1, d => 1, k => 1, j => 1, } if ( $type eq "co" ); + $opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" ); + $opt = { A => 0, P => 0, C => 0, d => 0, f => 0, l => 0, R => 0, p => 0, k => 1, r => 1, D => 1, j => 1, I => 1, W => 1 } if ( $type eq "update" ); + $opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" ); + $opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" ); + $opt = { k => 1, m => 1 } if ( $type eq "add" ); + $opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" ); + $opt = { l => 0, b => 0, h => 0, R => 0, t => 0, N => 0, S => 0, r => 1, d => 1, s => 1, w => 1 } if ( $type eq "log" ); + + + while ( scalar ( @{$state->{arguments}} ) > 0 ) + { + my $arg = shift @{$state->{arguments}}; + + next if ( $arg eq "--" ); + next unless ( $arg =~ /\S/ ); + + # if the argument looks like a switch + if ( $arg =~ /^-(\w)(.*)/ ) + { + # if it's a switch that takes an argument + if ( $opt->{$1} ) + { + # If this switch has already been provided + if ( $opt->{$1} > 1 and exists ( $state->{opt}{$1} ) ) + { + $state->{opt}{$1} = [ $state->{opt}{$1} ]; + if ( length($2) > 0 ) + { + push @{$state->{opt}{$1}},$2; + } else { + push @{$state->{opt}{$1}}, shift @{$state->{arguments}}; + } + } else { + # if there's extra data in the arg, use that as the argument for the switch + if ( length($2) > 0 ) + { + $state->{opt}{$1} = $2; + } else { + $state->{opt}{$1} = shift @{$state->{arguments}}; + } + } + } else { + $state->{opt}{$1} = undef; + } + } + else + { + push @{$state->{args}}, $arg; + } + } + } + else + { + my $mode = 0; + + foreach my $value ( @{$state->{arguments}} ) + { + if ( $value eq "--" ) + { + $mode++; + next; + } + push @{$state->{args}}, $value if ( $mode == 0 ); + push @{$state->{files}}, $value if ( $mode == 1 ); + } + } +} + +# This method uses $state->{directory} to populate $state->{args} with a list of filenames +sub argsfromdir +{ + my $updater = shift; + + $state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." ); + + return if ( scalar ( @{$state->{args}} ) > 1 ); + + my @gethead = @{$updater->gethead}; + + # push added files + foreach my $file (keys %{$state->{entries}}) { + if ( exists $state->{entries}{$file}{revision} && + $state->{entries}{$file}{revision} == 0 ) + { + push @gethead, { name => $file, filehash => 'added' }; + } + } + + if ( scalar(@{$state->{args}}) == 1 ) + { + my $arg = $state->{args}[0]; + $arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) ); + + $log->info("Only one arg specified, checking for directory expansion on '$arg'"); + + foreach my $file ( @gethead ) + { + next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) ); + next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg ); + push @{$state->{args}}, $file->{name}; + } + + shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 ); + } else { + $log->info("Only one arg specified, populating file list automatically"); + + $state->{args} = []; + + foreach my $file ( @gethead ) + { + next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) ); + next unless ( $file->{name} =~ s/^$state->{prependdir}// ); + push @{$state->{args}}, $file->{name}; + } + } +} + +# This method cleans up the $state variable after a command that uses arguments has run +sub statecleanup +{ + $state->{files} = []; + $state->{args} = []; + $state->{arguments} = []; + $state->{entries} = {}; +} + +sub revparse +{ + my $filename = shift; + + return undef unless ( defined ( $state->{entries}{$filename}{revision} ) ); + + return $1 if ( $state->{entries}{$filename}{revision} =~ /^1\.(\d+)/ ); + return -$1 if ( $state->{entries}{$filename}{revision} =~ /^-1\.(\d+)/ ); + + return undef; +} + +# This method takes a file hash and does a CVS "file transfer" which transmits the +# size of the file, and then the file contents. +# If a second argument $targetfile is given, the file is instead written out to +# a file by the name of $targetfile +sub transmitfile +{ + my $filehash = shift; + my $targetfile = shift; + + if ( defined ( $filehash ) and $filehash eq "deleted" ) + { + $log->warn("filehash is 'deleted'"); + return; + } + + die "Need filehash" unless ( defined ( $filehash ) and $filehash =~ /^[a-zA-Z0-9]{40}$/ ); + + my $type = `git-cat-file -t $filehash`; + chomp $type; + + die ( "Invalid type '$type' (expected 'blob')" ) unless ( defined ( $type ) and $type eq "blob" ); + + my $size = `git-cat-file -s $filehash`; + chomp $size; + + $log->debug("transmitfile($filehash) size=$size, type=$type"); + + if ( open my $fh, '-|', "git-cat-file", "blob", $filehash ) + { + if ( defined ( $targetfile ) ) + { + open NEWFILE, ">", $targetfile or die("Couldn't open '$targetfile' for writing : $!"); + print NEWFILE $_ while ( <$fh> ); + close NEWFILE; + } else { + print "$size\n"; + print while ( <$fh> ); + } + close $fh or die ("Couldn't close filehandle for transmitfile()"); + } else { + die("Couldn't execute git-cat-file"); + } +} + +# This method takes a file name, and returns ( $dirpart, $filepart ) which +# refers to the directory portion and the file portion of the filename +# respectively +sub filenamesplit +{ + my $filename = shift; + my $fixforlocaldir = shift; + + my ( $filepart, $dirpart ) = ( $filename, "." ); + ( $filepart, $dirpart ) = ( $2, $1 ) if ( $filename =~ /(.*)\/(.*)/ ); + $dirpart .= "/"; + + if ( $fixforlocaldir ) + { + $dirpart =~ s/^$state->{prependdir}//; + } + + return ( $filepart, $dirpart ); +} + +sub filecleanup +{ + my $filename = shift; + + return undef unless(defined($filename)); + if ( $filename =~ /^\// ) + { + print "E absolute filenames '$filename' not supported by server\n"; + return undef; + } + + $filename =~ s/^\.\///g; + $filename = $state->{prependdir} . $filename; + return $filename; +} + +package GITCVS::log; + +#### +#### Copyright The Open University UK - 2006. +#### +#### Authors: Martyn Smith <martyn@catalyst.net.nz> +#### Martin Langhoff <martin@catalyst.net.nz> +#### +#### + +use strict; +use warnings; + +=head1 NAME + +GITCVS::log + +=head1 DESCRIPTION + +This module provides very crude logging with a similar interface to +Log::Log4perl + +=head1 METHODS + +=cut + +=head2 new + +Creates a new log object, optionally you can specify a filename here to +indicate the file to log to. If no log file is specified, you can specify one +later with method setfile, or indicate you no longer want logging with method +nofile. + +Until one of these methods is called, all log calls will buffer messages ready +to write out. + +=cut +sub new +{ + my $class = shift; + my $filename = shift; + + my $self = {}; + + bless $self, $class; + + if ( defined ( $filename ) ) + { + open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!"); + } + + return $self; +} + +=head2 setfile + +This methods takes a filename, and attempts to open that file as the log file. +If successful, all buffered data is written out to the file, and any further +logging is written directly to the file. + +=cut +sub setfile +{ + my $self = shift; + my $filename = shift; + + if ( defined ( $filename ) ) + { + open $self->{fh}, ">>", $filename or die("Couldn't open '$filename' for writing : $!"); + } + + return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" ); + + while ( my $line = shift @{$self->{buffer}} ) + { + print {$self->{fh}} $line; + } +} + +=head2 nofile + +This method indicates no logging is going to be used. It flushes any entries in +the internal buffer, and sets a flag to ensure no further data is put there. + +=cut +sub nofile +{ + my $self = shift; + + $self->{nolog} = 1; + + return unless ( defined ( $self->{buffer} ) and ref $self->{buffer} eq "ARRAY" ); + + $self->{buffer} = []; +} + +=head2 _logopen + +Internal method. Returns true if the log file is open, false otherwise. + +=cut +sub _logopen +{ + my $self = shift; + + return 1 if ( defined ( $self->{fh} ) and ref $self->{fh} eq "GLOB" ); + return 0; +} + +=head2 debug info warn fatal + +These four methods are wrappers to _log. They provide the actual interface for +logging data. + +=cut +sub debug { my $self = shift; $self->_log("debug", @_); } +sub info { my $self = shift; $self->_log("info" , @_); } +sub warn { my $self = shift; $self->_log("warn" , @_); } +sub fatal { my $self = shift; $self->_log("fatal", @_); } + +=head2 _log + +This is an internal method called by the logging functions. It generates a +timestamp and pushes the logged line either to file, or internal buffer. + +=cut +sub _log +{ + my $self = shift; + my $level = shift; + + return if ( $self->{nolog} ); + + my @time = localtime; + my $timestring = sprintf("%4d-%02d-%02d %02d:%02d:%02d : %-5s", + $time[5] + 1900, + $time[4] + 1, + $time[3], + $time[2], + $time[1], + $time[0], + uc $level, + ); + + if ( $self->_logopen ) + { + print {$self->{fh}} $timestring . " - " . join(" ",@_) . "\n"; + } else { + push @{$self->{buffer}}, $timestring . " - " . join(" ",@_) . "\n"; + } +} + +=head2 DESTROY + +This method simply closes the file handle if one is open + +=cut +sub DESTROY +{ + my $self = shift; + + if ( $self->_logopen ) + { + close $self->{fh}; + } +} + +package GITCVS::updater; + +#### +#### Copyright The Open University UK - 2006. +#### +#### Authors: Martyn Smith <martyn@catalyst.net.nz> +#### Martin Langhoff <martin@catalyst.net.nz> +#### +#### + +use strict; +use warnings; +use DBI; + +=head1 METHODS + +=cut + +=head2 new + +=cut +sub new +{ + my $class = shift; + my $config = shift; + my $module = shift; + my $log = shift; + + die "Need to specify a git repository" unless ( defined($config) and -d $config ); + die "Need to specify a module" unless ( defined($module) ); + + $class = ref($class) || $class; + + my $self = {}; + + bless $self, $class; + + $self->{dbdir} = $config . "/"; + die "Database dir '$self->{dbdir}' isn't a directory" unless ( defined($self->{dbdir}) and -d $self->{dbdir} ); + + $self->{module} = $module; + $self->{file} = $self->{dbdir} . "/gitcvs.$module.sqlite"; + + $self->{git_path} = $config . "/"; + + $self->{log} = $log; + + die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} ); + + $self->{dbh} = DBI->connect("dbi:SQLite:dbname=" . $self->{file},"",""); + + $self->{tables} = {}; + foreach my $table ( $self->{dbh}->tables ) + { + $table =~ s/^"//; + $table =~ s/"$//; + $self->{tables}{$table} = 1; + } + + # Construct the revision table if required + unless ( $self->{tables}{revision} ) + { + $self->{dbh}->do(" + CREATE TABLE revision ( + name TEXT NOT NULL, + revision INTEGER NOT NULL, + filehash TEXT NOT NULL, + commithash TEXT NOT NULL, + author TEXT NOT NULL, + modified TEXT NOT NULL, + mode TEXT NOT NULL + ) + "); + $self->{dbh}->do(" + CREATE INDEX revision_ix1 + ON revision (name,revision) + "); + $self->{dbh}->do(" + CREATE INDEX revision_ix2 + ON revision (name,commithash) + "); + } + + # Construct the head table if required + unless ( $self->{tables}{head} ) + { + $self->{dbh}->do(" + CREATE TABLE head ( + name TEXT NOT NULL, + revision INTEGER NOT NULL, + filehash TEXT NOT NULL, + commithash TEXT NOT NULL, + author TEXT NOT NULL, + modified TEXT NOT NULL, + mode TEXT NOT NULL + ) + "); + $self->{dbh}->do(" + CREATE INDEX head_ix1 + ON head (name) + "); + } + + # Construct the properties table if required + unless ( $self->{tables}{properties} ) + { + $self->{dbh}->do(" + CREATE TABLE properties ( + key TEXT NOT NULL PRIMARY KEY, + value TEXT + ) + "); + } + + # Construct the commitmsgs table if required + unless ( $self->{tables}{commitmsgs} ) + { + $self->{dbh}->do(" + CREATE TABLE commitmsgs ( + key TEXT NOT NULL PRIMARY KEY, + value TEXT + ) + "); + } + + return $self; +} + +=head2 update + +=cut +sub update +{ + my $self = shift; + + # first lets get the commit list + $ENV{GIT_DIR} = $self->{git_path}; + + my $commitsha1 = `git rev-parse $self->{module}`; + chomp $commitsha1; + + my $commitinfo = `git cat-file commit $self->{module} 2>&1`; + unless ( $commitinfo =~ /tree\s+[a-zA-Z0-9]{40}/ ) + { + die("Invalid module '$self->{module}'"); + } + + + my $git_log; + my $lastcommit = $self->_get_prop("last_commit"); + + if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date + return 1; + } + + # Start exclusive lock here... + $self->{dbh}->begin_work() or die "Cannot lock database for BEGIN"; + + # TODO: log processing is memory bound + # if we can parse into a 2nd file that is in reverse order + # we can probably do something really efficient + my @git_log_params = ('--pretty', '--parents', '--topo-order'); + + if (defined $lastcommit) { + push @git_log_params, "$lastcommit..$self->{module}"; + } else { + push @git_log_params, $self->{module}; + } + # git-rev-list is the backend / plumbing version of git-log + open(GITLOG, '-|', 'git-rev-list', @git_log_params) or die "Cannot call git-rev-list: $!"; + + my @commits; + + my %commit = (); + + while ( <GITLOG> ) + { + chomp; + if (m/^commit\s+(.*)$/) { + # on ^commit lines put the just seen commit in the stack + # and prime things for the next one + if (keys %commit) { + my %copy = %commit; + unshift @commits, \%copy; + %commit = (); + } + my @parents = split(m/\s+/, $1); + $commit{hash} = shift @parents; + $commit{parents} = \@parents; + } elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) { + # on rfc822-like lines seen before we see any message, + # lowercase the entry and put it in the hash as key-value + $commit{lc($1)} = $2; + } else { + # message lines - skip initial empty line + # and trim whitespace + if (!exists($commit{message}) && m/^\s*$/) { + # define it to mark the end of headers + $commit{message} = ''; + next; + } + s/^\s+//; s/\s+$//; # trim ws + $commit{message} .= $_ . "\n"; + } + } + close GITLOG; + + unshift @commits, \%commit if ( keys %commit ); + + # Now all the commits are in the @commits bucket + # ordered by time DESC. for each commit that needs processing, + # determine whether it's following the last head we've seen or if + # it's on its own branch, grab a file list, and add whatever's changed + # NOTE: $lastcommit refers to the last commit from previous run + # $lastpicked is the last commit we picked in this run + my $lastpicked; + my $head = {}; + if (defined $lastcommit) { + $lastpicked = $lastcommit; + } + + my $committotal = scalar(@commits); + my $commitcount = 0; + + # Load the head table into $head (for cached lookups during the update process) + foreach my $file ( @{$self->gethead()} ) + { + $head->{$file->{name}} = $file; + } + + foreach my $commit ( @commits ) + { + $self->{log}->debug("GITCVS::updater - Processing commit $commit->{hash} (" . (++$commitcount) . " of $committotal)"); + if (defined $lastpicked) + { + if (!in_array($lastpicked, @{$commit->{parents}})) + { + # skip, we'll see this delta + # as part of a merge later + # warn "skipping off-track $commit->{hash}\n"; + next; + } elsif (@{$commit->{parents}} > 1) { + # it is a merge commit, for each parent that is + # not $lastpicked, see if we can get a log + # from the merge-base to that parent to put it + # in the message as a merge summary. + my @parents = @{$commit->{parents}}; + foreach my $parent (@parents) { + # git-merge-base can potentially (but rarely) throw + # several candidate merge bases. let's assume + # that the first one is the best one. + if ($parent eq $lastpicked) { + next; + } + open my $p, 'git-merge-base '. $lastpicked . ' ' + . $parent . '|'; + my @output = (<$p>); + close $p; + my $base = join('', @output); + chomp $base; + if ($base) { + my @merged; + # print "want to log between $base $parent \n"; + open(GITLOG, '-|', 'git-log', "$base..$parent") + or die "Cannot call git-log: $!"; + my $mergedhash; + while (<GITLOG>) { + chomp; + if (!defined $mergedhash) { + if (m/^commit\s+(.+)$/) { + $mergedhash = $1; + } else { + next; + } + } else { + # grab the first line that looks non-rfc822 + # aka has content after leading space + if (m/^\s+(\S.*)$/) { + my $title = $1; + $title = substr($title,0,100); # truncate + unshift @merged, "$mergedhash $title"; + undef $mergedhash; + } + } + } + close GITLOG; + if (@merged) { + $commit->{mergemsg} = $commit->{message}; + $commit->{mergemsg} .= "\nSummary of merged commits:\n\n"; + foreach my $summary (@merged) { + $commit->{mergemsg} .= "\t$summary\n"; + } + $commit->{mergemsg} .= "\n\n"; + # print "Message for $commit->{hash} \n$commit->{mergemsg}"; + } + } + } + } + } + + # convert the date to CVS-happy format + $commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ ); + + if ( defined ( $lastpicked ) ) + { + my $filepipe = open(FILELIST, '-|', 'git-diff-tree', '-z', '-r', $lastpicked, $commit->{hash}) or die("Cannot call git-diff-tree : $!"); + local ($/) = "\0"; + while ( <FILELIST> ) + { + chomp; + unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o ) + { + die("Couldn't process git-diff-tree line : $_"); + } + my ($mode, $hash, $change) = ($1, $2, $3); + my $name = <FILELIST>; + chomp($name); + + # $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name"); + + my $git_perms = ""; + $git_perms .= "r" if ( $mode & 4 ); + $git_perms .= "w" if ( $mode & 2 ); + $git_perms .= "x" if ( $mode & 1 ); + $git_perms = "rw" if ( $git_perms eq "" ); + + if ( $change eq "D" ) + { + #$log->debug("DELETE $name"); + $head->{$name} = { + name => $name, + revision => $head->{$name}{revision} + 1, + filehash => "deleted", + commithash => $commit->{hash}, + modified => $commit->{date}, + author => $commit->{author}, + mode => $git_perms, + }; + $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms); + } + elsif ( $change eq "M" ) + { + #$log->debug("MODIFIED $name"); + $head->{$name} = { + name => $name, + revision => $head->{$name}{revision} + 1, + filehash => $hash, + commithash => $commit->{hash}, + modified => $commit->{date}, + author => $commit->{author}, + mode => $git_perms, + }; + $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms); + } + elsif ( $change eq "A" ) + { + #$log->debug("ADDED $name"); + $head->{$name} = { + name => $name, + revision => 1, + filehash => $hash, + commithash => $commit->{hash}, + modified => $commit->{date}, + author => $commit->{author}, + mode => $git_perms, + }; + $self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms); + } + else + { + $log->warn("UNKNOWN FILE CHANGE mode=$mode, hash=$hash, change=$change, name=$name"); + die; + } + } + close FILELIST; + } else { + # this is used to detect files removed from the repo + my $seen_files = {}; + + my $filepipe = open(FILELIST, '-|', 'git-ls-tree', '-z', '-r', $commit->{hash}) or die("Cannot call git-ls-tree : $!"); + local $/ = "\0"; + while ( <FILELIST> ) + { + chomp; + unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o ) + { + die("Couldn't process git-ls-tree line : $_"); + } + + my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 ); + + $seen_files->{$git_filename} = 1; + + my ( $oldhash, $oldrevision, $oldmode ) = ( + $head->{$git_filename}{filehash}, + $head->{$git_filename}{revision}, + $head->{$git_filename}{mode} + ); + + if ( $git_perms =~ /^\d\d\d(\d)\d\d/o ) + { + $git_perms = ""; + $git_perms .= "r" if ( $1 & 4 ); + $git_perms .= "w" if ( $1 & 2 ); + $git_perms .= "x" if ( $1 & 1 ); + } else { + $git_perms = "rw"; + } + + # unless the file exists with the same hash, we need to update it ... + unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms ) + { + my $newrevision = ( $oldrevision or 0 ) + 1; + + $head->{$git_filename} = { + name => $git_filename, + revision => $newrevision, + filehash => $git_hash, + commithash => $commit->{hash}, + modified => $commit->{date}, + author => $commit->{author}, + mode => $git_perms, + }; + + + $self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms); + } + } + close FILELIST; + + # Detect deleted files + foreach my $file ( keys %$head ) + { + unless ( exists $seen_files->{$file} or $head->{$file}{filehash} eq "deleted" ) + { + $head->{$file}{revision}++; + $head->{$file}{filehash} = "deleted"; + $head->{$file}{commithash} = $commit->{hash}; + $head->{$file}{modified} = $commit->{date}; + $head->{$file}{author} = $commit->{author}; + + $self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode}); + } + } + # END : "Detect deleted files" + } + + + if (exists $commit->{mergemsg}) + { + $self->insert_mergelog($commit->{hash}, $commit->{mergemsg}); + } + + $lastpicked = $commit->{hash}; + + $self->_set_prop("last_commit", $commit->{hash}); + } + + $self->delete_head(); + foreach my $file ( keys %$head ) + { + $self->insert_head( + $file, + $head->{$file}{revision}, + $head->{$file}{filehash}, + $head->{$file}{commithash}, + $head->{$file}{modified}, + $head->{$file}{author}, + $head->{$file}{mode}, + ); + } + # invalidate the gethead cache + $self->{gethead_cache} = undef; + + + # Ending exclusive lock here + $self->{dbh}->commit() or die "Failed to commit changes to SQLite"; +} + +sub insert_rev +{ + my $self = shift; + my $name = shift; + my $revision = shift; + my $filehash = shift; + my $commithash = shift; + my $modified = shift; + my $author = shift; + my $mode = shift; + + my $insert_rev = $self->{dbh}->prepare_cached("INSERT INTO revision (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1); + $insert_rev->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode); +} + +sub insert_mergelog +{ + my $self = shift; + my $key = shift; + my $value = shift; + + my $insert_mergelog = $self->{dbh}->prepare_cached("INSERT INTO commitmsgs (key, value) VALUES (?,?)",{},1); + $insert_mergelog->execute($key, $value); +} + +sub delete_head +{ + my $self = shift; + + my $delete_head = $self->{dbh}->prepare_cached("DELETE FROM head",{},1); + $delete_head->execute(); +} + +sub insert_head +{ + my $self = shift; + my $name = shift; + my $revision = shift; + my $filehash = shift; + my $commithash = shift; + my $modified = shift; + my $author = shift; + my $mode = shift; + + my $insert_head = $self->{dbh}->prepare_cached("INSERT INTO head (name, revision, filehash, commithash, modified, author, mode) VALUES (?,?,?,?,?,?,?)",{},1); + $insert_head->execute($name, $revision, $filehash, $commithash, $modified, $author, $mode); +} + +sub _headrev +{ + my $self = shift; + my $filename = shift; + + my $db_query = $self->{dbh}->prepare_cached("SELECT filehash, revision, mode FROM head WHERE name=?",{},1); + $db_query->execute($filename); + my ( $hash, $revision, $mode ) = $db_query->fetchrow_array; + + return ( $hash, $revision, $mode ); +} + +sub _get_prop +{ + my $self = shift; + my $key = shift; + + my $db_query = $self->{dbh}->prepare_cached("SELECT value FROM properties WHERE key=?",{},1); + $db_query->execute($key); + my ( $value ) = $db_query->fetchrow_array; + + return $value; +} + +sub _set_prop +{ + my $self = shift; + my $key = shift; + my $value = shift; + + my $db_query = $self->{dbh}->prepare_cached("UPDATE properties SET value=? WHERE key=?",{},1); + $db_query->execute($value, $key); + + unless ( $db_query->rows ) + { + $db_query = $self->{dbh}->prepare_cached("INSERT INTO properties (key, value) VALUES (?,?)",{},1); + $db_query->execute($key, $value); + } + + return $value; +} + +=head2 gethead + +=cut + +sub gethead +{ + my $self = shift; + + return $self->{gethead_cache} if ( defined ( $self->{gethead_cache} ) ); + + my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, mode, revision, modified, commithash, author FROM head ORDER BY name ASC",{},1); + $db_query->execute(); + + my $tree = []; + while ( my $file = $db_query->fetchrow_hashref ) + { + push @$tree, $file; + } + + $self->{gethead_cache} = $tree; + + return $tree; +} + +=head2 getlog + +=cut + +sub getlog +{ + my $self = shift; + my $filename = shift; + + my $db_query = $self->{dbh}->prepare_cached("SELECT name, filehash, author, mode, revision, modified, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1); + $db_query->execute($filename); + + my $tree = []; + while ( my $file = $db_query->fetchrow_hashref ) + { + push @$tree, $file; + } + + return $tree; +} + +=head2 getmeta + +This function takes a filename (with path) argument and returns a hashref of +metadata for that file. + +=cut + +sub getmeta +{ + my $self = shift; + my $filename = shift; + my $revision = shift; + + my $db_query; + if ( defined($revision) and $revision =~ /^\d+$/ ) + { + $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND revision=?",{},1); + $db_query->execute($filename, $revision); + } + elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ ) + { + $db_query = $self->{dbh}->prepare_cached("SELECT * FROM revision WHERE name=? AND commithash=?",{},1); + $db_query->execute($filename, $revision); + } else { + $db_query = $self->{dbh}->prepare_cached("SELECT * FROM head WHERE name=?",{},1); + $db_query->execute($filename); + } + + return $db_query->fetchrow_hashref; +} + +=head2 commitmessage + +this function takes a commithash and returns the commit message for that commit + +=cut +sub commitmessage +{ + my $self = shift; + my $commithash = shift; + + die("Need commithash") unless ( defined($commithash) and $commithash =~ /^[a-zA-Z0-9]{40}$/ ); + + my $db_query; + $db_query = $self->{dbh}->prepare_cached("SELECT value FROM commitmsgs WHERE key=?",{},1); + $db_query->execute($commithash); + + my ( $message ) = $db_query->fetchrow_array; + + if ( defined ( $message ) ) + { + $message .= " " if ( $message =~ /\n$/ ); + return $message; + } + + my @lines = safe_pipe_capture("git-cat-file", "commit", $commithash); + shift @lines while ( $lines[0] =~ /\S/ ); + $message = join("",@lines); + $message .= " " if ( $message =~ /\n$/ ); + return $message; +} + +=head2 gethistory + +This function takes a filename (with path) argument and returns an arrayofarrays +containing revision,filehash,commithash ordered by revision descending + +=cut +sub gethistory +{ + my $self = shift; + my $filename = shift; + + my $db_query; + $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? ORDER BY revision DESC",{},1); + $db_query->execute($filename); + + return $db_query->fetchall_arrayref; +} + +=head2 gethistorydense + +This function takes a filename (with path) argument and returns an arrayofarrays +containing revision,filehash,commithash ordered by revision descending. + +This version of gethistory skips deleted entries -- so it is useful for annotate. +The 'dense' part is a reference to a '--dense' option available for git-rev-list +and other git tools that depend on it. + +=cut +sub gethistorydense +{ + my $self = shift; + my $filename = shift; + + my $db_query; + $db_query = $self->{dbh}->prepare_cached("SELECT revision, filehash, commithash FROM revision WHERE name=? AND filehash!='deleted' ORDER BY revision DESC",{},1); + $db_query->execute($filename); + + return $db_query->fetchall_arrayref; +} + +=head2 in_array() + +from Array::PAT - mimics the in_array() function +found in PHP. Yuck but works for small arrays. + +=cut +sub in_array +{ + my ($check, @array) = @_; + my $retval = 0; + foreach my $test (@array){ + if($check eq $test){ + $retval = 1; + } + } + return $retval; +} + +=head2 safe_pipe_capture + +an alternative to `command` that allows input to be passed as an array +to work around shell problems with weird characters in arguments + +=cut +sub safe_pipe_capture { + + my @output; + + if (my $pid = open my $child, '-|') { + @output = (<$child>); + close $child or die join(' ',@_).": $! $?"; + } else { + exec(@_) or die "$! $?"; # exec() can fail the executable can't be found + } + return wantarray ? @output : join('',@output); +} + + +1; diff --git a/git-fetch.sh b/git-fetch.sh new file mode 100755 index 0000000000..357cac28b2 --- /dev/null +++ b/git-fetch.sh @@ -0,0 +1,497 @@ +#!/bin/sh +# + +USAGE='<fetch-options> <repository> <refspec>...' +SUBDIRECTORY_OK=Yes +. git-sh-setup +set_reflog_action "fetch $*" +cd_to_toplevel ;# probably unnecessary... + +. git-parse-remote +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" + +LF=' +' +IFS="$LF" + +no_tags= +tags= +append= +force= +verbose= +update_head_ok= +exec= +keep= +shallow_depth= +while case "$#" in 0) break ;; esac +do + case "$1" in + -a|--a|--ap|--app|--appe|--appen|--append) + append=t + ;; + --upl|--uplo|--uploa|--upload|--upload-|--upload-p|\ + --upload-pa|--upload-pac|--upload-pack) + shift + exec="--upload-pack=$1" + ;; + --upl=*|--uplo=*|--uploa=*|--upload=*|\ + --upload-=*|--upload-p=*|--upload-pa=*|--upload-pac=*|--upload-pack=*) + exec=--upload-pack=$(expr "z$1" : 'z-[^=]*=\(.*\)') + shift + ;; + -f|--f|--fo|--for|--forc|--force) + force=t + ;; + -t|--t|--ta|--tag|--tags) + tags=t + ;; + -n|--n|--no|--no-|--no-t|--no-ta|--no-tag|--no-tags) + no_tags=t + ;; + -u|--u|--up|--upd|--upda|--updat|--update|--update-|--update-h|\ + --update-he|--update-hea|--update-head|--update-head-|\ + --update-head-o|--update-head-ok) + update_head_ok=t + ;; + -v|--verbose) + verbose=Yes + ;; + -k|--k|--ke|--kee|--keep) + keep='-k -k' + ;; + --depth=*) + shallow_depth="--depth=`expr "z$1" : 'z-[^=]*=\(.*\)'`" + ;; + --depth) + shift + shallow_depth="--depth=$1" + ;; + -*) + usage + ;; + *) + break + ;; + esac + shift +done + +case "$#" in +0) + origin=$(get_default_remote) + test -n "$(get_remote_url ${origin})" || + die "Where do you want to fetch from today?" + set x $origin ; shift ;; +esac + +if test -z "$exec" +then + # No command line override and we have configuration for the remote. + exec="--upload-pack=$(get_uploadpack $1)" +fi + +remote_nick="$1" +remote=$(get_remote_url "$@") +refs= +rref= +rsync_slurped_objects= + +if test "" = "$append" +then + : >"$GIT_DIR/FETCH_HEAD" +fi + +# Global that is reused later +ls_remote_result=$(git ls-remote $exec "$remote") || + die "Cannot get the repository state from $remote" + +append_fetch_head () { + head_="$1" + remote_="$2" + remote_name_="$3" + remote_nick_="$4" + local_name_="$5" + case "$6" in + t) not_for_merge_='not-for-merge' ;; + '') not_for_merge_= ;; + esac + + # remote-nick is the URL given on the command line (or a shorthand) + # remote-name is the $GIT_DIR relative refs/ path we computed + # for this refspec. + + # the $note_ variable will be fed to git-fmt-merge-msg for further + # processing. + case "$remote_name_" in + HEAD) + note_= ;; + refs/heads/*) + note_="$(expr "$remote_name_" : 'refs/heads/\(.*\)')" + note_="branch '$note_' of " ;; + refs/tags/*) + note_="$(expr "$remote_name_" : 'refs/tags/\(.*\)')" + note_="tag '$note_' of " ;; + refs/remotes/*) + note_="$(expr "$remote_name_" : 'refs/remotes/\(.*\)')" + note_="remote branch '$note_' of " ;; + *) + note_="$remote_name of " ;; + esac + remote_1_=$(expr "z$remote_" : 'z\(.*\)\.git/*$') && + remote_="$remote_1_" + note_="$note_$remote_" + + # 2.6.11-tree tag would not be happy to be fed to resolve. + if git-cat-file commit "$head_" >/dev/null 2>&1 + then + headc_=$(git-rev-parse --verify "$head_^0") || exit + echo "$headc_ $not_for_merge_ $note_" >>"$GIT_DIR/FETCH_HEAD" + else + echo "$head_ not-for-merge $note_" >>"$GIT_DIR/FETCH_HEAD" + fi + + update_local_ref "$local_name_" "$head_" "$note_" +} + +update_local_ref () { + # If we are storing the head locally make sure that it is + # a fast forward (aka "reverse push"). + + label_=$(git-cat-file -t $2) + newshort_=$(git-rev-parse --short $2) + if test -z "$1" ; then + [ "$verbose" ] && echo >&2 "* fetched $3" + [ "$verbose" ] && echo >&2 " $label_: $newshort_" + return 0 + fi + oldshort_=$(git show-ref --hash --abbrev "$1" 2>/dev/null) + + case "$1" in + refs/tags/*) + # Tags need not be pointing at commits so there + # is no way to guarantee "fast-forward" anyway. + if test -n "$oldshort_" + then + if now_=$(git show-ref --hash "$1") && test "$now_" = "$2" + then + [ "$verbose" ] && echo >&2 "* $1: same as $3" + [ "$verbose" ] && echo >&2 " $label_: $newshort_" ||: + else + echo >&2 "* $1: updating with $3" + echo >&2 " $label_: $newshort_" + git-update-ref -m "$GIT_REFLOG_ACTION: updating tag" "$1" "$2" + fi + else + echo >&2 "* $1: storing $3" + echo >&2 " $label_: $newshort_" + git-update-ref -m "$GIT_REFLOG_ACTION: storing tag" "$1" "$2" + fi + ;; + + refs/heads/* | refs/remotes/*) + # $1 is the ref being updated. + # $2 is the new value for the ref. + local=$(git-rev-parse --verify "$1^0" 2>/dev/null) + if test "$local" + then + # Require fast-forward. + mb=$(git-merge-base "$local" "$2") && + case "$2,$mb" in + $local,*) + if test -n "$verbose" + then + echo >&2 "* $1: same as $3" + echo >&2 " $label_: $newshort_" + fi + ;; + *,$local) + echo >&2 "* $1: fast forward to $3" + echo >&2 " old..new: $oldshort_..$newshort_" + git-update-ref -m "$GIT_REFLOG_ACTION: fast-forward" "$1" "$2" "$local" + ;; + *) + false + ;; + esac || { + case ",$force,$single_force," in + *,t,*) + echo >&2 "* $1: forcing update to non-fast forward $3" + echo >&2 " old...new: $oldshort_...$newshort_" + git-update-ref -m "$GIT_REFLOG_ACTION: forced-update" "$1" "$2" "$local" + ;; + *) + echo >&2 "* $1: not updating to non-fast forward $3" + echo >&2 " old...new: $oldshort_...$newshort_" + exit 1 + ;; + esac + } + else + echo >&2 "* $1: storing $3" + echo >&2 " $label_: $newshort_" + git-update-ref -m "$GIT_REFLOG_ACTION: storing head" "$1" "$2" + fi + ;; + esac +} + +# updating the current HEAD with git-fetch in a bare +# repository is always fine. +if test -z "$update_head_ok" && test $(is_bare_repository) = false +then + orig_head=$(git-rev-parse --verify HEAD 2>/dev/null) +fi + +# If --tags (and later --heads or --all) is specified, then we are +# not talking about defaults stored in Pull: line of remotes or +# branches file, and just fetch those and refspecs explicitly given. +# Otherwise we do what we always did. + +reflist=$(get_remote_refs_for_fetch "$@") +if test "$tags" +then + taglist=`IFS=' ' && + echo "$ls_remote_result" | + while read sha1 name + do + case "$sha1" in + fail) + exit 1 + esac + case "$name" in + *^*) continue ;; + refs/tags/*) ;; + *) continue ;; + esac + if git-check-ref-format "$name" + then + echo ".${name}:${name}" + else + echo >&2 "warning: tag ${name} ignored" + fi + done` || exit + if test "$#" -gt 1 + then + # remote URL plus explicit refspecs; we need to merge them. + reflist="$reflist$LF$taglist" + else + # No explicit refspecs; fetch tags only. + reflist=$taglist + fi +fi + +fetch_main () { + reflist="$1" + refs= + rref= + + for ref in $reflist + do + refs="$refs$LF$ref" + + # These are relative path from $GIT_DIR, typically starting at refs/ + # but may be HEAD + if expr "z$ref" : 'z\.' >/dev/null + then + not_for_merge=t + ref=$(expr "z$ref" : 'z\.\(.*\)') + else + not_for_merge= + fi + if expr "z$ref" : 'z+' >/dev/null + then + single_force=t + ref=$(expr "z$ref" : 'z+\(.*\)') + else + single_force= + fi + remote_name=$(expr "z$ref" : 'z\([^:]*\):') + local_name=$(expr "z$ref" : 'z[^:]*:\(.*\)') + + rref="$rref$LF$remote_name" + + # There are transports that can fetch only one head at a time... + case "$remote" in + http://* | https://* | ftp://*) + test -n "$shallow_depth" && + die "shallow clone with http not supported" + proto=`expr "$remote" : '\([^:]*\):'` + if [ -n "$GIT_SSL_NO_VERIFY" ]; then + curl_extra_args="-k" + fi + if [ -n "$GIT_CURL_FTP_NO_EPSV" -o \ + "`git-config --bool http.noEPSV`" = true ]; then + noepsv_opt="--disable-epsv" + fi + + # Find $remote_name from ls-remote output. + head=$( + IFS=' ' + echo "$ls_remote_result" | + while read sha1 name + do + test "z$name" = "z$remote_name" || continue + echo "$sha1" + break + done + ) + expr "z$head" : "z$_x40\$" >/dev/null || + die "No such ref $remote_name at $remote" + echo >&2 "Fetching $remote_name from $remote using $proto" + git-http-fetch -v -a "$head" "$remote/" || exit + ;; + rsync://*) + test -n "$shallow_depth" && + die "shallow clone with rsync not supported" + TMP_HEAD="$GIT_DIR/TMP_HEAD" + rsync -L -q "$remote/$remote_name" "$TMP_HEAD" || exit 1 + head=$(git-rev-parse --verify TMP_HEAD) + rm -f "$TMP_HEAD" + test "$rsync_slurped_objects" || { + rsync -av --ignore-existing --exclude info \ + "$remote/objects/" "$GIT_OBJECT_DIRECTORY/" || exit + + # Look at objects/info/alternates for rsync -- http will + # support it natively and git native ones will do it on + # the remote end. Not having that file is not a crime. + rsync -q "$remote/objects/info/alternates" \ + "$GIT_DIR/TMP_ALT" 2>/dev/null || + rm -f "$GIT_DIR/TMP_ALT" + if test -f "$GIT_DIR/TMP_ALT" + then + resolve_alternates "$remote" <"$GIT_DIR/TMP_ALT" | + while read alt + do + case "$alt" in 'bad alternate: '*) die "$alt";; esac + echo >&2 "Getting alternate: $alt" + rsync -av --ignore-existing --exclude info \ + "$alt" "$GIT_OBJECT_DIRECTORY/" || exit + done + rm -f "$GIT_DIR/TMP_ALT" + fi + rsync_slurped_objects=t + } + ;; + *) + # We will do git native transport with just one call later. + continue ;; + esac + + append_fetch_head "$head" "$remote" \ + "$remote_name" "$remote_nick" "$local_name" "$not_for_merge" || exit + + done + + case "$remote" in + http://* | https://* | ftp://* | rsync://* ) + ;; # we are already done. + *) + ( : subshell because we muck with IFS + IFS=" $LF" + ( + git-fetch-pack --thin $exec $keep $shallow_depth "$remote" $rref || + echo failed "$remote" + ) | + ( + trap ' + if test -n "$keepfile" && test -f "$keepfile" + then + rm -f "$keepfile" + fi + ' 0 + + keepfile= + while read sha1 remote_name + do + case "$sha1" in + failed) + echo >&2 "Fetch failure: $remote" + exit 1 ;; + # special line coming from index-pack with the pack name + pack) + continue ;; + keep) + keepfile="$GIT_OBJECT_DIRECTORY/pack/pack-$remote_name.keep" + continue ;; + esac + found= + single_force= + for ref in $refs + do + case "$ref" in + +$remote_name:*) + single_force=t + not_for_merge= + found="$ref" + break ;; + .+$remote_name:*) + single_force=t + not_for_merge=t + found="$ref" + break ;; + .$remote_name:*) + not_for_merge=t + found="$ref" + break ;; + $remote_name:*) + not_for_merge= + found="$ref" + break ;; + esac + done + local_name=$(expr "z$found" : 'z[^:]*:\(.*\)') + append_fetch_head "$sha1" "$remote" \ + "$remote_name" "$remote_nick" "$local_name" \ + "$not_for_merge" || exit + done + ) + ) || exit ;; + esac + +} + +fetch_main "$reflist" || exit + +# automated tag following +case "$no_tags$tags" in +'') + case "$reflist" in + *:refs/*) + # effective only when we are following remote branch + # using local tracking branch. + taglist=$(IFS=' ' && + echo "$ls_remote_result" | + git-show-ref --exclude-existing=refs/tags/ | + while read sha1 name + do + git-cat-file -t "$sha1" >/dev/null 2>&1 || continue + echo >&2 "Auto-following $name" + echo ".${name}:${name}" + done) + esac + case "$taglist" in + '') ;; + ?*) + # do not deepen a shallow tree when following tags + shallow_depth= + fetch_main "$taglist" || exit ;; + esac +esac + +# If the original head was empty (i.e. no "master" yet), or +# if we were told not to worry, we do not have to check. +case "$orig_head" in +'') + ;; +?*) + curr_head=$(git-rev-parse --verify HEAD 2>/dev/null) + if test "$curr_head" != "$orig_head" + then + git-update-ref \ + -m "$GIT_REFLOG_ACTION: Undoing incorrectly fetched HEAD." \ + HEAD "$orig_head" + die "Cannot fetch into the current branch." + fi + ;; +esac diff --git a/git-gc.sh b/git-gc.sh new file mode 100755 index 0000000000..3e8c87c814 --- /dev/null +++ b/git-gc.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# +# Copyright (c) 2006, Shawn O. Pearce +# +# Cleanup unreachable files and optimize the repository. + +USAGE='git-gc [--prune]' +SUBDIRECTORY_OK=Yes +. git-sh-setup + +no_prune=: +while case $# in 0) break ;; esac +do + case "$1" in + --prune) + no_prune= + ;; + --) + usage + ;; + esac + shift +done + +git-pack-refs --prune && +git-reflog expire --all && +git-repack -a -d -l && +$no_prune git-prune && +git-rerere gc || exit diff --git a/git-gui/.gitignore b/git-gui/.gitignore new file mode 100644 index 0000000000..c714d382e8 --- /dev/null +++ b/git-gui/.gitignore @@ -0,0 +1,3 @@ +GIT-VERSION-FILE +git-citool +git-gui diff --git a/git-gui/GIT-VERSION-GEN b/git-gui/GIT-VERSION-GEN new file mode 100755 index 0000000000..79f1c527ff --- /dev/null +++ b/git-gui/GIT-VERSION-GEN @@ -0,0 +1,46 @@ +#!/bin/sh + +GVF=GIT-VERSION-FILE +DEF_VER=v0.5.GIT + +LF=' +' + +# First try git-describe, then see if there is a version file +# (included in release tarballs), then default +if VN=$(git describe --abbrev=4 HEAD 2>/dev/null) && + case "$VN" in + *$LF*) (exit 1) ;; + v[0-9]*) : happy ;; + esac +then + VN=$(echo "$VN" | sed -e 's/-/./g'); +elif test -f version +then + VN=$(cat version) || VN="$DEF_VER" +else + VN="$DEF_VER" +fi + +VN=$(expr "$VN" : v*'\(.*\)') + +dirty=$(sh -c 'git diff-index --name-only HEAD' 2>/dev/null) || dirty= +case "$dirty" in +'') + ;; +*) + VN="$VN-dirty" ;; +esac + +if test -r $GVF +then + VC=$(sed -e 's/^GIT_VERSION = //' <$GVF) +else + VC=unset +fi +test "$VN" = "$VC" || { + echo >&2 "GIT_VERSION = $VN" + echo "GIT_VERSION = $VN" >$GVF +} + + diff --git a/git-gui/Makefile b/git-gui/Makefile new file mode 100644 index 0000000000..8fade69127 --- /dev/null +++ b/git-gui/Makefile @@ -0,0 +1,48 @@ +all:: + +GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE + @$(SHELL_PATH) ./GIT-VERSION-GEN +-include GIT-VERSION-FILE + +SCRIPT_SH = git-gui.sh +GITGUI_BUILT_INS = git-citool +ALL_PROGRAMS = $(GITGUI_BUILT_INS) $(patsubst %.sh,%,$(SCRIPT_SH)) + +ifndef SHELL_PATH + SHELL_PATH = /bin/sh +endif + +gitexecdir := $(shell git --exec-path) +INSTALL = install + +DESTDIR_SQ = $(subst ','\'',$(DESTDIR)) +gitexecdir_SQ = $(subst ','\'',$(gitexecdir)) + +SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) + +$(patsubst %.sh,%,$(SCRIPT_SH)) : % : %.sh + rm -f $@ $@+ + sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ + -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \ + $@.sh >$@+ + chmod +x $@+ + mv $@+ $@ + +$(GITGUI_BUILT_INS): git-gui + rm -f $@ && ln git-gui $@ + +# These can record GIT_VERSION +$(patsubst %.sh,%,$(SCRIPT_SH)): GIT-VERSION-FILE + +all:: $(ALL_PROGRAMS) + +install: all + $(INSTALL) -d -m755 '$(DESTDIR_SQ)$(gitexecdir_SQ)' + $(INSTALL) git-gui '$(DESTDIR_SQ)$(gitexecdir_SQ)' + $(foreach p,$(GITGUI_BUILT_INS), rm -f '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' && ln '$(DESTDIR_SQ)$(gitexecdir_SQ)/git-gui' '$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' ;) + +clean:: + rm -f $(ALL_PROGRAMS) GIT-VERSION-FILE + +.PHONY: all install clean +.PHONY: .FORCE-GIT-VERSION-FILE diff --git a/git-gui.sh b/git-gui/git-gui.sh index ea16be43ac..ea16be43ac 100755 --- a/git-gui.sh +++ b/git-gui/git-gui.sh diff --git a/git-instaweb.sh b/git-instaweb.sh new file mode 100755 index 0000000000..cbc7418e35 --- /dev/null +++ b/git-instaweb.sh @@ -0,0 +1,255 @@ +#!/bin/sh +# +# Copyright (c) 2006 Eric Wong +# +USAGE='[--start] [--stop] [--restart] + [--local] [--httpd=<httpd>] [--port=<port>] [--browser=<browser>] + [--module-path=<path> (for Apache2 only)]' + +. git-sh-setup + +case "$GIT_DIR" in +/*) + fqgitdir="$GIT_DIR" ;; +*) + fqgitdir="$PWD/$GIT_DIR" ;; +esac + +local="`git config --bool --get instaweb.local`" +httpd="`git config --get instaweb.httpd`" +browser="`git config --get instaweb.browser`" +port=`git config --get instaweb.port` +module_path="`git config --get instaweb.modulepath`" + +conf=$GIT_DIR/gitweb/httpd.conf + +# Defaults: + +# if installed, it doesn't need further configuration (module_path) +test -z "$httpd" && httpd='lighttpd -f' + +# probably the most popular browser among gitweb users +test -z "$browser" && browser='firefox' + +# any untaken local port will do... +test -z "$port" && port=1234 + +start_httpd () { + httpd_only="`echo $httpd | cut -f1 -d' '`" + if test "`expr index $httpd_only /`" -eq '1' || \ + which $httpd_only >/dev/null + then + $httpd $fqgitdir/gitweb/httpd.conf + else + # many httpds are installed in /usr/sbin or /usr/local/sbin + # these days and those are not in most users $PATHs + for i in /usr/local/sbin /usr/sbin + do + if test -x "$i/$httpd_only" + then + # don't quote $httpd, there can be + # arguments to it (-f) + $i/$httpd "$fqgitdir/gitweb/httpd.conf" + return + fi + done + echo "$httpd_only not found. Install $httpd_only or use" \ + "--httpd to specify another http daemon." + exit 1 + fi + if test $? != 0; then + echo "Could not execute http daemon $httpd." + exit 1 + fi +} + +stop_httpd () { + test -f "$fqgitdir/pid" && kill `cat "$fqgitdir/pid"` +} + +while case "$#" in 0) break ;; esac +do + case "$1" in + --stop|stop) + stop_httpd + exit 0 + ;; + --start|start) + start_httpd + exit 0 + ;; + --restart|restart) + stop_httpd + start_httpd + exit 0 + ;; + --local|-l) + local=true + ;; + -d|--httpd|--httpd=*) + case "$#,$1" in + *,*=*) + httpd=`expr "$1" : '-[^=]*=\(.*\)'` ;; + 1,*) + usage ;; + *) + httpd="$2" + shift ;; + esac + ;; + -b|--browser|--browser=*) + case "$#,$1" in + *,*=*) + browser=`expr "$1" : '-[^=]*=\(.*\)'` ;; + 1,*) + usage ;; + *) + browser="$2" + shift ;; + esac + ;; + -p|--port|--port=*) + case "$#,$1" in + *,*=*) + port=`expr "$1" : '-[^=]*=\(.*\)'` ;; + 1,*) + usage ;; + *) + port="$2" + shift ;; + esac + ;; + -m|--module-path=*|--module-path) + case "$#,$1" in + *,*=*) + module_path=`expr "$1" : '-[^=]*=\(.*\)'` ;; + 1,*) + usage ;; + *) + module_path="$2" + shift ;; + esac + ;; + *) + usage + ;; + esac + shift +done + +mkdir -p "$GIT_DIR/gitweb/tmp" +GIT_EXEC_PATH="`git --exec-path`" +GIT_DIR="$fqgitdir" +export GIT_EXEC_PATH GIT_DIR + + +lighttpd_conf () { + cat > "$conf" <<EOF +server.document-root = "$fqgitdir/gitweb" +server.port = $port +server.modules = ( "mod_cgi" ) +server.indexfiles = ( "gitweb.cgi" ) +server.pid-file = "$fqgitdir/pid" +cgi.assign = ( ".cgi" => "" ) +mimetype.assign = ( ".css" => "text/css" ) +EOF + test "$local" = true && echo 'server.bind = "127.0.0.1"' >> "$conf" +} + +apache2_conf () { + test -z "$module_path" && module_path=/usr/lib/apache2/modules + mkdir -p "$GIT_DIR/gitweb/logs" + bind= + test "$local" = true && bind='127.0.0.1:' + echo 'text/css css' > $fqgitdir/mime.types + cat > "$conf" <<EOF +ServerName "git-instaweb" +ServerRoot "$fqgitdir/gitweb" +DocumentRoot "$fqgitdir/gitweb" +PidFile "$fqgitdir/pid" +Listen $bind$port +EOF + + for mod in mime dir; do + if test -e $module_path/mod_${mod}.so; then + echo "LoadModule ${mod}_module " \ + "$module_path/mod_${mod}.so" >> "$conf" + fi + done + cat >> "$conf" <<EOF +TypesConfig $fqgitdir/mime.types +DirectoryIndex gitweb.cgi +EOF + + # check to see if Dennis Stosberg's mod_perl compatibility patch + # (<20060621130708.Gcbc6e5c@leonov.stosberg.net>) has been applied + if test -f "$module_path/mod_perl.so" && grep '^our $gitbin' \ + "$GIT_DIR/gitweb/gitweb.cgi" >/dev/null + then + # favor mod_perl if available + cat >> "$conf" <<EOF +LoadModule perl_module $module_path/mod_perl.so +PerlPassEnv GIT_DIR +PerlPassEnv GIT_EXEC_DIR +<Location /gitweb.cgi> + SetHandler perl-script + PerlResponseHandler ModPerl::Registry + PerlOptions +ParseHeaders + Options +ExecCGI +</Location> +EOF + else + # plain-old CGI + list_mods=`echo "$httpd" | sed "s/-f$/-l/"` + $list_mods | grep 'mod_cgi\.c' >/dev/null 2>&1 || \ + echo "LoadModule cgi_module $module_path/mod_cgi.so" >> "$conf" + cat >> "$conf" <<EOF +AddHandler cgi-script .cgi +<Location /gitweb.cgi> + Options +ExecCGI +</Location> +EOF + fi +} + +script=' +s#^\(my\|our\) $projectroot =.*#\1 $projectroot = "'`dirname $fqgitdir`'";# +s#\(my\|our\) $gitbin =.*#\1 $gitbin = "'$GIT_EXEC_PATH'";# +s#\(my\|our\) $projects_list =.*#\1 $projects_list = $projectroot;# +s#\(my\|our\) $git_temp =.*#\1 $git_temp = "'$fqgitdir/gitweb/tmp'";#' + +gitweb_cgi () { + cat > "$1.tmp" <<\EOFGITWEB +@@GITWEB_CGI@@ +EOFGITWEB + sed "$script" "$1.tmp" > "$1" + chmod +x "$1" + rm -f "$1.tmp" +} + +gitweb_css () { + cat > "$1" <<\EOFGITWEB +@@GITWEB_CSS@@ +EOFGITWEB +} + +gitweb_cgi $GIT_DIR/gitweb/gitweb.cgi +gitweb_css $GIT_DIR/gitweb/gitweb.css + +case "$httpd" in +*lighttpd*) + lighttpd_conf + ;; +*apache2*) + apache2_conf + ;; +*) + echo "Unknown httpd specified: $httpd" + exit 1 + ;; +esac + +start_httpd +test -z "$browser" && browser=echo +url=http://127.0.0.1:$port +$browser $url || echo $url diff --git a/git-lost-found.sh b/git-lost-found.sh new file mode 100755 index 0000000000..9360804711 --- /dev/null +++ b/git-lost-found.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +USAGE='' +SUBDIRECTORY_OK='Yes' +. git-sh-setup + +if [ "$#" != "0" ] +then + usage +fi + +laf="$GIT_DIR/lost-found" +rm -fr "$laf" && mkdir -p "$laf/commit" "$laf/other" || exit + +git fsck --full | +while read dangling type sha1 +do + case "$dangling" in + dangling) + if git-rev-parse --verify "$sha1^0" >/dev/null 2>/dev/null + then + dir="$laf/commit" + git-show-branch "$sha1" + else + dir="$laf/other" + fi + echo "$sha1" >"$dir/$sha1" + ;; + esac +done diff --git a/git-ls-remote.sh b/git-ls-remote.sh new file mode 100755 index 0000000000..8ea5c5e816 --- /dev/null +++ b/git-ls-remote.sh @@ -0,0 +1,136 @@ +#!/bin/sh +# + +usage () { + echo >&2 "usage: $0 [--heads] [--tags] [-u|--upload-pack <upload-pack>]" + echo >&2 " <repository> <refs>..." + exit 1; +} + +die () { + echo >&2 "$*" + exit 1 +} + +exec= +while case "$#" in 0) break;; esac +do + case "$1" in + -h|--h|--he|--hea|--head|--heads) + heads=heads; shift ;; + -t|--t|--ta|--tag|--tags) + tags=tags; shift ;; + -u|--u|--up|--upl|--uploa|--upload|--upload-|--upload-p|--upload-pa|\ + --upload-pac|--upload-pack) + shift + exec="--upload-pack=$1" + shift;; + -u=*|--u=*|--up=*|--upl=*|--uplo=*|--uploa=*|--upload=*|\ + --upload-=*|--upload-p=*|--upload-pa=*|--upload-pac=*|--upload-pack=*) + exec=--upload-pack=$(expr "z$1" : 'z-[^=]*=\(.*\)') + shift;; + --) + shift; break ;; + -*) + usage ;; + *) + break ;; + esac +done + +case "$#" in 0) usage ;; esac + +case ",$heads,$tags," in +,,,) heads=heads tags=tags other=other ;; +esac + +. git-parse-remote +peek_repo="$(get_remote_url "$@")" +shift + +tmp=.ls-remote-$$ +trap "rm -fr $tmp-*" 0 1 2 3 15 +tmpdir=$tmp-d + +case "$peek_repo" in +http://* | https://* | ftp://* ) + if [ -n "$GIT_SSL_NO_VERIFY" ]; then + curl_extra_args="-k" + fi + if [ -n "$GIT_CURL_FTP_NO_EPSV" -o \ + "`git-config --bool http.noEPSV`" = true ]; then + curl_extra_args="${curl_extra_args} --disable-epsv" + fi + curl -nsf $curl_extra_args --header "Pragma: no-cache" "$peek_repo/info/refs" || + echo "failed slurping" + ;; + +rsync://* ) + mkdir $tmpdir && + rsync -rlq "$peek_repo/HEAD" $tmpdir && + rsync -rq "$peek_repo/refs" $tmpdir || { + echo "failed slurping" + exit + } + head=$(cat "$tmpdir/HEAD") && + case "$head" in + ref:' '*) + head=$(expr "z$head" : 'zref: \(.*\)') && + head=$(cat "$tmpdir/$head") || exit + esac && + echo "$head HEAD" + (cd $tmpdir && find refs -type f) | + while read path + do + cat "$tmpdir/$path" | tr -d '\012' + echo " $path" + done && + rm -fr $tmpdir + ;; + +* ) + git-peek-remote $exec "$peek_repo" || + echo "failed slurping" + ;; +esac | +sort -t ' ' -k 2 | +while read sha1 path +do + case "$sha1" in + failed) + exit 1 ;; + esac + case "$path" in + refs/heads/*) + group=heads ;; + refs/tags/*) + group=tags ;; + *) + group=other ;; + esac + case ",$heads,$tags,$other," in + *,$group,*) + ;; + *) + continue;; + esac + case "$#" in + 0) + match=yes ;; + *) + match=no + for pat + do + case "/$path" in + */$pat ) + match=yes + break ;; + esac + done + esac + case "$match" in + no) + continue ;; + esac + echo "$sha1 $path" +done diff --git a/git-merge-octopus.sh b/git-merge-octopus.sh new file mode 100755 index 0000000000..eb3f473d5a --- /dev/null +++ b/git-merge-octopus.sh @@ -0,0 +1,114 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# +# Resolve two or more trees. +# + +LF=' +' + +die () { + echo >&2 "$*" + exit 1 +} + +# The first parameters up to -- are merge bases; the rest are heads. +bases= head= remotes= sep_seen= +for arg +do + case ",$sep_seen,$head,$arg," in + *,--,) + sep_seen=yes + ;; + ,yes,,*) + head=$arg + ;; + ,yes,*) + remotes="$remotes$arg " + ;; + *) + bases="$bases$arg " + ;; + esac +done + +# Reject if this is not an Octopus -- resolve should be used instead. +case "$remotes" in +?*' '?*) + ;; +*) + exit 2 ;; +esac + +# MRC is the current "merge reference commit" +# MRT is the current "merge result tree" + +MRC=$head MSG= PARENT="-p $head" +MRT=$(git-write-tree) +CNT=1 ;# counting our head +NON_FF_MERGE=0 +OCTOPUS_FAILURE=0 +for SHA1 in $remotes +do + case "$OCTOPUS_FAILURE" in + 1) + # We allow only last one to have a hand-resolvable + # conflicts. Last round failed and we still had + # a head to merge. + echo "Automated merge did not work." + echo "Should not be doing an Octopus." + exit 2 + esac + + common=$(git-merge-base --all $MRC $SHA1) || + die "Unable to find common commit with $SHA1" + + case "$LF$common$LF" in + *"$LF$SHA1$LF"*) + echo "Already up-to-date with $SHA1" + continue + ;; + esac + + CNT=`expr $CNT + 1` + PARENT="$PARENT -p $SHA1" + + if test "$common,$NON_FF_MERGE" = "$MRC,0" + then + # The first head being merged was a fast-forward. + # Advance MRC to the head being merged, and use that + # tree as the intermediate result of the merge. + # We still need to count this as part of the parent set. + + echo "Fast forwarding to: $SHA1" + git-read-tree -u -m $head $SHA1 || exit + MRC=$SHA1 MRT=$(git-write-tree) + continue + fi + + NON_FF_MERGE=1 + + echo "Trying simple merge with $SHA1" + git-read-tree -u -m --aggressive $common $MRT $SHA1 || exit 2 + next=$(git-write-tree 2>/dev/null) + if test $? -ne 0 + then + echo "Simple merge did not work, trying automatic merge." + git-merge-index -o git-merge-one-file -a || + OCTOPUS_FAILURE=1 + next=$(git-write-tree 2>/dev/null) + fi + + # We have merged the other branch successfully. Ideally + # we could implement OR'ed heads in merge-base, and keep + # a list of commits we have merged so far in MRC to feed + # them to merge-base, but we approximate it by keep using + # the current MRC. We used to update it to $common, which + # was incorrectly doing AND'ed merge-base here, which was + # unneeded. + + MRT=$next +done + +exit "$OCTOPUS_FAILURE" diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh new file mode 100755 index 0000000000..7d62d7902c --- /dev/null +++ b/git-merge-one-file.sh @@ -0,0 +1,134 @@ +#!/bin/sh +# +# Copyright (c) Linus Torvalds, 2005 +# +# This is the git per-file merge script, called with +# +# $1 - original file SHA1 (or empty) +# $2 - file in branch1 SHA1 (or empty) +# $3 - file in branch2 SHA1 (or empty) +# $4 - pathname in repository +# $5 - original file mode (or empty) +# $6 - file in branch1 mode (or empty) +# $7 - file in branch2 mode (or empty) +# +# Handle some trivial cases.. The _really_ trivial cases have +# been handled already by git-read-tree, but that one doesn't +# do any merges that might change the tree layout. + +case "${1:-.}${2:-.}${3:-.}" in +# +# Deleted in both or deleted in one and unchanged in the other +# +"$1.." | "$1.$1" | "$1$1.") + if [ "$2" ]; then + echo "Removing $4" + else + # read-tree checked that index matches HEAD already, + # so we know we do not have this path tracked. + # there may be an unrelated working tree file here, + # which we should just leave unmolested. + exit 0 + fi + if test -f "$4"; then + rm -f -- "$4" && + rmdir -p "$(expr "z$4" : 'z\(.*\)/')" 2>/dev/null || : + fi && + exec git-update-index --remove -- "$4" + ;; + +# +# Added in one. +# +".$2.") + # the other side did not add and we added so there is nothing + # to be done. + ;; +"..$3") + echo "Adding $4" + test -f "$4" || { + echo "ERROR: untracked $4 is overwritten by the merge." + exit 1 + } + git-update-index --add --cacheinfo "$6$7" "$2$3" "$4" && + exec git-checkout-index -u -f -- "$4" + ;; + +# +# Added in both, identically (check for same permissions). +# +".$3$2") + if [ "$6" != "$7" ]; then + echo "ERROR: File $4 added identically in both branches," + echo "ERROR: but permissions conflict $6->$7." + exit 1 + fi + echo "Adding $4" + git-update-index --add --cacheinfo "$6" "$2" "$4" && + exec git-checkout-index -u -f -- "$4" + ;; + +# +# Modified in both, but differently. +# +"$1$2$3" | ".$2$3") + + case ",$6,$7," in + *,120000,*) + echo "ERROR: $4: Not merging symbolic link changes." + exit 1 + ;; + esac + + src2=`git-unpack-file $3` + case "$1" in + '') + echo "Added $4 in both, but differently." + # This extracts OUR file in $orig, and uses git-apply to + # remove lines that are unique to ours. + orig=`git-unpack-file $2` + sz0=`wc -c <"$orig"` + diff -u -La/$orig -Lb/$orig $orig $src2 | git-apply --no-add + sz1=`wc -c <"$orig"` + + # If we do not have enough common material, it is not + # worth trying two-file merge using common subsections. + expr "$sz0" \< "$sz1" \* 2 >/dev/null || : >$orig + ;; + *) + echo "Auto-merging $4" + orig=`git-unpack-file $1` + ;; + esac + + # Be careful for funny filename such as "-L" in "$4", which + # would confuse "merge" greatly. + src1=`git-unpack-file $2` + git-merge-file "$src1" "$orig" "$src2" + ret=$? + + # Create the working tree file, using "our tree" version from the + # index, and then store the result of the merge. + git-checkout-index -f --stage=2 -- "$4" && cat "$src1" >"$4" + rm -f -- "$orig" "$src1" "$src2" + + if [ "$6" != "$7" ]; then + echo "ERROR: Permissions conflict: $5->$6,$7." + ret=1 + fi + if [ "$1" = '' ]; then + ret=1 + fi + + if [ $ret -ne 0 ]; then + echo "ERROR: Merge conflict in $4" + exit 1 + fi + exec git-update-index -- "$4" + ;; + +*) + echo "ERROR: $4: Not handling case $1 -> $2 -> $3" + ;; +esac +exit 1 diff --git a/git-merge-ours.sh b/git-merge-ours.sh new file mode 100755 index 0000000000..4f3d053889 --- /dev/null +++ b/git-merge-ours.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# +# Pretend we resolved the heads, but declare our tree trumps everybody else. +# + +# We need to exit with 2 if the index does not match our HEAD tree, +# because the current index is what we will be committing as the +# merge result. + +test "$(git-diff-index --cached --name-status HEAD)" = "" || exit 2 + +exit 0 diff --git a/git-merge-resolve.sh b/git-merge-resolve.sh new file mode 100755 index 0000000000..75e1de49ac --- /dev/null +++ b/git-merge-resolve.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# Copyright (c) 2005 Junio C Hamano +# +# Resolve two trees, using enhanced multi-base read-tree. + +# The first parameters up to -- are merge bases; the rest are heads. +bases= head= remotes= sep_seen= +for arg +do + case ",$sep_seen,$head,$arg," in + *,--,) + sep_seen=yes + ;; + ,yes,,*) + head=$arg + ;; + ,yes,*) + remotes="$remotes$arg " + ;; + *) + bases="$bases$arg " + ;; + esac +done + +# Give up if we are given more than two remotes -- not handling octopus. +case "$remotes" in +?*' '?*) + exit 2 ;; +esac + +# Give up if this is a baseless merge. +if test '' = "$bases" +then + exit 2 +fi + +git-update-index --refresh 2>/dev/null +git-read-tree -u -m --aggressive $bases $head $remotes || exit 2 +echo "Trying simple merge." +if result_tree=$(git-write-tree 2>/dev/null) +then + exit 0 +else + echo "Simple merge failed, trying Automatic merge." + if git-merge-index -o git-merge-one-file -a + then + exit 0 + else + exit 1 + fi +fi diff --git a/git-merge-stupid.sh b/git-merge-stupid.sh new file mode 100755 index 0000000000..4faecb933d --- /dev/null +++ b/git-merge-stupid.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# +# Resolve two trees, 'stupid merge'. + +# The first parameters up to -- are merge bases; the rest are heads. +bases= head= remotes= sep_seen= +for arg +do + case ",$sep_seen,$head,$arg," in + *,--,) + sep_seen=yes + ;; + ,yes,,*) + head=$arg + ;; + ,yes,*) + remotes="$remotes$arg " + ;; + *) + bases="$bases$arg " + ;; + esac +done + +# Give up if we are given more than two remotes -- not handling octopus. +case "$remotes" in +?*' '?*) + exit 2 ;; +esac + +# Find an optimum merge base if there are more than one candidates. +case "$bases" in +?*' '?*) + echo "Trying to find the optimum merge base." + G=.tmp-index$$ + best= + best_cnt=-1 + for c in $bases + do + rm -f $G + GIT_INDEX_FILE=$G git-read-tree -m $c $head $remotes \ + 2>/dev/null || continue + # Count the paths that are unmerged. + cnt=`GIT_INDEX_FILE=$G git-ls-files --unmerged | wc -l` + if test $best_cnt -le 0 -o $cnt -le $best_cnt + then + best=$c + best_cnt=$cnt + if test "$best_cnt" -eq 0 + then + # Cannot do any better than all trivial merge. + break + fi + fi + done + rm -f $G + common="$best" + ;; +*) + common="$bases" + ;; +esac + +git-update-index --refresh 2>/dev/null +git-read-tree -u -m $common $head $remotes || exit 2 +echo "Trying simple merge." +if result_tree=$(git-write-tree 2>/dev/null) +then + exit 0 +else + echo "Simple merge failed, trying Automatic merge." + if git-merge-index -o git-merge-one-file -a + then + exit 0 + else + exit 1 + fi +fi diff --git a/git-merge.sh b/git-merge.sh new file mode 100755 index 0000000000..04a5eb0f29 --- /dev/null +++ b/git-merge.sh @@ -0,0 +1,485 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +USAGE='[-n] [--no-commit] [--squash] [-s <strategy>] [-m=<merge-message>] <commit>+' + +SUBDIRECTORY_OK=Yes +. git-sh-setup +require_work_tree +cd_to_toplevel + +test -z "$(git ls-files -u)" || + die "You are in the middle of a conflicted merge." + +LF=' +' + +all_strategies='recur recursive octopus resolve stupid ours' +default_twohead_strategies='recursive' +default_octopus_strategies='octopus' +no_trivial_merge_strategies='ours' +use_strategies= + +index_merge=t + +dropsave() { + rm -f -- "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/MERGE_MSG" \ + "$GIT_DIR/MERGE_SAVE" || exit 1 +} + +savestate() { + # Stash away any local modifications. + git-diff-index -z --name-only $head | + cpio -0 -o >"$GIT_DIR/MERGE_SAVE" +} + +restorestate() { + if test -f "$GIT_DIR/MERGE_SAVE" + then + git reset --hard $head >/dev/null + cpio -iuv <"$GIT_DIR/MERGE_SAVE" + git-update-index --refresh >/dev/null + fi +} + +finish_up_to_date () { + case "$squash" in + t) + echo "$1 (nothing to squash)" ;; + '') + echo "$1" ;; + esac + dropsave +} + +squash_message () { + echo Squashed commit of the following: + echo + git-log --no-merges ^"$head" $remote +} + +finish () { + if test '' = "$2" + then + rlogm="$GIT_REFLOG_ACTION" + else + echo "$2" + rlogm="$GIT_REFLOG_ACTION: $2" + fi + case "$squash" in + t) + echo "Squash commit -- not updating HEAD" + squash_message >"$GIT_DIR/SQUASH_MSG" + ;; + '') + case "$merge_msg" in + '') + echo "No merge message -- not updating HEAD" + ;; + *) + git-update-ref -m "$rlogm" HEAD "$1" "$head" || exit 1 + ;; + esac + ;; + esac + case "$1" in + '') + ;; + ?*) + case "$no_summary" in + '') + git-diff-tree --stat --summary -M "$head" "$1" + ;; + esac + ;; + esac +} + +merge_name () { + remote="$1" + rh=$(git-rev-parse --verify "$remote^0" 2>/dev/null) || return + bh=$(git-show-ref -s --verify "refs/heads/$remote" 2>/dev/null) + if test "$rh" = "$bh" + then + echo "$rh branch '$remote' of ." + elif truname=$(expr "$remote" : '\(.*\)~[1-9][0-9]*$') && + git-show-ref -q --verify "refs/heads/$truname" 2>/dev/null + then + echo "$rh branch '$truname' (early part) of ." + else + echo "$rh commit '$remote'" + fi +} + +case "$#" in 0) usage ;; esac + +have_message= +while case "$#" in 0) break ;; esac +do + case "$1" in + -n|--n|--no|--no-|--no-s|--no-su|--no-sum|--no-summ|\ + --no-summa|--no-summar|--no-summary) + no_summary=t ;; + --sq|--squ|--squa|--squas|--squash) + squash=t no_commit=t ;; + --no-c|--no-co|--no-com|--no-comm|--no-commi|--no-commit) + no_commit=t ;; + -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\ + --strateg=*|--strategy=*|\ + -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy) + case "$#,$1" in + *,*=*) + strategy=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; + 1,*) + usage ;; + *) + strategy="$2" + shift ;; + esac + case " $all_strategies " in + *" $strategy "*) + use_strategies="$use_strategies$strategy " ;; + *) + die "available strategies are: $all_strategies" ;; + esac + ;; + -m=*|--m=*|--me=*|--mes=*|--mess=*|--messa=*|--messag=*|--message=*) + merge_msg=`expr "z$1" : 'z-[^=]*=\(.*\)'` + have_message=t + ;; + -m|--m|--me|--mes|--mess|--messa|--messag|--message) + shift + case "$#" in + 1) usage ;; + esac + merge_msg="$1" + have_message=t + ;; + -*) usage ;; + *) break ;; + esac + shift +done + +# This could be traditional "merge <msg> HEAD <commit>..." and the +# way we can tell it is to see if the second token is HEAD, but some +# people might have misused the interface and used a committish that +# is the same as HEAD there instead. Traditional format never would +# have "-m" so it is an additional safety measure to check for it. + +if test -z "$have_message" && + second_token=$(git-rev-parse --verify "$2^0" 2>/dev/null) && + head_commit=$(git-rev-parse --verify "HEAD" 2>/dev/null) && + test "$second_token" = "$head_commit" +then + merge_msg="$1" + shift + head_arg="$1" + shift +elif ! git-rev-parse --verify HEAD >/dev/null 2>&1 +then + # If the merged head is a valid one there is no reason to + # forbid "git merge" into a branch yet to be born. We do + # the same for "git pull". + if test 1 -ne $# + then + echo >&2 "Can merge only exactly one commit into empty head" + exit 1 + fi + + rh=$(git rev-parse --verify "$1^0") || + die "$1 - not something we can merge" + + git-update-ref -m "initial pull" HEAD "$rh" "" && + git-read-tree --reset -u HEAD + exit + +else + # We are invoked directly as the first-class UI. + head_arg=HEAD + + # All the rest are the commits being merged; prepare + # the standard merge summary message to be appended to + # the given message. If remote is invalid we will die + # later in the common codepath so we discard the error + # in this loop. + merge_name=$(for remote + do + merge_name "$remote" + done | git-fmt-merge-msg + ) + merge_msg="${merge_msg:+$merge_msg$LF$LF}$merge_name" +fi +head=$(git-rev-parse --verify "$head_arg"^0) || usage + +# All the rest are remote heads +test "$#" = 0 && usage ;# we need at least one remote head. +set_reflog_action "merge $*" + +remoteheads= +for remote +do + remotehead=$(git-rev-parse --verify "$remote"^0 2>/dev/null) || + die "$remote - not something we can merge" + remoteheads="${remoteheads}$remotehead " + eval GITHEAD_$remotehead='"$remote"' + export GITHEAD_$remotehead +done +set x $remoteheads ; shift + +case "$use_strategies" in +'') + case "$#" in + 1) + var="`git-config --get pull.twohead`" + if test -n "$var" + then + use_strategies="$var" + else + use_strategies="$default_twohead_strategies" + fi ;; + *) + var="`git-config --get pull.octopus`" + if test -n "$var" + then + use_strategies="$var" + else + use_strategies="$default_octopus_strategies" + fi ;; + esac + ;; +esac + +for s in $use_strategies +do + case " $s " in + *" $no_trivial_merge_strategies "*) + index_merge=f + break + ;; + esac +done + +case "$#" in +1) + common=$(git-merge-base --all $head "$@") + ;; +*) + common=$(git-show-branch --merge-base $head "$@") + ;; +esac +echo "$head" >"$GIT_DIR/ORIG_HEAD" + +case "$index_merge,$#,$common,$no_commit" in +f,*) + # We've been told not to try anything clever. Skip to real merge. + ;; +?,*,'',*) + # No common ancestors found. We need a real merge. + ;; +?,1,"$1",*) + # If head can reach all the merge then we are up to date. + # but first the most common case of merging one remote. + finish_up_to_date "Already up-to-date." + exit 0 + ;; +?,1,"$head",*) + # Again the most common case of merging one remote. + echo "Updating $(git-rev-parse --short $head)..$(git-rev-parse --short $1)" + git-update-index --refresh 2>/dev/null + new_head=$(git-rev-parse --verify "$1^0") && + git-read-tree -v -m -u --exclude-per-directory=.gitignore $head "$new_head" && + finish "$new_head" "Fast forward" + dropsave + exit 0 + ;; +?,1,?*"$LF"?*,*) + # We are not doing octopus and not fast forward. Need a + # real merge. + ;; +?,1,*,) + # We are not doing octopus, not fast forward, and have only + # one common. + git-update-index --refresh 2>/dev/null + case " $use_strategies " in + *' recursive '*|*' recur '*) + : run merge later + ;; + *) + # See if it is really trivial. + git var GIT_COMMITTER_IDENT >/dev/null || exit + echo "Trying really trivial in-index merge..." + if git-read-tree --trivial -m -u -v $common $head "$1" && + result_tree=$(git-write-tree) + then + echo "Wonderful." + result_commit=$( + echo "$merge_msg" | + git-commit-tree $result_tree -p HEAD -p "$1" + ) || exit + finish "$result_commit" "In-index merge" + dropsave + exit 0 + fi + echo "Nope." + esac + ;; +*) + # An octopus. If we can reach all the remote we are up to date. + up_to_date=t + for remote + do + common_one=$(git-merge-base --all $head $remote) + if test "$common_one" != "$remote" + then + up_to_date=f + break + fi + done + if test "$up_to_date" = t + then + finish_up_to_date "Already up-to-date. Yeeah!" + exit 0 + fi + ;; +esac + +# We are going to make a new commit. +git var GIT_COMMITTER_IDENT >/dev/null || exit + +# At this point, we need a real merge. No matter what strategy +# we use, it would operate on the index, possibly affecting the +# working tree, and when resolved cleanly, have the desired tree +# in the index -- this means that the index must be in sync with +# the $head commit. The strategies are responsible to ensure this. + +case "$use_strategies" in +?*' '?*) + # Stash away the local changes so that we can try more than one. + savestate + single_strategy=no + ;; +*) + rm -f "$GIT_DIR/MERGE_SAVE" + single_strategy=yes + ;; +esac + +result_tree= best_cnt=-1 best_strategy= wt_strategy= +merge_was_ok= +for strategy in $use_strategies +do + test "$wt_strategy" = '' || { + echo "Rewinding the tree to pristine..." + restorestate + } + case "$single_strategy" in + no) + echo "Trying merge strategy $strategy..." + ;; + esac + + # Remember which strategy left the state in the working tree + wt_strategy=$strategy + + git-merge-$strategy $common -- "$head_arg" "$@" + exit=$? + if test "$no_commit" = t && test "$exit" = 0 + then + merge_was_ok=t + exit=1 ;# pretend it left conflicts. + fi + + test "$exit" = 0 || { + + # The backend exits with 1 when conflicts are left to be resolved, + # with 2 when it does not handle the given merge at all. + + if test "$exit" -eq 1 + then + cnt=`{ + git-diff-files --name-only + git-ls-files --unmerged + } | wc -l` + if test $best_cnt -le 0 -o $cnt -le $best_cnt + then + best_strategy=$strategy + best_cnt=$cnt + fi + fi + continue + } + + # Automerge succeeded. + result_tree=$(git-write-tree) && break +done + +# If we have a resulting tree, that means the strategy module +# auto resolved the merge cleanly. +if test '' != "$result_tree" +then + parents=$(git-show-branch --independent "$head" "$@" | sed -e 's/^/-p /') + result_commit=$(echo "$merge_msg" | git-commit-tree $result_tree $parents) || exit + finish "$result_commit" "Merge made by $wt_strategy." + dropsave + exit 0 +fi + +# Pick the result from the best strategy and have the user fix it up. +case "$best_strategy" in +'') + restorestate + case "$use_strategies" in + ?*' '?*) + echo >&2 "No merge strategy handled the merge." + ;; + *) + echo >&2 "Merge with strategy $use_strategies failed." + ;; + esac + exit 2 + ;; +"$wt_strategy") + # We already have its result in the working tree. + ;; +*) + echo "Rewinding the tree to pristine..." + restorestate + echo "Using the $best_strategy to prepare resolving by hand." + git-merge-$best_strategy $common -- "$head_arg" "$@" + ;; +esac + +if test "$squash" = t +then + finish +else + for remote + do + echo $remote + done >"$GIT_DIR/MERGE_HEAD" + echo "$merge_msg" >"$GIT_DIR/MERGE_MSG" +fi + +if test "$merge_was_ok" = t +then + echo >&2 \ + "Automatic merge went well; stopped before committing as requested" + exit 0 +else + { + echo ' +Conflicts: +' + git ls-files --unmerged | + sed -e 's/^[^ ]* / /' | + uniq + } >>"$GIT_DIR/MERGE_MSG" + if test -d "$GIT_DIR/rr-cache" + then + git-rerere + fi + die "Automatic merge failed; fix conflicts and then commit the result." +fi diff --git a/git-p4import.py b/git-p4import.py new file mode 100644 index 0000000000..60a758bfe3 --- /dev/null +++ b/git-p4import.py @@ -0,0 +1,361 @@ +#!/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\" ?") + 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("config --get %s" % variable)[0].rstrip() + except: + return None + + def set_config(self, variable, value): + try: + self.git("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 + diff --git a/git-parse-remote.sh b/git-parse-remote.sh new file mode 100755 index 0000000000..5208ee6ce0 --- /dev/null +++ b/git-parse-remote.sh @@ -0,0 +1,297 @@ +#!/bin/sh + +# git-ls-remote could be called from outside a git managed repository; +# this would fail in that case and would issue an error message. +GIT_DIR=$(git-rev-parse --git-dir 2>/dev/null) || :; + +get_data_source () { + case "$1" in + */*) + echo '' + ;; + *) + if test "$(git-config --get "remote.$1.url")" + then + echo config + elif test -f "$GIT_DIR/remotes/$1" + then + echo remotes + elif test -f "$GIT_DIR/branches/$1" + then + echo branches + else + echo '' + fi ;; + esac +} + +get_remote_url () { + data_source=$(get_data_source "$1") + case "$data_source" in + '') + echo "$1" + ;; + config) + git-config --get "remote.$1.url" + ;; + remotes) + sed -ne '/^URL: */{ + s///p + q + }' "$GIT_DIR/remotes/$1" + ;; + branches) + sed -e 's/#.*//' "$GIT_DIR/branches/$1" + ;; + *) + die "internal error: get-remote-url $1" ;; + esac +} + +get_default_remote () { + curr_branch=$(git-symbolic-ref -q HEAD | sed -e 's|^refs/heads/||') + origin=$(git-config --get "branch.$curr_branch.remote") + echo ${origin:-origin} +} + +get_remote_default_refs_for_push () { + data_source=$(get_data_source "$1") + case "$data_source" in + '' | branches) + ;; # no default push mapping, just send matching refs. + config) + git-config --get-all "remote.$1.push" ;; + remotes) + sed -ne '/^Push: */{ + s///p + }' "$GIT_DIR/remotes/$1" ;; + *) + die "internal error: get-remote-default-ref-for-push $1" ;; + esac +} + +# Called from canon_refs_list_for_fetch -d "$remote", which +# is called from get_remote_default_refs_for_fetch to grok +# refspecs that are retrieved from the configuration, but not +# from get_remote_refs_for_fetch when it deals with refspecs +# supplied on the command line. $ls_remote_result has the list +# of refs available at remote. +# +# The first token returned is either "explicit" or "glob"; this +# is to help prevent randomly "globbed" ref from being chosen as +# a merge candidate +expand_refs_wildcard () { + remote="$1" + shift + first_one=yes + if test "$#" = 0 + then + echo empty + echo >&2 "Nothing specified for fetching with remote.$remote.fetch" + fi + for ref + do + lref=${ref#'+'} + # a non glob pattern is given back as-is. + expr "z$lref" : 'zrefs/.*/\*:refs/.*/\*$' >/dev/null || { + if test -n "$first_one" + then + echo "explicit" + first_one= + fi + echo "$ref" + continue + } + + # glob + if test -n "$first_one" + then + echo "glob" + first_one= + fi + from=`expr "z$lref" : 'z\(refs/.*/\)\*:refs/.*/\*$'` + to=`expr "z$lref" : 'zrefs/.*/\*:\(refs/.*/\)\*$'` + local_force= + test "z$lref" = "z$ref" || local_force='+' + echo "$ls_remote_result" | + sed -e '/\^{}$/d' | + ( + IFS=' ' + while read sha1 name + do + # ignore the ones that do not start with $from + mapped=${name#"$from"} + test "z$name" = "z$mapped" && continue + echo "${local_force}${name}:${to}${mapped}" + done + ) + done +} + +# Subroutine to canonicalize remote:local notation. +canon_refs_list_for_fetch () { + # If called from get_remote_default_refs_for_fetch + # leave the branches in branch.${curr_branch}.merge alone, + # or the first one otherwise; add prefix . to the rest + # to prevent the secondary branches to be merged by default. + merge_branches= + curr_branch= + if test "$1" = "-d" + then + shift ; remote="$1" ; shift + set $(expand_refs_wildcard "$remote" "$@") + is_explicit="$1" + shift + if test "$remote" = "$(get_default_remote)" + then + curr_branch=$(git-symbolic-ref -q HEAD | \ + sed -e 's|^refs/heads/||') + merge_branches=$(git-config \ + --get-all "branch.${curr_branch}.merge") + fi + if test -z "$merge_branches" && test $is_explicit != explicit + then + merge_branches=..this.will.never.match.any.ref.. + fi + fi + for ref + do + force= + case "$ref" in + +*) + ref=$(expr "z$ref" : 'z+\(.*\)') + force=+ + ;; + esac + expr "z$ref" : 'z.*:' >/dev/null || ref="${ref}:" + remote=$(expr "z$ref" : 'z\([^:]*\):') + local=$(expr "z$ref" : 'z[^:]*:\(.*\)') + dot_prefix=. + if test -z "$merge_branches" + then + merge_branches=$remote + dot_prefix= + else + for merge_branch in $merge_branches + do + [ "$remote" = "$merge_branch" ] && + dot_prefix= && break + done + fi + case "$remote" in + '' | HEAD ) remote=HEAD ;; + refs/heads/* | refs/tags/* | refs/remotes/*) ;; + heads/* | tags/* | remotes/* ) remote="refs/$remote" ;; + *) remote="refs/heads/$remote" ;; + esac + case "$local" in + '') local= ;; + refs/heads/* | refs/tags/* | refs/remotes/*) ;; + heads/* | tags/* | remotes/* ) local="refs/$local" ;; + *) local="refs/heads/$local" ;; + esac + + if local_ref_name=$(expr "z$local" : 'zrefs/\(.*\)') + then + git-check-ref-format "$local_ref_name" || + die "* refusing to create funny ref '$local_ref_name' locally" + fi + echo "${dot_prefix}${force}${remote}:${local}" + done +} + +# Returns list of src: (no store), or src:dst (store) +get_remote_default_refs_for_fetch () { + data_source=$(get_data_source "$1") + case "$data_source" in + '') + echo "HEAD:" ;; + config) + canon_refs_list_for_fetch -d "$1" \ + $(git-config --get-all "remote.$1.fetch") ;; + branches) + remote_branch=$(sed -ne '/#/s/.*#//p' "$GIT_DIR/branches/$1") + case "$remote_branch" in '') remote_branch=master ;; esac + echo "refs/heads/${remote_branch}:refs/heads/$1" + ;; + remotes) + canon_refs_list_for_fetch -d "$1" $(sed -ne '/^Pull: */{ + s///p + }' "$GIT_DIR/remotes/$1") + ;; + *) + die "internal error: get-remote-default-ref-for-push $1" ;; + esac +} + +get_remote_refs_for_push () { + case "$#" in + 0) die "internal error: get-remote-refs-for-push." ;; + 1) get_remote_default_refs_for_push "$@" ;; + *) shift; echo "$@" ;; + esac +} + +get_remote_refs_for_fetch () { + case "$#" in + 0) + die "internal error: get-remote-refs-for-fetch." ;; + 1) + get_remote_default_refs_for_fetch "$@" ;; + *) + shift + tag_just_seen= + for ref + do + if test "$tag_just_seen" + then + echo "refs/tags/${ref}:refs/tags/${ref}" + tag_just_seen= + continue + else + case "$ref" in + tag) + tag_just_seen=yes + continue + ;; + esac + fi + canon_refs_list_for_fetch "$ref" + done + ;; + esac +} + +resolve_alternates () { + # original URL (xxx.git) + top_=`expr "z$1" : 'z\([^:]*:/*[^/]*\)/'` + while read path + do + case "$path" in + \#* | '') + continue ;; + /*) + echo "$top_$path/" ;; + ../*) + # relative -- ugly but seems to work. + echo "$1/objects/$path/" ;; + *) + # exit code may not be caught by the reader. + echo "bad alternate: $path" + exit 1 ;; + esac + done +} + +get_uploadpack () { + data_source=$(get_data_source "$1") + case "$data_source" in + config) + uplp=$(git-config --get "remote.$1.uploadpack") + echo ${uplp:-git-upload-pack} + ;; + *) + echo "git-upload-pack" + ;; + esac +} diff --git a/git-pull.sh b/git-pull.sh new file mode 100755 index 0000000000..a3665d7751 --- /dev/null +++ b/git-pull.sh @@ -0,0 +1,120 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# +# Fetch one or more remote refs and merge it/them into the current HEAD. + +USAGE='[-n | --no-summary] [--no-commit] [-s strategy]... [<fetch-options>] <repo> <head>...' +LONG_USAGE='Fetch one or more remote refs and merge it/them into the current HEAD.' +SUBDIRECTORY_OK=Yes +. git-sh-setup +set_reflog_action "pull $*" +require_work_tree +cd_to_toplevel + +test -z "$(git ls-files -u)" || + die "You are in the middle of a conflicted merge." + +strategy_args= no_summary= no_commit= squash= +while case "$#,$1" in 0) break ;; *,-*) ;; *) break ;; esac +do + case "$1" in + -n|--n|--no|--no-|--no-s|--no-su|--no-sum|--no-summ|\ + --no-summa|--no-summar|--no-summary) + no_summary=-n ;; + --no-c|--no-co|--no-com|--no-comm|--no-commi|--no-commit) + no_commit=--no-commit ;; + --sq|--squ|--squa|--squas|--squash) + squash=--squash ;; + -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\ + --strateg=*|--strategy=*|\ + -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy) + case "$#,$1" in + *,*=*) + strategy=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; + 1,*) + usage ;; + *) + strategy="$2" + shift ;; + esac + strategy_args="${strategy_args}-s $strategy " + ;; + -h|--h|--he|--hel|--help) + usage + ;; + -*) + # Pass thru anything that is meant for fetch. + break + ;; + esac + shift +done + +orig_head=$(git-rev-parse --verify HEAD 2>/dev/null) +git-fetch --update-head-ok "$@" || exit 1 + +curr_head=$(git-rev-parse --verify HEAD 2>/dev/null) +if test "$curr_head" != "$orig_head" +then + # The fetch involved updating the current branch. + + # The working tree and the index file is still based on the + # $orig_head commit, but we are merging into $curr_head. + # First update the working tree to match $curr_head. + + echo >&2 "Warning: fetch updated the current branch head." + echo >&2 "Warning: fast forwarding your working tree from" + echo >&2 "Warning: commit $orig_head." + git-update-index --refresh 2>/dev/null + git-read-tree -u -m "$orig_head" "$curr_head" || + die 'Cannot fast-forward your working tree. +After making sure that you saved anything precious from +$ git diff '$orig_head' +output, run +$ git reset --hard +to recover.' + +fi + +merge_head=$(sed -e '/ not-for-merge /d' \ + -e 's/ .*//' "$GIT_DIR"/FETCH_HEAD | \ + tr '\012' ' ') + +case "$merge_head" in +'') + curr_branch=$(git-symbolic-ref -q HEAD) + case $? in + 0) ;; + 1) echo >&2 "You are not currently on a branch; you must explicitly" + echo >&2 "specify which branch you wish to merge:" + echo >&2 " git pull <remote> <branch>" + exit 1;; + *) exit $?;; + esac + curr_branch=${curr_branch#refs/heads/} + + echo >&2 "Warning: No merge candidate found because value of config option + \"branch.${curr_branch}.merge\" does not match any remote branch fetched." + echo >&2 "No changes." + exit 0 + ;; +?*' '?*) + if test -z "$orig_head" + then + echo >&2 "Cannot merge multiple branches into empty head" + exit 1 + fi + ;; +esac + +if test -z "$orig_head" +then + git-update-ref -m "initial pull" HEAD $merge_head "" && + git-read-tree --reset -u HEAD || exit 1 + exit +fi + +merge_name=$(git-fmt-merge-msg <"$GIT_DIR/FETCH_HEAD") || exit +exec git-merge $no_summary $no_commit $squash $strategy_args \ + "$merge_name" HEAD $merge_head diff --git a/git-quiltimport.sh b/git-quiltimport.sh new file mode 100755 index 0000000000..671a5ff865 --- /dev/null +++ b/git-quiltimport.sh @@ -0,0 +1,118 @@ +#!/bin/sh +USAGE='--dry-run --author <author> --patches </path/to/quilt/patch/directory>' +SUBDIRECTORY_ON=Yes +. git-sh-setup + +dry_run="" +quilt_author="" +while case "$#" in 0) break;; esac +do + case "$1" in + --au=*|--aut=*|--auth=*|--autho=*|--author=*) + quilt_author=$(expr "z$1" : 'z-[^=]*\(.*\)') + shift + ;; + + --au|--aut|--auth|--autho|--author) + case "$#" in 1) usage ;; esac + shift + quilt_author="$1" + shift + ;; + + --dry-run) + shift + dry_run=1 + ;; + + --pa=*|--pat=*|--patc=*|--patch=*|--patche=*|--patches=*) + QUILT_PATCHES=$(expr "z$1" : 'z-[^=]*\(.*\)') + shift + ;; + + --pa|--pat|--patc|--patch|--patche|--patches) + case "$#" in 1) usage ;; esac + shift + QUILT_PATCHES="$1" + shift + ;; + + *) + break + ;; + esac +done + +# Quilt Author +if [ -n "$quilt_author" ] ; then + quilt_author_name=$(expr "z$quilt_author" : 'z\(.*[^ ]\) *<.*') && + quilt_author_email=$(expr "z$quilt_author" : '.*<\([^>]*\)') && + test '' != "$quilt_author_name" && + test '' != "$quilt_author_email" || + die "malformed --author parameter" +fi + +# Quilt patch directory +: ${QUILT_PATCHES:=patches} +if ! [ -d "$QUILT_PATCHES" ] ; then + echo "The \"$QUILT_PATCHES\" directory does not exist." + exit 1 +fi + +# Temporary directories +tmp_dir=.dotest +tmp_msg="$tmp_dir/msg" +tmp_patch="$tmp_dir/patch" +tmp_info="$tmp_dir/info" + + +# Find the intial commit +commit=$(git-rev-parse HEAD) + +mkdir $tmp_dir || exit 2 +for patch_name in $(cat "$QUILT_PATCHES/series" | grep -v '^#'); do + echo $patch_name + (cat $QUILT_PATCHES/$patch_name | git-mailinfo "$tmp_msg" "$tmp_patch" > "$tmp_info") || exit 3 + + # Parse the author information + export GIT_AUTHOR_NAME=$(sed -ne 's/Author: //p' "$tmp_info") + export GIT_AUTHOR_EMAIL=$(sed -ne 's/Email: //p' "$tmp_info") + while test -z "$GIT_AUTHOR_EMAIL" && test -z "$GIT_AUTHOR_NAME" ; do + if [ -n "$quilt_author" ] ; then + GIT_AUTHOR_NAME="$quilt_author_name"; + GIT_AUTHOR_EMAIL="$quilt_author_email"; + elif [ -n "$dry_run" ]; then + echo "No author found in $patch_name" >&2; + GIT_AUTHOR_NAME="dry-run-not-found"; + GIT_AUTHOR_EMAIL="dry-run-not-found"; + else + echo "No author found in $patch_name" >&2; + echo "---" + cat $tmp_msg + printf "Author: "; + read patch_author + + echo "$patch_author" + + patch_author_name=$(expr "z$patch_author" : 'z\(.*[^ ]\) *<.*') && + patch_author_email=$(expr "z$patch_author" : '.*<\([^>]*\)') && + test '' != "$patch_author_name" && + test '' != "$patch_author_email" && + GIT_AUTHOR_NAME="$patch_author_name" && + GIT_AUTHOR_EMAIL="$patch_author_email" + fi + done + export GIT_AUTHOR_DATE=$(sed -ne 's/Date: //p' "$tmp_info") + export SUBJECT=$(sed -ne 's/Subject: //p' "$tmp_info") + if [ -z "$SUBJECT" ] ; then + SUBJECT=$(echo $patch_name | sed -e 's/.patch$//') + fi + + if [ -z "$dry_run" ] ; then + git-apply --index -C1 "$tmp_patch" && + tree=$(git-write-tree) && + commit=$((echo "$SUBJECT"; echo; cat "$tmp_msg") | git-commit-tree $tree -p $commit) && + git-update-ref -m "quiltimport: $patch_name" HEAD $commit || exit 4 + fi +done +rm -rf $tmp_dir || exit 5 diff --git a/git-rebase.sh b/git-rebase.sh new file mode 100755 index 0000000000..b51d19d12e --- /dev/null +++ b/git-rebase.sh @@ -0,0 +1,363 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano. +# + +USAGE='[-v] [--onto <newbase>] <upstream> [<branch>]' +LONG_USAGE='git-rebase replaces <branch> with a new branch of the +same name. When the --onto option is provided the new branch starts +out with a HEAD equal to <newbase>, otherwise it is equal to <upstream> +It then attempts to create a new commit for each commit from the original +<branch> that does not exist in the <upstream> branch. + +It is possible that a merge failure will prevent this process from being +completely automatic. You will have to resolve any such merge failure +and run git rebase --continue. Another option is to bypass the commit +that caused the merge failure with git rebase --skip. To restore the +original <branch> and remove the .dotest working files, use the command +git rebase --abort instead. + +Note that if <branch> is not specified on the command line, the +currently checked out branch is used. You must be in the top +directory of your project to start (or continue) a rebase. + +Example: git-rebase master~1 topic + + A---B---C topic A'\''--B'\''--C'\'' topic + / --> / + D---E---F---G master D---E---F---G master +' + +SUBDIRECTORY_OK=Yes +. git-sh-setup +set_reflog_action rebase +require_work_tree +cd_to_toplevel + +RESOLVEMSG=" +When you have resolved this problem run \"git rebase --continue\". +If you would prefer to skip this patch, instead run \"git rebase --skip\". +To restore the original branch and stop rebasing run \"git rebase --abort\". +" +unset newbase +strategy=recursive +do_merge= +dotest=$GIT_DIR/.dotest-merge +prec=4 +verbose= +git_am_opt= + +continue_merge () { + test -n "$prev_head" || die "prev_head must be defined" + test -d "$dotest" || die "$dotest directory does not exist" + + unmerged=$(git-ls-files -u) + if test -n "$unmerged" + then + echo "You still have unmerged paths in your index" + echo "did you forget update-index?" + die "$RESOLVEMSG" + fi + + if test -n "`git-diff-index HEAD`" + then + if ! git-commit -C "`cat $dotest/current`" + then + echo "Commit failed, please do not call \"git commit\"" + echo "directly, but instead do one of the following: " + die "$RESOLVEMSG" + fi + printf "Committed: %0${prec}d" $msgnum + else + printf "Already applied: %0${prec}d" $msgnum + fi + echo ' '`git-rev-list --pretty=oneline -1 HEAD | \ + sed 's/^[a-f0-9]\+ //'` + + prev_head=`git-rev-parse HEAD^0` + # save the resulting commit so we can read-tree on it later + echo "$prev_head" > "$dotest/prev_head" + + # onto the next patch: + msgnum=$(($msgnum + 1)) + echo "$msgnum" >"$dotest/msgnum" +} + +call_merge () { + cmt="$(cat $dotest/cmt.$1)" + echo "$cmt" > "$dotest/current" + hd=$(git-rev-parse --verify HEAD) + cmt_name=$(git-symbolic-ref HEAD) + msgnum=$(cat $dotest/msgnum) + end=$(cat $dotest/end) + eval GITHEAD_$cmt='"${cmt_name##refs/heads/}~$(($end - $msgnum))"' + eval GITHEAD_$hd='"$(cat $dotest/onto_name)"' + export GITHEAD_$cmt GITHEAD_$hd + git-merge-$strategy "$cmt^" -- "$hd" "$cmt" + rv=$? + case "$rv" in + 0) + unset GITHEAD_$cmt GITHEAD_$hd + return + ;; + 1) + test -d "$GIT_DIR/rr-cache" && git-rerere + die "$RESOLVEMSG" + ;; + 2) + echo "Strategy: $rv $strategy failed, try another" 1>&2 + die "$RESOLVEMSG" + ;; + *) + die "Unknown exit code ($rv) from command:" \ + "git-merge-$strategy $cmt^ -- HEAD $cmt" + ;; + esac +} + +finish_rb_merge () { + rm -r "$dotest" + echo "All done." +} + +while case "$#" in 0) break ;; esac +do + case "$1" in + --continue) + diff=$(git-diff-files) + case "$diff" in + ?*) echo "You must edit all merge conflicts and then" + echo "mark them as resolved using git update-index" + exit 1 + ;; + esac + if test -d "$dotest" + then + prev_head="`cat $dotest/prev_head`" + end="`cat $dotest/end`" + msgnum="`cat $dotest/msgnum`" + onto="`cat $dotest/onto`" + continue_merge + while test "$msgnum" -le "$end" + do + call_merge "$msgnum" + continue_merge + done + finish_rb_merge + exit + fi + git am --resolved --3way --resolvemsg="$RESOLVEMSG" + exit + ;; + --skip) + if test -d "$dotest" + then + if test -d "$GIT_DIR/rr-cache" + then + git-rerere clear + fi + prev_head="`cat $dotest/prev_head`" + end="`cat $dotest/end`" + msgnum="`cat $dotest/msgnum`" + msgnum=$(($msgnum + 1)) + onto="`cat $dotest/onto`" + while test "$msgnum" -le "$end" + do + call_merge "$msgnum" + continue_merge + done + finish_rb_merge + exit + fi + git am -3 --skip --resolvemsg="$RESOLVEMSG" + exit + ;; + --abort) + if test -d "$GIT_DIR/rr-cache" + then + git-rerere clear + fi + if test -d "$dotest" + then + rm -r "$dotest" + elif test -d .dotest + then + rm -r .dotest + else + die "No rebase in progress?" + fi + git reset --hard ORIG_HEAD + exit + ;; + --onto) + test 2 -le "$#" || usage + newbase="$2" + shift + ;; + -M|-m|--m|--me|--mer|--merg|--merge) + do_merge=t + ;; + -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\ + --strateg=*|--strategy=*|\ + -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy) + case "$#,$1" in + *,*=*) + strategy=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; + 1,*) + usage ;; + *) + strategy="$2" + shift ;; + esac + do_merge=t + ;; + -v|--verbose) + verbose=t + ;; + -C*) + git_am_opt=$1 + shift + ;; + -*) + usage + ;; + *) + break + ;; + esac + shift +done + +# Make sure we do not have .dotest +if test -z "$do_merge" +then + if mkdir .dotest + then + rmdir .dotest + else + echo >&2 ' +It seems that I cannot create a .dotest directory, and I wonder if you +are in the middle of patch application or another rebase. If that is not +the case, please rm -fr .dotest and run me again. I am stopping in case +you still have something valuable there.' + exit 1 + fi +else + if test -d "$dotest" + then + die "previous dotest directory $dotest still exists." \ + 'try git-rebase < --continue | --abort >' + fi +fi + +# The tree must be really really clean. +git-update-index --refresh || exit +diff=$(git-diff-index --cached --name-status -r HEAD) +case "$diff" in +?*) echo "cannot rebase: your index is not up-to-date" + echo "$diff" + exit 1 + ;; +esac + +# The upstream head must be given. Make sure it is valid. +upstream_name="$1" +upstream=`git rev-parse --verify "${upstream_name}^0"` || + die "invalid upstream $upstream_name" + +# If a hook exists, give it a chance to interrupt +if test -x "$GIT_DIR/hooks/pre-rebase" +then + "$GIT_DIR/hooks/pre-rebase" ${1+"$@"} || { + echo >&2 "The pre-rebase hook refused to rebase." + exit 1 + } +fi + +# If the branch to rebase is given, first switch to it. +case "$#" in +2) + branch_name="$2" + git-checkout "$2" || usage + ;; +*) + if branch_name=`git symbolic-ref -q HEAD` + then + branch_name=`expr "z$branch_name" : 'zrefs/heads/\(.*\)'` + else + branch_name=HEAD ;# detached + fi + ;; +esac +branch=$(git-rev-parse --verify "${branch_name}^0") || exit + +# Make sure the branch to rebase onto is valid. +onto_name=${newbase-"$upstream_name"} +onto=$(git-rev-parse --verify "${onto_name}^0") || exit + +# Now we are rebasing commits $upstream..$branch on top of $onto + +# Check if we are already based on $onto, but this should be +# done only when upstream and onto are the same. +mb=$(git-merge-base "$onto" "$branch") +if test "$upstream" = "$onto" && test "$mb" = "$onto" +then + echo >&2 "Current branch $branch_name is up to date." + exit 0 +fi + +if test -n "$verbose" +then + echo "Changes from $mb to $onto:" + git-diff-tree --stat --summary "$mb" "$onto" +fi + +# Rewind the head to "$onto"; this saves our current head in ORIG_HEAD. +echo "First, rewinding head to replay your work on top of it..." +git-reset --hard "$onto" + +# If the $onto is a proper descendant of the tip of the branch, then +# we just fast forwarded. +if test "$mb" = "$branch" +then + echo >&2 "Fast-forwarded $branch_name to $onto_name." + exit 0 +fi + +if test -z "$do_merge" +then + git-format-patch -k --stdout --full-index --ignore-if-in-upstream "$upstream"..ORIG_HEAD | + git am $git_am_opt --binary -3 -k --resolvemsg="$RESOLVEMSG" + exit $? +fi + +# start doing a rebase with git-merge +# this is rename-aware if the recursive (default) strategy is used + +mkdir -p "$dotest" +echo "$onto" > "$dotest/onto" +echo "$onto_name" > "$dotest/onto_name" +prev_head=`git-rev-parse HEAD^0` +echo "$prev_head" > "$dotest/prev_head" + +msgnum=0 +for cmt in `git-rev-list --no-merges "$upstream"..ORIG_HEAD \ + | @@PERL@@ -e 'print reverse <>'` +do + msgnum=$(($msgnum + 1)) + echo "$cmt" > "$dotest/cmt.$msgnum" +done + +echo 1 >"$dotest/msgnum" +echo $msgnum >"$dotest/end" + +end=$msgnum +msgnum=1 + +while test "$msgnum" -le "$end" +do + call_merge "$msgnum" + continue_merge +done + +finish_rb_merge diff --git a/git-relink.perl b/git-relink.perl new file mode 100755 index 0000000000..f6b4f6a2f8 --- /dev/null +++ b/git-relink.perl @@ -0,0 +1,173 @@ +#!/usr/bin/env perl +# Copyright 2005, Ryan Anderson <ryan@michonline.com> +# Distribution permitted under the GPL v2, as distributed +# by the Free Software Foundation. +# Later versions of the GPL at the discretion of Linus Torvalds +# +# Scan two git object-trees, and hardlink any common objects between them. + +use 5.006; +use strict; +use warnings; +use Getopt::Long; + +sub get_canonical_form($); +sub do_scan_directory($$$); +sub compare_two_files($$); +sub usage(); +sub link_two_files($$); + +# stats +my $total_linked = 0; +my $total_already = 0; +my ($linked,$already); + +my $fail_on_different_sizes = 0; +my $help = 0; +GetOptions("safe" => \$fail_on_different_sizes, + "help" => \$help); + +usage() if $help; + +my (@dirs) = @ARGV; + +usage() if (!defined $dirs[0] || !defined $dirs[1]); + +$_ = get_canonical_form($_) foreach (@dirs); + +my $master_dir = pop @dirs; + +opendir(D,$master_dir . "objects/") + or die "Failed to open $master_dir/objects/ : $!"; + +my @hashdirs = grep !/^\.{1,2}$/, readdir(D); + +foreach my $repo (@dirs) { + $linked = 0; + $already = 0; + printf("Searching '%s' and '%s' for common objects and hardlinking them...\n", + $master_dir,$repo); + + foreach my $hashdir (@hashdirs) { + do_scan_directory($master_dir, $hashdir, $repo); + } + + printf("Linked %d files, %d were already linked.\n",$linked, $already); + + $total_linked += $linked; + $total_already += $already; +} + +printf("Totals: Linked %d files, %d were already linked.\n", + $total_linked, $total_already); + + +sub do_scan_directory($$$) { + my ($srcdir, $subdir, $dstdir) = @_; + + my $sfulldir = sprintf("%sobjects/%s/",$srcdir,$subdir); + my $dfulldir = sprintf("%sobjects/%s/",$dstdir,$subdir); + + opendir(S,$sfulldir) + or die "Failed to opendir $sfulldir: $!"; + + foreach my $file (grep(!/\.{1,2}$/, readdir(S))) { + my $sfilename = $sfulldir . $file; + my $dfilename = $dfulldir . $file; + + compare_two_files($sfilename,$dfilename); + + } + closedir(S); +} + +sub compare_two_files($$) { + my ($sfilename, $dfilename) = @_; + + # Perl's stat returns relevant information as follows: + # 0 = dev number + # 1 = inode number + # 7 = size + my @sstatinfo = stat($sfilename); + my @dstatinfo = stat($dfilename); + + if (@sstatinfo == 0 && @dstatinfo == 0) { + die sprintf("Stat of both %s and %s failed: %s\n",$sfilename, $dfilename, $!); + + } elsif (@dstatinfo == 0) { + return; + } + + if ( ($sstatinfo[0] == $dstatinfo[0]) && + ($sstatinfo[1] != $dstatinfo[1])) { + if ($sstatinfo[7] == $dstatinfo[7]) { + link_two_files($sfilename, $dfilename); + + } else { + my $err = sprintf("ERROR: File sizes are not the same, cannot relink %s to %s.\n", + $sfilename, $dfilename); + if ($fail_on_different_sizes) { + die $err; + } else { + warn $err; + } + } + + } elsif ( ($sstatinfo[0] == $dstatinfo[0]) && + ($sstatinfo[1] == $dstatinfo[1])) { + $already++; + } +} + +sub get_canonical_form($) { + my $dir = shift; + my $original = $dir; + + die "$dir is not a directory." unless -d $dir; + + $dir .= "/" unless $dir =~ m#/$#; + $dir .= ".git/" unless $dir =~ m#\.git/$#; + + die "$original does not have a .git/ subdirectory.\n" unless -d $dir; + + return $dir; +} + +sub link_two_files($$) { + my ($sfilename, $dfilename) = @_; + my $tmpdname = sprintf("%s.old",$dfilename); + rename($dfilename,$tmpdname) + or die sprintf("Failure renaming %s to %s: %s", + $dfilename, $tmpdname, $!); + + if (! link($sfilename,$dfilename)) { + my $failtxt = ""; + unless (rename($tmpdname,$dfilename)) { + $failtxt = sprintf( + "Git Repository containing %s is probably corrupted, " . + "please copy '%s' to '%s' to fix.\n", + $tmpdname, $dfilename); + } + + die sprintf("Failed to link %s to %s: %s\n%s" . + $sfilename, $dfilename, + $!, $dfilename, $failtxt); + } + + unlink($tmpdname) + or die sprintf("Unlink of %s failed: %s\n", + $dfilename, $!); + + $linked++; +} + + +sub usage() { + print("Usage: $0 [--safe] <dir> [<dir> ...] <master_dir> \n"); + print("All directories should contain a .git/objects/ subdirectory.\n"); + print("Options\n"); + print("\t--safe\t" . + "Stops if two objects with the same hash exist but " . + "have different sizes. Default is to warn and continue.\n"); + exit(1); +} diff --git a/git-remote.perl b/git-remote.perl new file mode 100755 index 0000000000..c56c5a84a4 --- /dev/null +++ b/git-remote.perl @@ -0,0 +1,364 @@ +#!/usr/bin/perl -w + +use Git; +my $git = Git->repository(); + +sub add_remote_config { + my ($hash, $name, $what, $value) = @_; + if ($what eq 'url') { + if (exists $hash->{$name}{'URL'}) { + print STDERR "Warning: more than one remote.$name.url\n"; + } + $hash->{$name}{'URL'} = $value; + } + elsif ($what eq 'fetch') { + $hash->{$name}{'FETCH'} ||= []; + push @{$hash->{$name}{'FETCH'}}, $value; + } + if (!exists $hash->{$name}{'SOURCE'}) { + $hash->{$name}{'SOURCE'} = 'config'; + } +} + +sub add_remote_remotes { + my ($hash, $file, $name) = @_; + + if (exists $hash->{$name}) { + $hash->{$name}{'WARNING'} = 'ignored due to config'; + return; + } + + my $fh; + if (!open($fh, '<', $file)) { + print STDERR "Warning: cannot open $file\n"; + return; + } + my $it = { 'SOURCE' => 'remotes' }; + $hash->{$name} = $it; + while (<$fh>) { + chomp; + if (/^URL:\s*(.*)$/) { + # Having more than one is Ok -- it is used for push. + if (! exists $it->{'URL'}) { + $it->{'URL'} = $1; + } + } + elsif (/^Push:\s*(.*)$/) { + ; # later + } + elsif (/^Pull:\s*(.*)$/) { + $it->{'FETCH'} ||= []; + push @{$it->{'FETCH'}}, $1; + } + elsif (/^\#/) { + ; # ignore + } + else { + print STDERR "Warning: funny line in $file: $_\n"; + } + } + close($fh); +} + +sub list_remote { + my ($git) = @_; + my %seen = (); + my @remotes = eval { + $git->command(qw(config --get-regexp), '^remote\.'); + }; + for (@remotes) { + if (/^remote\.([^.]*)\.(\S*)\s+(.*)$/) { + add_remote_config(\%seen, $1, $2, $3); + } + } + + my $dir = $git->repo_path() . "/remotes"; + if (opendir(my $dh, $dir)) { + local $_; + while ($_ = readdir($dh)) { + chomp; + next if (! -f "$dir/$_" || ! -r _); + add_remote_remotes(\%seen, "$dir/$_", $_); + } + } + + return \%seen; +} + +sub add_branch_config { + my ($hash, $name, $what, $value) = @_; + if ($what eq 'remote') { + if (exists $hash->{$name}{'REMOTE'}) { + print STDERR "Warning: more than one branch.$name.remote\n"; + } + $hash->{$name}{'REMOTE'} = $value; + } + elsif ($what eq 'merge') { + $hash->{$name}{'MERGE'} ||= []; + push @{$hash->{$name}{'MERGE'}}, $value; + } +} + +sub list_branch { + my ($git) = @_; + my %seen = (); + my @branches = eval { + $git->command(qw(config --get-regexp), '^branch\.'); + }; + for (@branches) { + if (/^branch\.([^.]*)\.(\S*)\s+(.*)$/) { + add_branch_config(\%seen, $1, $2, $3); + } + } + + return \%seen; +} + +my $remote = list_remote($git); +my $branch = list_branch($git); + +sub update_ls_remote { + my ($harder, $info) = @_; + + return if (($harder == 0) || + (($harder == 1) && exists $info->{'LS_REMOTE'})); + + my @ref = map { + s|^[0-9a-f]{40}\s+refs/heads/||; + $_; + } $git->command(qw(ls-remote --heads), $info->{'URL'}); + $info->{'LS_REMOTE'} = \@ref; +} + +sub list_wildcard_mapping { + my ($forced, $ours, $ls) = @_; + my %refs; + for (@$ls) { + $refs{$_} = 01; # bit #0 to say "they have" + } + for ($git->command('for-each-ref', "refs/remotes/$ours")) { + chomp; + next unless (s|^[0-9a-f]{40}\s[a-z]+\srefs/remotes/$ours/||); + next if ($_ eq 'HEAD'); + $refs{$_} ||= 0; + $refs{$_} |= 02; # bit #1 to say "we have" + } + my (@new, @stale, @tracked); + for (sort keys %refs) { + my $have = $refs{$_}; + if ($have == 1) { + push @new, $_; + } + elsif ($have == 2) { + push @stale, $_; + } + elsif ($have == 3) { + push @tracked, $_; + } + } + return \@new, \@stale, \@tracked; +} + +sub list_mapping { + my ($name, $info) = @_; + my $fetch = $info->{'FETCH'}; + my $ls = $info->{'LS_REMOTE'}; + my (@new, @stale, @tracked); + + for (@$fetch) { + next unless (/(\+)?([^:]+):(.*)/); + my ($forced, $theirs, $ours) = ($1, $2, $3); + if ($theirs eq 'refs/heads/*' && + $ours =~ /^refs\/remotes\/(.*)\/\*$/) { + # wildcard mapping + my ($w_new, $w_stale, $w_tracked) + = list_wildcard_mapping($forced, $1, $ls); + push @new, @$w_new; + push @stale, @$w_stale; + push @tracked, @$w_tracked; + } + elsif ($theirs =~ /\*/ || $ours =~ /\*/) { + print STDERR "Warning: unrecognized mapping in remotes.$name.fetch: $_\n"; + } + elsif ($theirs =~ s|^refs/heads/||) { + if (!grep { $_ eq $theirs } @$ls) { + push @stale, $theirs; + } + elsif ($ours ne '') { + push @tracked, $theirs; + } + } + } + return \@new, \@stale, \@tracked; +} + +sub show_mapping { + my ($name, $info) = @_; + my ($new, $stale, $tracked) = list_mapping($name, $info); + if (@$new) { + print " New remote branches (next fetch will store in remotes/$name)\n"; + print " @$new\n"; + } + if (@$stale) { + print " Stale tracking branches in remotes/$name (use 'git remote prune')\n"; + print " @$stale\n"; + } + if (@$tracked) { + print " Tracked remote branches\n"; + print " @$tracked\n"; + } +} + +sub prune_remote { + my ($name, $ls_remote) = @_; + if (!exists $remote->{$name}) { + print STDERR "No such remote $name\n"; + return; + } + my $info = $remote->{$name}; + update_ls_remote($ls_remote, $info); + + my ($new, $stale, $tracked) = list_mapping($name, $info); + my $prefix = "refs/remotes/$name"; + foreach my $to_prune (@$stale) { + my @v = $git->command(qw(rev-parse --verify), "$prefix/$to_prune"); + $git->command(qw(update-ref -d), "$prefix/$to_prune", $v[0]); + } +} + +sub show_remote { + my ($name, $ls_remote) = @_; + if (!exists $remote->{$name}) { + print STDERR "No such remote $name\n"; + return; + } + my $info = $remote->{$name}; + update_ls_remote($ls_remote, $info); + + print "* remote $name\n"; + print " URL: $info->{'URL'}\n"; + for my $branchname (sort keys %$branch) { + next if ($branch->{$branchname}{'REMOTE'} ne $name); + my @merged = map { + s|^refs/heads/||; + $_; + } split(' ',"@{$branch->{$branchname}{'MERGE'}}"); + next unless (@merged); + print " Remote branch(es) merged with 'git pull' while on branch $branchname\n"; + print " @merged\n"; + } + if ($info->{'LS_REMOTE'}) { + show_mapping($name, $info); + } +} + +sub add_remote { + my ($name, $url, $opts) = @_; + if (exists $remote->{$name}) { + print STDERR "remote $name already exists.\n"; + exit(1); + } + $git->command('config', "remote.$name.url", $url); + my $track = $opts->{'track'} || ["*"]; + + for (@$track) { + $git->command('config', '--add', "remote.$name.fetch", + "+refs/heads/$_:refs/remotes/$name/$_"); + } + if ($opts->{'fetch'}) { + $git->command('fetch', $name); + } + if (exists $opts->{'master'}) { + $git->command('symbolic-ref', "refs/remotes/$name/HEAD", + "refs/remotes/$name/$opts->{'master'}"); + } +} + +sub add_usage { + print STDERR "Usage: git remote add [-f] [-t track]* [-m master] <name> <url>\n"; + exit(1); +} + +if (!@ARGV) { + for (sort keys %$remote) { + print "$_\n"; + } +} +elsif ($ARGV[0] eq 'show') { + my $ls_remote = 1; + my $i; + for ($i = 1; $i < @ARGV; $i++) { + if ($ARGV[$i] eq '-n') { + $ls_remote = 0; + } + else { + last; + } + } + if ($i >= @ARGV) { + print STDERR "Usage: git remote show <remote>\n"; + exit(1); + } + for (; $i < @ARGV; $i++) { + show_remote($ARGV[$i], $ls_remote); + } +} +elsif ($ARGV[0] eq 'prune') { + my $ls_remote = 1; + my $i; + for ($i = 1; $i < @ARGV; $i++) { + if ($ARGV[$i] eq '-n') { + $ls_remote = 0; + } + else { + last; + } + } + if ($i >= @ARGV) { + print STDERR "Usage: git remote prune <remote>\n"; + exit(1); + } + for (; $i < @ARGV; $i++) { + prune_remote($ARGV[$i], $ls_remote); + } +} +elsif ($ARGV[0] eq 'add') { + my %opts = (); + while (1 < @ARGV && $ARGV[1] =~ /^-/) { + my $opt = $ARGV[1]; + shift @ARGV; + if ($opt eq '-f' || $opt eq '--fetch') { + $opts{'fetch'} = 1; + next; + } + if ($opt eq '-t' || $opt eq '--track') { + if (@ARGV < 1) { + add_usage(); + } + $opts{'track'} ||= []; + push @{$opts{'track'}}, $ARGV[1]; + shift @ARGV; + next; + } + if ($opt eq '-m' || $opt eq '--master') { + if ((@ARGV < 1) || exists $opts{'master'}) { + add_usage(); + } + $opts{'master'} = $ARGV[1]; + shift @ARGV; + next; + } + add_usage(); + } + if (@ARGV != 3) { + add_usage(); + } + add_remote($ARGV[1], $ARGV[2], \%opts); +} +else { + print STDERR "Usage: git remote\n"; + print STDERR " git remote add <name> <url>\n"; + print STDERR " git remote show <name>\n"; + print STDERR " git remote prune <name>\n"; + exit(1); +} diff --git a/git-repack.sh b/git-repack.sh new file mode 100755 index 0000000000..ddfa8b44a1 --- /dev/null +++ b/git-repack.sh @@ -0,0 +1,119 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# + +USAGE='[-a] [-d] [-f] [-l] [-n] [-q] [--window=N] [--depth=N]' +SUBDIRECTORY_OK='Yes' +. git-sh-setup + +no_update_info= all_into_one= remove_redundant= +local= quiet= no_reuse_delta= extra= +while case "$#" in 0) break ;; esac +do + case "$1" in + -n) no_update_info=t ;; + -a) all_into_one=t ;; + -d) remove_redundant=t ;; + -q) quiet=-q ;; + -f) no_reuse_delta=--no-reuse-delta ;; + -l) local=--local ;; + --window=*) extra="$extra $1" ;; + --depth=*) extra="$extra $1" ;; + *) usage ;; + esac + shift +done + +# Later we will default repack.UseDeltaBaseOffset to true +default_dbo=false + +case "`git config --bool repack.usedeltabaseoffset || + echo $default_dbo`" in +true) + extra="$extra --delta-base-offset" ;; +esac + +PACKDIR="$GIT_OBJECT_DIRECTORY/pack" +PACKTMP="$GIT_DIR/.tmp-$$-pack" +rm -f "$PACKTMP"-* +trap 'rm -f "$PACKTMP"-*' 0 1 2 3 15 + +# There will be more repacking strategies to come... +case ",$all_into_one," in +,,) + args='--unpacked --incremental' + ;; +,t,) + if [ -d "$PACKDIR" ]; then + for e in `cd "$PACKDIR" && find . -type f -name '*.pack' \ + | sed -e 's/^\.\///' -e 's/\.pack$//'` + do + if [ -e "$PACKDIR/$e.keep" ]; then + : keep + else + args="$args --unpacked=$e.pack" + existing="$existing $e" + fi + done + fi + [ -z "$args" ] && args='--unpacked --incremental' + ;; +esac + +args="$args $local $quiet $no_reuse_delta$extra" +name=$(git-pack-objects --non-empty --all --reflog $args </dev/null "$PACKTMP") || + exit 1 +if [ -z "$name" ]; then + echo Nothing new to pack. +else + chmod a-w "$PACKTMP-$name.pack" + chmod a-w "$PACKTMP-$name.idx" + if test "$quiet" != '-q'; then + echo "Pack pack-$name created." + fi + mkdir -p "$PACKDIR" || exit + + for sfx in pack idx + do + if test -f "$PACKDIR/pack-$name.$sfx" + then + mv -f "$PACKDIR/pack-$name.$sfx" \ + "$PACKDIR/old-pack-$name.$sfx" + fi + done && + mv -f "$PACKTMP-$name.pack" "$PACKDIR/pack-$name.pack" && + mv -f "$PACKTMP-$name.idx" "$PACKDIR/pack-$name.idx" && + test -f "$PACKDIR/pack-$name.pack" && + test -f "$PACKDIR/pack-$name.idx" || { + echo >&2 "Couldn't replace the existing pack with updated one." + echo >&2 "The original set of packs have been saved as" + echo >&2 "old-pack-$name.{pack,idx} in $PACKDIR." + exit 1 + } + rm -f "$PACKDIR/old-pack-$name.pack" "$PACKDIR/old-pack-$name.idx" +fi + +if test "$remove_redundant" = t +then + # We know $existing are all redundant. + if [ -n "$existing" ] + then + sync + ( cd "$PACKDIR" && + for e in $existing + do + case "$e" in + pack-$name) ;; + *) rm -f "$e.pack" "$e.idx" "$e.keep" ;; + esac + done + ) + fi + git-prune-packed $quiet +fi + +case "$no_update_info" in +t) : ;; +*) git-update-server-info ;; +esac diff --git a/git-request-pull.sh b/git-request-pull.sh new file mode 100755 index 0000000000..4eacc3a059 --- /dev/null +++ b/git-request-pull.sh @@ -0,0 +1,33 @@ +#!/bin/sh -e +# Copyright 2005, Ryan Anderson <ryan@michonline.com> +# +# This file is licensed under the GPL v2, or a later version +# at the discretion of Linus Torvalds. + +USAGE='<commit> <url> [<head>]' +LONG_USAGE='Summarizes the changes since <commit> to the standard output, +and includes <url> in the message generated.' +SUBDIRECTORY_OK='Yes' +. git-sh-setup + +revision=$1 +url=$2 +head=${3-HEAD} + +[ "$revision" ] || usage +[ "$url" ] || usage + +baserev=`git-rev-parse --verify "$revision"^0` && +headrev=`git-rev-parse --verify "$head"^0` || exit + +echo "The following changes since commit $baserev:" +git log --max-count=1 --pretty=short "$baserev" | +git-shortlog | sed -e 's/^\(.\)/ \1/' + +echo "are found in the git repository at:" +echo +echo " $url" +echo + +git log $baserev..$headrev | git-shortlog ; +git diff -M --stat --summary $baserev..$headrev diff --git a/git-reset.sh b/git-reset.sh new file mode 100755 index 0000000000..fee6d98d9c --- /dev/null +++ b/git-reset.sh @@ -0,0 +1,106 @@ +#!/bin/sh +# +# Copyright (c) 2005, 2006 Linus Torvalds and Junio C Hamano +# +USAGE='[--mixed | --soft | --hard] [<commit-ish>] [ [--] <paths>...]' +SUBDIRECTORY_OK=Yes +. git-sh-setup +set_reflog_action "reset $*" +require_work_tree + +update= reset_type=--mixed +unset rev + +while case $# in 0) break ;; esac +do + case "$1" in + --mixed | --soft | --hard) + reset_type="$1" + ;; + --) + break + ;; + -*) + usage + ;; + *) + rev=$(git-rev-parse --verify "$1") || exit + shift + break + ;; + esac + shift +done + +: ${rev=HEAD} +rev=$(git-rev-parse --verify $rev^0) || exit + +# Skip -- in "git reset HEAD -- foo" and "git reset -- foo". +case "$1" in --) shift ;; esac + +# git reset --mixed tree [--] paths... can be used to +# load chosen paths from the tree into the index without +# affecting the working tree nor HEAD. +if test $# != 0 +then + test "$reset_type" = "--mixed" || + die "Cannot do partial $reset_type reset." + + git-diff-index --cached $rev -- "$@" | + sed -e 's/^:\([0-7][0-7]*\) [0-7][0-7]* \([0-9a-f][0-9a-f]*\) [0-9a-f][0-9a-f]* [A-Z] \(.*\)$/\1 \2 \3/' | + git update-index --add --remove --index-info || exit + git update-index --refresh + exit +fi + +cd_to_toplevel + +if test "$reset_type" = "--hard" +then + update=-u +fi + +# Soft reset does not touch the index file nor the working tree +# at all, but requires them in a good order. Other resets reset +# the index file to the tree object we are switching to. +if test "$reset_type" = "--soft" +then + if test -f "$GIT_DIR/MERGE_HEAD" || + test "" != "$(git-ls-files --unmerged)" + then + die "Cannot do a soft reset in the middle of a merge." + fi +else + git-read-tree --reset $update "$rev" || exit +fi + +# Any resets update HEAD to the head being switched to. +if orig=$(git-rev-parse --verify HEAD 2>/dev/null) +then + echo "$orig" >"$GIT_DIR/ORIG_HEAD" +else + rm -f "$GIT_DIR/ORIG_HEAD" +fi +git-update-ref -m "$GIT_REFLOG_ACTION" HEAD "$rev" +update_ref_status=$? + +case "$reset_type" in +--hard ) + test $update_ref_status = 0 && { + printf "HEAD is now at " + GIT_PAGER= git log --max-count=1 --pretty=oneline \ + --abbrev-commit HEAD + } + ;; +--soft ) + ;; # Nothing else to do +--mixed ) + # Report what has not been updated. + git-update-index --refresh + ;; +esac + +rm -f "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/rr-cache/MERGE_RR" \ + "$GIT_DIR/SQUASH_MSG" "$GIT_DIR/MERGE_MSG" + +exit $update_ref_status diff --git a/git-resolve.sh b/git-resolve.sh new file mode 100755 index 0000000000..36b90e3849 --- /dev/null +++ b/git-resolve.sh @@ -0,0 +1,112 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# +# Resolve two trees. +# + +echo 'WARNING: This command is DEPRECATED and will be removed very soon.' >&2 +echo 'WARNING: Please use git-merge or git-pull instead.' >&2 +sleep 2 + +USAGE='<head> <remote> <merge-message>' +. git-sh-setup + +dropheads() { + rm -f -- "$GIT_DIR/MERGE_HEAD" \ + "$GIT_DIR/LAST_MERGE" || exit 1 +} + +head=$(git-rev-parse --verify "$1"^0) && +merge=$(git-rev-parse --verify "$2"^0) && +merge_name="$2" && +merge_msg="$3" || usage + +# +# The remote name is just used for the message, +# but we do want it. +# +if [ -z "$head" -o -z "$merge" -o -z "$merge_msg" ]; then + usage +fi + +dropheads +echo $head > "$GIT_DIR"/ORIG_HEAD +echo $merge > "$GIT_DIR"/LAST_MERGE + +common=$(git-merge-base $head $merge) +if [ -z "$common" ]; then + die "Unable to find common commit between" $merge $head +fi + +case "$common" in +"$merge") + echo "Already up-to-date. Yeeah!" + dropheads + exit 0 + ;; +"$head") + echo "Updating $(git-rev-parse --short $head)..$(git-rev-parse --short $merge)" + git-read-tree -u -m $head $merge || exit 1 + git-update-ref -m "resolve $merge_name: Fast forward" \ + HEAD "$merge" "$head" + git-diff-tree -p $head $merge | git-apply --stat + dropheads + exit 0 + ;; +esac + +# We are going to make a new commit. +git var GIT_COMMITTER_IDENT >/dev/null || exit + +# Find an optimum merge base if there are more than one candidates. +LF=' +' +common=$(git-merge-base -a $head $merge) +case "$common" in +?*"$LF"?*) + echo "Trying to find the optimum merge base." + G=.tmp-index$$ + best= + best_cnt=-1 + for c in $common + do + rm -f $G + GIT_INDEX_FILE=$G git-read-tree -m $c $head $merge \ + 2>/dev/null || continue + # Count the paths that are unmerged. + cnt=`GIT_INDEX_FILE=$G git-ls-files --unmerged | wc -l` + if test $best_cnt -le 0 -o $cnt -le $best_cnt + then + best=$c + best_cnt=$cnt + if test "$best_cnt" -eq 0 + then + # Cannot do any better than all trivial merge. + break + fi + fi + done + rm -f $G + common="$best" +esac + +echo "Trying to merge $merge into $head using $common." +git-update-index --refresh 2>/dev/null +git-read-tree -u -m $common $head $merge || exit 1 +result_tree=$(git-write-tree 2> /dev/null) +if [ $? -ne 0 ]; then + echo "Simple merge failed, trying Automatic merge" + git-merge-index -o git-merge-one-file -a + if [ $? -ne 0 ]; then + echo $merge > "$GIT_DIR"/MERGE_HEAD + die "Automatic merge failed, fix up by hand" + fi + result_tree=$(git-write-tree) || exit 1 +fi +result_commit=$(echo "$merge_msg" | git-commit-tree $result_tree -p $head -p $merge) +echo "Committed merge $result_commit" +git-update-ref -m "resolve $merge_name: In-index merge" \ + HEAD "$result_commit" "$head" +git-diff-tree -p $head $result_commit | git-apply --stat +dropheads diff --git a/git-revert.sh b/git-revert.sh new file mode 100755 index 0000000000..49f00321b2 --- /dev/null +++ b/git-revert.sh @@ -0,0 +1,197 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# Copyright (c) 2005 Junio C Hamano +# + +case "$0" in +*-revert* ) + test -t 0 && edit=-e + replay= + me=revert + USAGE='[--edit | --no-edit] [-n] <commit-ish>' ;; +*-cherry-pick* ) + replay=t + edit= + me=cherry-pick + USAGE='[--edit] [-n] [-r] [-x] <commit-ish>' ;; +* ) + echo >&2 "What are you talking about?" + exit 1 ;; +esac + +SUBDIRECTORY_OK=Yes ;# we will cd up +. git-sh-setup +require_work_tree +cd_to_toplevel + +no_commit= +while case "$#" in 0) break ;; esac +do + case "$1" in + -n|--n|--no|--no-|--no-c|--no-co|--no-com|--no-comm|\ + --no-commi|--no-commit) + no_commit=t + ;; + -e|--e|--ed|--edi|--edit) + edit=-e + ;; + --n|--no|--no-|--no-e|--no-ed|--no-edi|--no-edit) + edit= + ;; + -r) + : no-op ;; + -x|--i-really-want-to-expose-my-private-commit-object-name) + replay= + ;; + -*) + usage + ;; + *) + break + ;; + esac + shift +done + +set_reflog_action "$me" + +test "$me,$replay" = "revert,t" && usage + +case "$no_commit" in +t) + # We do not intend to commit immediately. We just want to + # merge the differences in. + head=$(git-write-tree) || + die "Your index file is unmerged." + ;; +*) + head=$(git-rev-parse --verify HEAD) || + die "You do not have a valid HEAD" + files=$(git-diff-index --cached --name-only $head) || exit + if [ "$files" ]; then + die "Dirty index: cannot $me (dirty: $files)" + fi + ;; +esac + +rev=$(git-rev-parse --verify "$@") && +commit=$(git-rev-parse --verify "$rev^0") || + die "Not a single commit $@" +prev=$(git-rev-parse --verify "$commit^1" 2>/dev/null) || + die "Cannot run $me a root commit" +git-rev-parse --verify "$commit^2" >/dev/null 2>&1 && + die "Cannot run $me a multi-parent commit." + +encoding=$(git config i18n.commitencoding || echo UTF-8) + +# "commit" is an existing commit. We would want to apply +# the difference it introduces since its first parent "prev" +# on top of the current HEAD if we are cherry-pick. Or the +# reverse of it if we are revert. + +case "$me" in +revert) + git show -s --pretty=oneline --encoding="$encoding" $commit | + sed -e ' + s/^[^ ]* /Revert "/ + s/$/"/ + ' + echo + echo "This reverts commit $commit." + test "$rev" = "$commit" || + echo "(original 'git revert' arguments: $@)" + base=$commit next=$prev + ;; + +cherry-pick) + pick_author_script=' + /^author /{ + s/'\''/'\''\\'\'\''/g + h + s/^author \([^<]*\) <[^>]*> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_NAME='\''&'\''/p + + g + s/^author [^<]* <\([^>]*\)> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p + + g + s/^author [^<]* <[^>]*> \(.*\)$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_DATE='\''&'\''/p + + q + }' + + logmsg=`git show -s --pretty=raw --encoding="$encoding" "$commit"` + set_author_env=`echo "$logmsg" | + LANG=C LC_ALL=C sed -ne "$pick_author_script"` + eval "$set_author_env" + export GIT_AUTHOR_NAME + export GIT_AUTHOR_EMAIL + export GIT_AUTHOR_DATE + + echo "$logmsg" | + sed -e '1,/^$/d' -e 's/^ //' + case "$replay" in + '') + echo "(cherry picked from commit $commit)" + test "$rev" = "$commit" || + echo "(original 'git cherry-pick' arguments: $@)" + ;; + esac + base=$prev next=$commit + ;; + +esac >.msg + +eval GITHEAD_$head=HEAD +eval GITHEAD_$next='`git show -s \ + --pretty=oneline --encoding="$encoding" "$commit" | + sed -e "s/^[^ ]* //"`' +export GITHEAD_$head GITHEAD_$next + +# This three way merge is an interesting one. We are at +# $head, and would want to apply the change between $commit +# and $prev on top of us (when reverting), or the change between +# $prev and $commit on top of us (when cherry-picking or replaying). + +git-merge-recursive $base -- $head $next && +result=$(git-write-tree 2>/dev/null) || { + mv -f .msg "$GIT_DIR/MERGE_MSG" + { + echo ' +Conflicts: +' + git ls-files --unmerged | + sed -e 's/^[^ ]* / /' | + uniq + } >>"$GIT_DIR/MERGE_MSG" + echo >&2 "Automatic $me failed. After resolving the conflicts," + echo >&2 "mark the corrected paths with 'git-add <paths>'" + echo >&2 "and commit the result." + case "$me" in + cherry-pick) + echo >&2 "You may choose to use the following when making" + echo >&2 "the commit:" + echo >&2 "$set_author_env" + esac + exit 1 +} +echo >&2 "Finished one $me." + +# If we are cherry-pick, and if the merge did not result in +# hand-editing, we will hit this commit and inherit the original +# author date and name. +# If we are revert, or if our cherry-pick results in a hand merge, +# we had better say that the current user is responsible for that. + +case "$no_commit" in +'') + git-commit -n -F .msg $edit + rm -f .msg + ;; +esac diff --git a/git-send-email.perl b/git-send-email.perl new file mode 100755 index 0000000000..6a285bfd21 --- /dev/null +++ b/git-send-email.perl @@ -0,0 +1,613 @@ +#!/usr/bin/perl -w +# +# Copyright 2002,2005 Greg Kroah-Hartman <greg@kroah.com> +# Copyright 2005 Ryan Anderson <ryan@michonline.com> +# +# GPL v2 (See COPYING) +# +# Ported to support git "mbox" format files by Ryan Anderson <ryan@michonline.com> +# +# Sends a collection of emails to the given email addresses, disturbingly fast. +# +# Supports two formats: +# 1. mbox format files (ignoring most headers and MIME formatting - this is designed for sending patches) +# 2. The original format support by Greg's script: +# first line of the message is who to CC, +# and second line is the subject of the message. +# + +use strict; +use warnings; +use Term::ReadLine; +use Getopt::Long; +use Data::Dumper; +use Git; + +package FakeTerm; +sub new { + my ($class, $reason) = @_; + return bless \$reason, shift; +} +sub readline { + my $self = shift; + die "Cannot use readline on FakeTerm: $$self"; +} +package main; + +# most mail servers generate the Date: header, but not all... +sub format_2822_time { + my ($time) = @_; + my @localtm = localtime($time); + my @gmttm = gmtime($time); + my $localmin = $localtm[1] + $localtm[2] * 60; + my $gmtmin = $gmttm[1] + $gmttm[2] * 60; + if ($localtm[0] != $gmttm[0]) { + die "local zone differs from GMT by a non-minute interval\n"; + } + if ((($gmttm[6] + 1) % 7) == $localtm[6]) { + $localmin += 1440; + } elsif ((($gmttm[6] - 1) % 7) == $localtm[6]) { + $localmin -= 1440; + } elsif ($gmttm[6] != $localtm[6]) { + die "local time offset greater than or equal to 24 hours\n"; + } + my $offset = $localmin - $gmtmin; + my $offhour = $offset / 60; + my $offmin = abs($offset % 60); + if (abs($offhour) >= 24) { + die ("local time offset greater than or equal to 24 hours\n"); + } + + return sprintf("%s, %2d %s %d %02d:%02d:%02d %s%02d%02d", + qw(Sun Mon Tue Wed Thu Fri Sat)[$localtm[6]], + $localtm[3], + qw(Jan Feb Mar Apr May Jun + Jul Aug Sep Oct Nov Dec)[$localtm[4]], + $localtm[5]+1900, + $localtm[2], + $localtm[1], + $localtm[0], + ($offset >= 0) ? '+' : '-', + abs($offhour), + $offmin, + ); +} + +my $have_email_valid = eval { require Email::Valid; 1 }; +my $smtp; + +sub unique_email_list(@); +sub cleanup_compose_files(); + +# Constants (essentially) +my $compose_filename = ".msg.$$"; + +# Variables we fill in automatically, or via prompting: +my (@to,@cc,@initial_cc,@bcclist,@xh, + $initial_reply_to,$initial_subject,@files,$from,$compose,$time); + +# Behavior modification variables +my ($chain_reply_to, $quiet, $suppress_from, $no_signed_off_cc, + $dry_run) = (1, 0, 0, 0, 0); +my $smtp_server; + +# Example reply to: +#$initial_reply_to = ''; #<20050203173208.GA23964@foobar.com>'; + +my $repo = Git->repository(); +my $term = eval { + new Term::ReadLine 'git-send-email'; +}; +if ($@) { + $term = new FakeTerm "$@: going non-interactive"; +} + +# Begin by accumulating all the variables (defined above), that we will end up +# needing, first, from the command line: + +my $rc = GetOptions("from=s" => \$from, + "in-reply-to=s" => \$initial_reply_to, + "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, + "quiet" => \$quiet, + "suppress-from" => \$suppress_from, + "no-signed-off-cc|no-signed-off-by-cc" => \$no_signed_off_cc, + "dry-run" => \$dry_run, + ); + +# Verify the user input + +foreach my $entry (@to) { + die "Comma in --to entry: $entry'\n" unless $entry !~ m/,/; +} + +foreach my $entry (@initial_cc) { + die "Comma in --cc entry: $entry'\n" unless $entry !~ m/,/; +} + +foreach my $entry (@bcclist) { + die "Comma in --bcclist entry: $entry'\n" unless $entry !~ m/,/; +} + +# Now, let's fill any that aren't set in with defaults: + +my ($author) = $repo->ident_person('author'); +my ($committer) = $repo->ident_person('committer'); + +my %aliases; +my @alias_files = $repo->config('sendemail.aliasesfile'); +my $aliasfiletype = $repo->config('sendemail.aliasfiletype'); +my %parse_alias = ( + # multiline formats can be supported in the future + mutt => sub { my $fh = shift; while (<$fh>) { + if (/^alias\s+(\S+)\s+(.*)$/) { + my ($alias, $addr) = ($1, $2); + $addr =~ s/#.*$//; # mutt allows # comments + # commas delimit multiple addresses + $aliases{$alias} = [ split(/\s*,\s*/, $addr) ]; + }}}, + mailrc => sub { my $fh = shift; while (<$fh>) { + if (/^alias\s+(\S+)\s+(.*)$/) { + # spaces delimit multiple addresses + $aliases{$1} = [ split(/\s+/, $2) ]; + }}}, + pine => sub { my $fh = shift; while (<$fh>) { + if (/^(\S+)\s+(.*)$/) { + $aliases{$1} = [ split(/\s*,\s*/, $2) ]; + }}}, + gnus => sub { my $fh = shift; while (<$fh>) { + if (/\(define-mail-alias\s+"(\S+?)"\s+"(\S+?)"\)/) { + $aliases{$1} = [ $2 ]; + }}} +); + +if (@alias_files and $aliasfiletype and defined $parse_alias{$aliasfiletype}) { + foreach my $file (@alias_files) { + open my $fh, '<', $file or die "opening $file: $!\n"; + $parse_alias{$aliasfiletype}->($fh); + close $fh; + } +} + +my $prompting = 0; +if (!defined $from) { + $from = $author || $committer; + do { + $_ = $term->readline("Who should the emails appear to be from? [$from] "); + } while (!defined $_); + + $from = $_ if ($_); + print "Emails will be sent from: ", $from, "\n"; + $prompting++; +} + +if (!@to) { + do { + $_ = $term->readline("Who should the emails be sent to? ", + ""); + } while (!defined $_); + my $to = $_; + push @to, split /,/, $to; + $prompting++; +} + +sub expand_aliases { + my @cur = @_; + my @last; + do { + @last = @cur; + @cur = map { $aliases{$_} ? @{$aliases{$_}} : $_ } @last; + } while (join(',',@cur) ne join(',',@last)); + return @cur; +} + +@to = expand_aliases(@to); +@initial_cc = expand_aliases(@initial_cc); +@bcclist = expand_aliases(@bcclist); + +if (!defined $initial_subject && $compose) { + do { + $_ = $term->readline("What subject should the emails start with? ", + $initial_subject); + } while (!defined $_); + $initial_subject = $_; + $prompting++; +} + +if (!defined $initial_reply_to && $prompting) { + do { + $_= $term->readline("Message-ID to be used as In-Reply-To for the first email? ", + $initial_reply_to); + } while (!defined $_); + + $initial_reply_to = $_; + $initial_reply_to =~ s/(^\s+|\s+$)//g; +} + +if (!$smtp_server) { + $smtp_server = $repo->config('sendemail.smtpserver'); +} +if (!$smtp_server) { + foreach (qw( /usr/sbin/sendmail /usr/lib/sendmail )) { + if (-x $_) { + $smtp_server = $_; + last; + } + } + $smtp_server ||= 'localhost'; # could be 127.0.0.1, too... *shrug* +} + +if ($compose) { + # Note that this does not need to be secure, but we will make a small + # effort to have it be unique + open(C,">",$compose_filename) + or die "Failed to open for writing $compose_filename: $!"; + print C "From $from # This line is ignored.\n"; + printf C "Subject: %s\n\n", $initial_subject; + printf C <<EOT; +GIT: Please enter your email below. +GIT: Lines beginning in "GIT: " will be removed. +GIT: Consider including an overall diffstat or table of contents +GIT: for the patch you are writing. + +EOT + close(C); + + my $editor = $ENV{EDITOR}; + $editor = 'vi' unless defined $editor; + system($editor, $compose_filename); + + open(C2,">",$compose_filename . ".final") + or die "Failed to open $compose_filename.final : " . $!; + + open(C,"<",$compose_filename) + or die "Failed to open $compose_filename : " . $!; + + while(<C>) { + next if m/^GIT: /; + print C2 $_; + } + close(C); + close(C2); + + do { + $_ = $term->readline("Send this email? (y|n) "); + } while (!defined $_); + + if (uc substr($_,0,1) ne 'Y') { + cleanup_compose_files(); + exit(0); + } + + @files = ($compose_filename . ".final"); +} + + +# Now that all the defaults are set, process the rest of the command line +# arguments and collect up the files that need to be processed. +for my $f (@ARGV) { + if (-d $f) { + opendir(DH,$f) + or die "Failed to opendir $f: $!"; + + push @files, grep { -f $_ } map { +$f . "/" . $_ } + sort readdir(DH); + + } elsif (-f $f) { + push @files, $f; + + } else { + print STDERR "Skipping $f - not found.\n"; + } +} + +if (@files) { + unless ($quiet) { + print $_,"\n" for (@files); + } +} else { + print <<EOT; +git-send-email [options] <file | directory> [... file | directory ] +Options: + --from Specify the "From:" line of the email to be sent. + + --to Specify the primary "To:" line of the email. + + --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. + + --subject Specify the initial "Subject:" line. + Only necessary if --compose is also set. If --compose + is not set, this will be prompted for. + + --in-reply-to Specify the first "In-Reply-To:" header line. + Only used if --compose is also set. If --compose is not + set, this will be prompted for. + + --chain-reply-to If set, the replies will all be to the previous + email sent, rather than to the first email sent. + Defaults to on. + + --no-signed-off-cc Suppress the automatic addition of email addresses + that appear in a Signed-off-by: line, to the cc: list. + Note: Using this option is not recommended. + + --smtp-server If set, specifies the outgoing SMTP server to use. + Defaults to localhost. + + --suppress-from Suppress sending emails to yourself if your address + appears in a From: line. + + --quiet Make git-send-email less verbose. One line per email should be + all that is output. + +Error: Please specify a file or a directory on the command line. +EOT + exit(1); +} + +# Variables we set as part of the loop over files +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 =~ /^($local_part_regexp)$/); + + if ($have_email_valid) { + 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 + $address =~ /($local_part_regexp\@$domain_regexp)/; + return $1; + } +} + +# Usually don't need to change anything below here. + +# we make a "fake" message id by taking the current number +# of seconds since the beginning of Unix time and tacking on +# a random number to the end, in case we are called quicker than +# 1 second since the last time we were called. + +# We'll setup a template for the message id, using the "from" address: +my $message_id_from = extract_valid_address($from); +my $message_id_template = "<%s-git-send-email-$message_id_from>"; + +sub make_message_id +{ + my $date = time; + my $pseudo_rand = int (rand(4200)); + $message_id = sprintf $message_id_template, "$date$pseudo_rand"; + #print "new message id = $message_id\n"; # Was useful for debugging +} + + + +$cc = ""; +$time = time - scalar $#files; + +sub unquote_rfc2047 { + local ($_) = @_; + if (s/=\?utf-8\?q\?(.*)\?=/$1/g) { + s/_/ /g; + s/=([0-9A-F]{2})/chr(hex($1))/eg; + } + return "$_"; +} + +sub send_message +{ + my @recipients = unique_email_list(@to); + my $to = join (",\n\t", @recipients); + @recipients = unique_email_list(@recipients,@cc,@bcclist); + my $date = format_2822_time($time++); + my $gitversion = '@@GIT_VERSION@@'; + if ($gitversion =~ m/..GIT_VERSION../) { + $gitversion = Git::version(); + } + + my ($author_name) = ($from =~ /^(.*?)\s+</); + if ($author_name && $author_name =~ /\./ && $author_name !~ /^".*"$/) { + my ($name, $addr) = ($from =~ /^(.*?)(\s+<.*)/); + $from = "\"$name\"$addr"; + } + my $header = "From: $from +To: $to +Cc: $cc +Subject: $subject +Date: $date +Message-Id: $message_id +X-Mailer: git-send-email $gitversion +"; + if ($reply_to) { + + $header .= "In-Reply-To: $reply_to\n"; + $header .= "References: $references\n"; + } + if (@xh) { + $header .= join("\n", @xh) . "\n"; + } + + if ($dry_run) { + # We don't want to send the email. + } elsif ($smtp_server =~ m#^/#) { + my $pid = open my $sm, '|-'; + defined $pid or die $!; + if (!$pid) { + 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; + $smtp->data or die $smtp->message; + $smtp->datasend("$header\n$message") or die $smtp->message; + $smtp->dataend() or die $smtp->message; + $smtp->ok or die "Failed to send $subject\n".$smtp->message; + } + if ($quiet) { + printf "Sent %s\n", $subject; + } else { + print "OK. Log says:\nDate: $date\n"; + if ($smtp) { + print "Server: $smtp_server\n"; + } else { + print "Sendmail: $smtp_server\n"; + } + print "From: $from\nSubject: $subject\nCc: $cc\nTo: $to\n\n"; + if ($smtp) { + print "Result: ", $smtp->code, ' ', + ($smtp->message =~ /\n([^\n]+\n)$/s), "\n"; + } else { + print "Result: OK\n"; + } + } +} + +$reply_to = $initial_reply_to; +$references = $initial_reply_to || ''; +make_message_id(); +$subject = $initial_subject; + +foreach my $t (@files) { + open(F,"<",$t) or die "can't open file $t"; + + my $author_not_sender = undef; + @cc = @initial_cc; + @xh = (); + my $input_format = undef; + my $header_done = 0; + $message = ""; + while(<F>) { + if (!$header_done) { + if (/^From /) { + $input_format = 'mbox'; + next; + } + chomp; + if (!defined $input_format && /^[-A-Za-z]+:\s/) { + $input_format = 'mbox'; + } + + if (defined $input_format && $input_format eq 'mbox') { + if (/^Subject:\s+(.*)$/) { + $subject = $1; + + } elsif (/^(Cc|From):\s+(.*)$/) { + if ($2 eq $from) { + next if ($suppress_from); + } + elsif ($1 eq 'From') { + $author_not_sender = $2; + } + printf("(mbox) Adding cc: %s from line '%s'\n", + $2, $_) unless $quiet; + push @cc, $2; + } + elsif (!/^Date:\s/ && /^[-A-Za-z]+:\s+\S/) { + push @xh, $_; + } + + } else { + # In the traditional + # "send lots of email" format, + # line 1 = cc + # line 2 = subject + # So let's support that, too. + $input_format = 'lots'; + if (@cc == 0) { + printf("(non-mbox) Adding cc: %s from line '%s'\n", + $_, $_) unless $quiet; + + push @cc, $_; + + } elsif (!defined $subject) { + $subject = $_; + } + } + + # A whitespace line will terminate the headers + if (m/^\s*$/) { + $header_done = 1; + } + } else { + $message .= $_; + if (/^Signed-off-by: (.*)$/i && !$no_signed_off_cc) { + my $c = $1; + chomp $c; + push @cc, $c; + printf("(sob) Adding cc: %s from line '%s'\n", + $c, $_) unless $quiet; + } + } + } + close F; + if (defined $author_not_sender) { + $author_not_sender = unquote_rfc2047($author_not_sender); + $message = "From: $author_not_sender\n\n$message"; + } + + $cc = join(", ", unique_email_list(@cc)); + + send_message(); + + # set up for the next message + if ($chain_reply_to || !defined $reply_to || length($reply_to) == 0) { + $reply_to = $message_id; + if (length $references > 0) { + $references .= " $message_id"; + } else { + $references = "$message_id"; + } + } + make_message_id(); +} + +if ($compose) { + cleanup_compose_files(); +} + +sub cleanup_compose_files() { + unlink($compose_filename, $compose_filename . ".final"); + +} + +$smtp->quit if $smtp; + +sub unique_email_list(@) { + my %seen; + my @emails; + + foreach my $entry (@_) { + if (my $clean = extract_valid_address($entry)) { + $seen{$clean} ||= 0; + next if $seen{$clean}++; + push @emails, $entry; + } else { + print STDERR "W: unable to extract a valid address", + " from: $entry\n"; + } + } + return @emails; +} diff --git a/git-sh-setup.sh b/git-sh-setup.sh new file mode 100755 index 0000000000..f24c7f2d23 --- /dev/null +++ b/git-sh-setup.sh @@ -0,0 +1,83 @@ +#!/bin/sh +# +# This is included in commands that either have to be run from the toplevel +# of the repository, or with GIT_DIR environment variable properly. +# If the GIT_DIR does not look like the right correct git-repository, +# it dies. + +# Having this variable in your environment would break scripts because +# you would cause "cd" to be be taken to unexpected places. If you +# like CDPATH, define it for your interactive shell sessions without +# exporting it. +unset CDPATH + +die() { + echo >&2 "$@" + exit 1 +} + +usage() { + die "Usage: $0 $USAGE" +} + +set_reflog_action() { + if [ -z "${GIT_REFLOG_ACTION:+set}" ] + then + GIT_REFLOG_ACTION="$*" + export GIT_REFLOG_ACTION + fi +} + +is_bare_repository () { + git-config --bool --get core.bare || + case "$GIT_DIR" in + .git | */.git) echo false ;; + *) echo true ;; + esac +} + +cd_to_toplevel () { + cdup=$(git-rev-parse --show-cdup) + if test ! -z "$cdup" + then + cd "$cdup" || { + echo >&2 "Cannot chdir to $cdup, the toplevel of the working tree" + exit 1 + } + fi +} + +require_work_tree () { + test $(is_bare_repository) = false && + test $(git-rev-parse --is-inside-git-dir) = false || + die "fatal: $0 cannot be used without a working tree." +} + +if [ -z "$LONG_USAGE" ] +then + LONG_USAGE="Usage: $0 $USAGE" +else + LONG_USAGE="Usage: $0 $USAGE + +$LONG_USAGE" +fi + +case "$1" in + -h|--h|--he|--hel|--help) + echo "$LONG_USAGE" + exit +esac + +# Make sure we are in a valid repository of a vintage we understand. +if [ -z "$SUBDIRECTORY_OK" ] +then + : ${GIT_DIR=.git} + GIT_DIR=$(GIT_DIR="$GIT_DIR" git-rev-parse --git-dir) || { + exit=$? + echo >&2 "You need to run this command from the toplevel of the working tree." + exit $exit + } +else + GIT_DIR=$(git-rev-parse --git-dir) || exit +fi +: ${GIT_OBJECT_DIRECTORY="$GIT_DIR/objects"} diff --git a/git-svn.perl b/git-svn.perl new file mode 100755 index 0000000000..d792a62d7c --- /dev/null +++ b/git-svn.perl @@ -0,0 +1,3063 @@ +#!/usr/bin/env perl +# Copyright (C) 2006, Eric Wong <normalperson@yhbt.net> +# License: GPL v2 or later +use warnings; +use strict; +use vars qw/ $AUTHOR $VERSION + $SVN_URL $SVN_INFO $SVN_WC $SVN_UUID + $GIT_SVN_INDEX $GIT_SVN + $GIT_DIR $GIT_SVN_DIR $REVDB/; +$AUTHOR = 'Eric Wong <normalperson@yhbt.net>'; +$VERSION = '@@GIT_VERSION@@'; + +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'; +$| = 1; # unbuffer STDOUT + +# properties that we do not log: +my %SKIP = ( 'svn:wc:ra_dav:version-url' => 1, + 'svn:special' => 1, + 'svn:executable' => 1, + 'svn:entry:committed-rev' => 1, + 'svn:entry:last-author' => 1, + 'svn:entry:uuid' => 1, + 'svn:entry:committed-date' => 1, +); + +sub fatal (@) { print STDERR @_; exit 1 } +require SVN::Core; # use()-ing this causes segfaults for me... *shrug* +require SVN::Ra; +require SVN::Delta; +if ($SVN::Core::VERSION lt '1.1.0') { + fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)\n"; +} +push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor'; +push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor'; +*SVN::Git::Fetcher::process_rm = *process_rm; +use Carp qw/croak/; +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 pass_through/; +use POSIX qw/strftime/; +use IPC::Open3; +use Memoize; +use Git qw/command command_oneline command_noisy + command_output_pipe command_input_pipe command_close_pipe/; +memoize('revisions_eq'); +memoize('cmt_metadata'); +memoize('get_commit_time'); + +my ($SVN); + +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 $_esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/; +my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, + $_find_copies_harder, $_l, $_cp_similarity, $_cp_remote, + $_repack, $_repack_nr, $_repack_flags, $_q, + $_message, $_file, $_follow_parent, $_no_metadata, + $_template, $_shared, $_no_default_regex, $_no_graft_copy, + $_limit, $_verbose, $_incremental, $_oneline, $_l_fmt, $_show_commit, + $_version, $_upgrade, $_authors, $_branch_all_refs, @_opt_m, + $_merge, $_strategy, $_dry_run, $_ignore_nodate, $_non_recursive, + $_username, $_config_dir, $_no_auth_cache, + $_pager, $_color, $_prefix); +my (@_branch_from, %tree_map, %users, %rusers, %equiv); +my ($_svn_can_do_switch); +my @repo_path_split_cache; + +my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext, + 'branch|b=s' => \@_branch_from, + 'follow-parent|follow' => \$_follow_parent, + 'branch-all-refs|B' => \$_branch_all_refs, + 'authors-file|A=s' => \$_authors, + 'repack:i' => \$_repack, + 'no-metadata' => \$_no_metadata, + 'quiet|q' => \$_q, + 'username=s' => \$_username, + 'config-dir=s' => \$_config_dir, + 'no-auth-cache' => \$_no_auth_cache, + 'ignore-nodate' => \$_ignore_nodate, + '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 ); +my %cmt_opts = ( 'edit|e' => \$_edit, + 'rmdir' => \$_rmdir, + 'find-copies-harder' => \$_find_copies_harder, + 'l=i' => \$_l, + 'copy-similarity|C=i'=> \$_cp_similarity +); + +my %cmd = ( + fetch => [ \&cmd_fetch, "Download new revisions from SVN", + { 'revision|r=s' => \$_revision, %fc_opts } ], + init => [ \&init, "Initialize a repo for tracking" . + " (requires URL argument)", + \%init_opts ], + dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream', + { 'merge|m|M' => \$_merge, + 'strategy|s=s' => \$_strategy, + 'dry-run|n' => \$_dry_run, + %cmt_opts, %fc_opts } ], + 'set-tree' => [ \&commit, "Set an SVN repository to a git tree-ish", + { 'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ], + '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, + 'branch|b=s' => \@_branch_from, + 'branch-all-refs|B' => \$_branch_all_refs, + 'no-default-regex' => \$_no_default_regex, + 'no-graft-copy' => \$_no_graft_copy } ], + 'multi-init' => [ \&multi_init, + 'Initialize multiple trees (like git-svnimport)', + { %multi_opts, %init_opts, + 'revision|r=i' => \$_revision, + 'username=s' => \$_username, + 'config-dir=s' => \$_config_dir, + 'no-auth-cache' => \$_no_auth_cache, + 'prefix=s' => \$_prefix, + } ], + '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, + 'non-recursive' => \$_non_recursive, + 'authors-file|A=s' => \$_authors, + 'color' => \$_color, + 'pager=s' => \$_pager, + } ], + 'commit-diff' => [ \&commit_diff, 'Commit a diff between two trees', + { 'message|m=s' => \$_message, + 'file|F=s' => \$_file, + 'revision|r=s' => \$_revision, + %cmt_opts } ], +); + +my $cmd; +for (my $i = 0; $i < @ARGV; $i++) { + if (defined $cmd{$ARGV[$i]}) { + $cmd = $ARGV[$i]; + splice @ARGV, $i, 1; + last; + } +}; + +my %opts = %{$cmd{$cmd}->[2]} if (defined $cmd); + +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; +migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init|commit-diff)$/; +$cmd{$cmd}->[0]->(@ARGV); +exit 0; + +####################### primary functions ###################### +sub usage { + my $exit = shift || 0; + my $fd = $exit ? \*STDERR : \*STDOUT; + print $fd <<""; +git-svn - bidirectional operations between a single Subversion tree and git +Usage: $0 <command> [options] [arguments]\n + + print $fd "Available commands:\n" unless $cmd; + + foreach (sort keys %cmd) { + next if $cmd && $cmd ne $_; + print $fd ' ',pack('A17',$_),$cmd{$_}->[1],"\n"; + foreach (keys %{$cmd{$_}->[2]}) { + # prints out arguments as they should be passed: + my $x = s#[:=]s$## ? '<arg>' : s#[:=]i$## ? '<num>' : ''; + print $fd ' ' x 21, join(', ', map { length $_ > 1 ? + "--$_" : "-$_" } + split /\|/,$_)," $x\n"; + } + } + print $fd <<""; +\nGIT_SVN_ID may be set in the environment or via the --id/-i switch to an +arbitrary identifier if you're tracking multiple SVN branches/repositories in +one git repository and want to keep them separate. See git-svn(1) for more +information. + + exit $exit; +} + +sub version { + print "git-svn version $VERSION (svn $SVN::Core::VERSION)\n"; + exit 0; +} + +sub rebuild { + if (!verify_ref("refs/remotes/$GIT_SVN^0")) { + copy_remote_ref(); + } + $SVN_URL = shift or undef; + my $newest_rev = 0; + if ($_upgrade) { + command_noisy('update-ref',"refs/remotes/$GIT_SVN"," + $GIT_SVN-HEAD"); + } else { + check_upgrade_needed(); + } + + my ($rev_list, $ctx) = command_output_pipe("rev-list", + "refs/remotes/$GIT_SVN"); + my $latest; + while (<$rev_list>) { + chomp; + my $c = $_; + croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o; + my @commit = grep(/^git-svn-id: /, + command(qw/cat-file commit/, $c)); + next if (!@commit); # skip merges + my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]); + if (!defined $rev || !$uuid) { + croak "Unable to extract revision or UUID from ", + "$c, $commit[$#commit]\n"; + } + + # if we merged or otherwise started elsewhere, this is + # how we break out of it + next if (defined $SVN_UUID && ($uuid ne $SVN_UUID)); + next if (defined $SVN_URL && defined $url && ($url ne $SVN_URL)); + + unless (defined $latest) { + if (!$SVN_URL && !$url) { + croak "SVN repository location required: $url\n"; + } + $SVN_URL ||= $url; + $SVN_UUID ||= $uuid; + setup_git_svn(); + $latest = $rev; + } + revdb_set($REVDB, $rev, $c); + print "r$rev = $c\n"; + $newest_rev = $rev if ($rev > $newest_rev); + } + command_close_pipe($rev_list, $ctx); +} + +sub init { + my $url = shift or die "SVN repository location required " . + "as a command-line argument\n"; + $url =~ s!/+$!!; # strip trailing slash + + if (my $repo_path = shift) { + unless (-d $repo_path) { + mkpath([$repo_path]); + } + $GIT_DIR = $ENV{GIT_DIR} = $repo_path . "/.git"; + init_vars(); + } + + $SVN_URL = $url; + unless (-d $GIT_DIR) { + my @init_db = ('init'); + push @init_db, "--template=$_template" if defined $_template; + push @init_db, "--shared" if defined $_shared; + command_noisy(@init_db); + } + setup_git_svn(); +} + +sub cmd_fetch { + fetch_child_id($GIT_SVN, @_); +} + +sub fetch { + check_upgrade_needed(); + $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); + my $ret = fetch_lib(@_); + if ($ret->{commit} && !verify_ref('refs/heads/master^0')) { + command_noisy(qw(update-ref refs/heads/master),$ret->{commit}); + } + return $ret; +} + +sub fetch_lib { + my (@parents) = @_; + $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); + $SVN ||= libsvn_connect($SVN_URL); + 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) { + command_noisy('read-tree', $last_commit); + } + my $x = command_oneline('write-tree'); + my ($y) = (command(qw/cat-file commit/, $last_commit) + =~ /^tree ($sha1)/m); + if ($y ne $x) { + unlink $GIT_SVN_INDEX or croak $!; + command_noisy('read-tree', $last_commit); + } + $x = command_oneline('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. + libsvn_get_log(libsvn_dup_ra($SVN), [''], + $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); + $SVN = libsvn_connect($SVN_URL); + } + restore_index($index); + return { revision => $last_rev, commit => $last_commit }; +} + +sub commit { + my (@commits) = @_; + check_upgrade_needed(); + if ($_stdin || !@commits) { + print "Reading from stdin...\n"; + @commits = (); + while (<STDIN>) { + if (/\b($sha1_short)\b/o) { + unshift @commits, $1; + } + } + } + my @revs; + foreach my $c (@commits) { + my @tmp = command('rev-parse',$c); + if (scalar @tmp == 1) { + push @revs, $tmp[0]; + } elsif (scalar @tmp > 1) { + push @revs, reverse(command('rev-list',@tmp)); + } else { + die "Failed to rev-parse $c\n"; + } + } + commit_lib(@revs); + 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.$$"; + + my $repo; + set_svn_commit_env(); + foreach my $c (@revs) { + my $log_msg = get_commit_message($c, $commit_msg); + + # 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) { + my $ed = SVN::Git::Editor->new( + { r => $r_last, + ra => libsvn_dup_ra($SVN), + c => $c, + svn_path => $SVN->{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 exit 1; + 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); + } + } + $ENV{LC_ALL} = 'C'; + unlink $commit_msg; +} + +sub dcommit { + my $head = shift || 'HEAD'; + my $gs = "refs/remotes/$GIT_SVN"; + my @refs = command(qw/rev-list --no-merges/, "$gs..$head"); + my $last_rev; + foreach my $d (reverse @refs) { + if (!verify_ref("$d~1")) { + die "Commit $d\n", + "has no parent commit, and therefore ", + "nothing to diff against.\n", + "You should be working from a repository ", + "originally created by git-svn\n"; + } + unless (defined $last_rev) { + (undef, $last_rev, undef) = cmt_metadata("$d~1"); + unless (defined $last_rev) { + die "Unable to extract revision information ", + "from commit $d~1\n"; + } + } + if ($_dry_run) { + print "diff-tree $d~1 $d\n"; + } else { + if (my $r = commit_diff("$d~1", $d, undef, $last_rev)) { + $last_rev = $r; + } # else: no changes, same $last_rev + } + } + return if $_dry_run; + fetch(); + my @diff = command('diff-tree', 'HEAD', $gs, '--'); + my @finish; + if (@diff) { + @finish = qw/rebase/; + push @finish, qw/--merge/ if $_merge; + push @finish, "--strategy=$_strategy" if $_strategy; + print STDERR "W: HEAD and $gs differ, using @finish:\n", @diff; + } else { + print "No changes between current HEAD and $gs\n", + "Resetting to the latest $gs\n"; + @finish = qw/reset --mixed/; + } + command_noisy(@finish, $gs); +} + +sub show_ignore { + $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); + my $repo; + $SVN ||= libsvn_connect($SVN_URL); + my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum; + libsvn_traverse_ignore(\*STDOUT, '', $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 = command(qw/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+with\s+([\w\.\-]+)/i, + qr/\b(?:merge|merging|merged)\s+([\w\.\-]+)/i, + qr/\b(?:from|of)\s+([\w\.\-]+)/i ); + } + foreach my $u (keys %$l_map) { + if (@re) { + foreach my $p (keys %{$l_map->{$u}}) { + graft_merge_msg($grafts,$l_map,$u,$p,@re); + } + } + unless ($_no_graft_copy) { + graft_file_copy_lib($grafts,$l_map,$u); + } + } + graft_tree_joins($grafts); + + write_grafts($grafts, $comments, $gr_file); + unlink "$gr_file~$gr_sha1" if $gr_sha1; +} + +sub multi_init { + my $url = shift; + unless (defined $_trunk || defined $_branches || defined $_tags) { + usage(1); + } + if (defined $_trunk) { + my $trunk_url = complete_svn_url($url, $_trunk); + my $ch_id; + if ($GIT_SVN eq 'git-svn') { + $ch_id = 1; + $GIT_SVN = $ENV{GIT_SVN_ID} = 'trunk'; + } + init_vars(); + unless (-d $GIT_SVN_DIR) { + if ($ch_id) { + print "GIT_SVN_ID set to 'trunk' for ", + "$trunk_url ($_trunk)\n"; + } + init($trunk_url); + command_noisy('config', 'svn.trunk', $trunk_url); + } + } + $_prefix = '' unless defined $_prefix; + complete_url_ls_init($url, $_branches, '--branches/-b', $_prefix); + complete_url_ls_init($url, $_tags, '--tags/-t', $_prefix . '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; + } + } + + config_pager(); + @args = (git_svn_log_cmd($r_min, $r_max), @args); + my $log = command_output_pipe(@args); + run_pager(); + my (@k, $c, $d); + + while (<$log>) { + if (/^${_esc_color}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 (/^${_esc_color}author (.+) (\d+) ([\-\+]?\d+)$/) { + get_author_info($c, $1, $2, $3); + } elsif (/^${_esc_color}(?:tree|parent|committer) /) { + # ignore + } elsif (/^${_esc_color}:\d{6} \d{6} $sha1_short/o) { + push @{$c->{raw}}, $_; + } elsif (/^${_esc_color}[ACRMDT]\t/) { + # we could add $SVN->{svn_path} here, but that requires + # remote access at the moment (repo_path_split)... + s#^(${_esc_color})([ACRMDT])\t#$1 $2 #; + push @{$c->{changed}}, $_; + } elsif (/^${_esc_color}diff /) { + $d = 1; + push @{$c->{diff}}, $_; + } elsif ($d) { + push @{$c->{diff}}, $_; + } elsif (/^${_esc_color} (git-svn-id:.+)$/) { + ($c->{url}, $c->{r}, undef) = extract_metadata($1); + } elsif (s/^${_esc_color} //) { + 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; +} + +sub commit_diff_usage { + print STDERR "Usage: $0 commit-diff <tree-ish> <tree-ish> [<URL>]\n"; + exit 1 +} + +sub commit_diff { + my $ta = shift or commit_diff_usage(); + my $tb = shift or commit_diff_usage(); + if (!eval { $SVN_URL = shift || file_to_s("$GIT_SVN_DIR/info/url") }) { + print STDERR "Needed URL or usable git-svn id command-line\n"; + commit_diff_usage(); + } + my $r = shift; + unless (defined $r) { + if (defined $_revision) { + $r = $_revision + } else { + die "-r|--revision is a required argument\n"; + } + } + if (defined $_message && defined $_file) { + print STDERR "Both --message/-m and --file/-F specified ", + "for the commit message.\n", + "I have no idea what you mean\n"; + exit 1; + } + if (defined $_file) { + $_message = file_to_s($_file); + } else { + $_message ||= get_commit_message($tb, + "$GIT_DIR/.svn-commit.tmp.$$")->{msg}; + } + $SVN ||= libsvn_connect($SVN_URL); + if ($r eq 'HEAD') { + $r = $SVN->get_latest_revnum; + } elsif ($r !~ /^\d+$/) { + die "revision argument: $r not understood by git-svn\n"; + } + my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef, 0) : (); + my $rev_committed; + my $ed = SVN::Git::Editor->new({ r => $r, + ra => libsvn_dup_ra($SVN), + c => $tb, + svn_path => $SVN->{svn_path} + }, + $SVN->get_commit_editor($_message, + sub { + $rev_committed = $_[0]; + print "Committed $_[0]\n"; + }, @lock) + ); + eval { + my $mods = libsvn_checkout_tree($ta, $tb, $ed); + if (@$mods == 0) { + print "No changes\n$ta == $tb\n"; + $ed->abort_edit; + } else { + $ed->close_edit; + } + }; + fatal "$@\n" if $@; + $_message = $_file = undef; + return $rev_committed; +} + +########################### 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 = command(qw/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 log_use_color { + return 1 if $_color; + my ($dc, $dcvar); + $dcvar = 'color.diff'; + $dc = `git-config --get $dcvar`; + if ($dc eq '') { + # nothing at all; fallback to "diff.color" + $dcvar = 'diff.color'; + $dc = `git-config --get $dcvar`; + } + chomp($dc); + if ($dc eq 'auto') { + my $pc; + $pc = `git-config --get color.pager`; + if ($pc eq '') { + # does not have it -- fallback to pager.color + $pc = `git-config --bool --get pager.color`; + } + else { + $pc = `git-config --bool --get color.pager`; + if ($?) { + $pc = 'false'; + } + } + chomp($pc); + if (-t *STDOUT || (defined $_pager && $pc eq 'true')) { + return ($ENV{TERM} && $ENV{TERM} ne 'dumb'); + } + return 0; + } + return 0 if $dc eq 'never'; + return 1 if $dc eq 'always'; + chomp($dc = `git-config --bool --get $dcvar`); + return ($dc eq 'true'); +} + +sub git_svn_log_cmd { + my ($r_min, $r_max) = @_; + my @cmd = (qw/log --abbrev-commit --pretty=raw + --default/, "refs/remotes/$GIT_SVN"); + push @cmd, '-r' unless $_non_recursive; + push @cmd, qw/--raw --name-status/ if $_verbose; + push @cmd, '--color' if log_use_color(); + 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 (defined $c_min && defined $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"; + defined(my $pid = open my $fh, '-|') or croak $!; + if (!$pid) { + $GIT_SVN = $ENV{GIT_SVN_ID} = $id; + init_vars(); + fetch(@_); + exit 0; + } + while (<$fh>) { + print $_; + check_repack() if (/^r\d+ = $sha1/o); + } + close $fh or croak $?; +} + +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_svn_url { + my ($url, $path) = @_; + $path =~ s#/+$##; + $url =~ s#/+$## if $url; + if ($path !~ m#^[a-z\+]+://#) { + $path = '/' . $path if ($path !~ m#^/#); + if (!defined $url || $url !~ m#^[a-z\+]+://#) { + fatal("E: '$path' is not a complete URL ", + "and a separate URL is not specified\n"); + } + $path = $url . $path; + } + return $path; +} + +sub complete_url_ls_init { + my ($url, $path, $switch, $pfx) = @_; + unless ($path) { + print STDERR "W: $switch not specified\n"; + return; + } + my $full_url = complete_svn_url($url, $path); + my @ls = libsvn_ls_fullurl($full_url); + defined(my $pid = fork) or croak $!; + if (!$pid) { + foreach my $u (map { "$full_url/$_" } (grep m!/$!, @ls)) { + $u =~ s#/+$##; + if ($u !~ m!\Q$full_url\E/(.+)$!) { + print STDERR "W: Unrecognized URL: $u\n"; + die "This should never happen\n"; + } + # don't try to init already existing refs + my $id = $pfx.$1; + $GIT_SVN = $ENV{GIT_SVN_ID} = $id; + init_vars(); + unless (-d $GIT_SVN_DIR) { + print "init $u => $id\n"; + init($u); + } + } + exit 0; + } + waitpid $pid, 0; + croak $? if $?; + my ($n) = ($switch =~ /^--(\w+)/); + command_noisy('config', "svn.$n", $full_url); +} + +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 ''; +} + +# grafts set here are 'stronger' in that they're based on actual tree +# matches, and won't be deleted from merge-base checking in write_grafts() +sub graft_tree_joins { + my $grafts = shift; + map_tree_joins() if (@_branch_from && !%tree_map); + return unless %tree_map; + + git_svn_each(sub { + my $i = shift; + my @args = (qw/rev-list --pretty=raw/, "refs/remotes/$i"); + my ($fh, $ctx) = command_output_pipe(@args); + while (<$fh>) { + next unless /^commit ($sha1)$/o; + my $c = $1; + my ($t) = (<$fh> =~ /^tree ($sha1)$/o); + next unless $tree_map{$t}; + + my $l; + do { + $l = readline $fh; + } until ($l =~ /^committer (?:.+) (\d+) ([\-\+]?\d+)$/); + + my ($s, $tz) = ($1, $2); + if ($tz =~ s/^\+//) { + $s += tz_to_s_offset($tz); + } elsif ($tz =~ s/^\-//) { + $s -= tz_to_s_offset($tz); + } + + my ($url_a, $r_a, $uuid_a) = cmt_metadata($c); + + foreach my $p (@{$tree_map{$t}}) { + next if $p eq $c; + my $mb = eval { command('merge-base', $c, $p) }; + next unless ($@ || $?); + if (defined $r_a) { + # see if SVN says it's a relative + my ($url_b, $r_b, $uuid_b) = + cmt_metadata($p); + next if (defined $url_b && + defined $url_a && + ($url_a eq $url_b) && + ($uuid_a eq $uuid_b)); + if ($uuid_a eq $uuid_b) { + if ($r_b < $r_a) { + $grafts->{$c}->{$p} = 2; + next; + } elsif ($r_b > $r_a) { + $grafts->{$p}->{$c} = 2; + next; + } + } + } + my $ct = get_commit_time($p); + if ($ct < $s) { + $grafts->{$c}->{$p} = 2; + } elsif ($ct > $s) { + $grafts->{$p}->{$c} = 2; + } + # what should we do when $ct == $s ? + } + } + command_close_pipe($fh, $ctx); + }); +} + +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 = 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; + libsvn_get_log(libsvn_dup_ra($SVN), [$path], + $min, $max, 0, 2, 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, $l_map->{$u}->{$_}; + last; + } + } + last if @strong; + $w = basename($w); + $re = qr/\Q$w\E/i; + foreach (keys %{$l_map->{$u}}) { + if (/$re/) { + push @strong, $l_map->{$u}->{$_}; + 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, 1); + $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("refs/remotes/$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 $pool = SVN::Pool->new; + $SVN_UUID = $SVN->get_uuid($pool); + $pool->clear; +} + +sub verify_ref { + my ($ref) = @_; + eval { command_oneline([ 'rev-parse', '--verify', $ref ], + { STDERR => 0 }); }; +} + +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 $tmp = libsvn_connect($full_url); + return ($tmp->{repos_root}, $tmp->{svn_path}); +} + +sub setup_git_svn { + defined $SVN_URL or croak "SVN repository location required\n"; + unless (-d $GIT_DIR) { + croak "GIT_DIR=$GIT_DIR does not exist!\n"; + } + 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"); + +} + +sub get_tree_from_treeish { + my ($treeish) = @_; + croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o; + my $type = command_oneline(qw/cat-file -t/, $treeish); + my $expected; + while ($type eq 'tag') { + ($treeish, $type) = command(qw/cat-file tag/, $treeish); + } + if ($type eq 'commit') { + $expected = (grep /^tree /, command(qw/cat-file commit/, + $treeish))[0]; + ($expected) = ($expected =~ /^tree ($sha1)$/); + die "Unable to get tree from $treeish\n" unless $expected; + } elsif ($type eq 'tree') { + $expected = $treeish; + } else { + die "$treeish is a $type, expected tree, tag or commit\n"; + } + return $expected; +} + +sub get_diff { + my ($from, $treeish) = @_; + print "diff-tree $from $treeish\n"; + my @diff_tree = qw(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; + push @diff_tree, $from, $treeish; + my ($diff_fh, $ctx) = command_output_pipe(@diff_tree); + local $/ = "\0"; + my $state = 'meta'; + my @mods; + while (<$diff_fh>) { + chomp $_; # this gets rid of the trailing "\0" + if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s + $sha1\s($sha1)\s([MTCRAD])\d*$/xo) { + push @mods, { mode_a => $1, mode_b => $2, + sha1_b => $3, chg => $4 }; + if ($4 =~ /^(?:C|R)$/) { + $state = 'file_a'; + } else { + $state = 'file_b'; + } + } elsif ($state eq 'file_a') { + my $x = $mods[$#mods] or croak "Empty array\n"; + if ($x->{chg} !~ /^(?:C|R)$/) { + croak "Error parsing $_, $x->{chg}\n"; + } + $x->{file_a} = $_; + $state = 'file_b'; + } elsif ($state eq 'file_b') { + my $x = $mods[$#mods] or croak "Empty array\n"; + if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) { + croak "Error parsing $_, $x->{chg}\n"; + } + if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) { + croak "Error parsing $_, $x->{chg}\n"; + } + $x->{file_b} = $_; + $state = 'meta'; + } else { + croak "Error parsing $_\n"; + } + } + command_close_pipe($diff_fh, $ctx); + 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, $_q); + } else { + croak "Invalid change type: $f\n"; + } + } + $ed->rmdirs($_q) if $_rmdir; + return $mods; +} + +sub get_commit_message { + my ($commit, $commit_msg) = (@_); + my %log_msg = ( msg => '' ); + open my $msg, '>', $commit_msg or croak $!; + + my $type = command_oneline(qw/cat-file -t/, $commit); + if ($type eq 'commit' || $type eq 'tag') { + my ($msg_fh, $ctx) = command_output_pipe('cat-file', + $type, $commit); + my $in_msg = 0; + while (<$msg_fh>) { + if (!$in_msg) { + $in_msg = 1 if (/^\s*$/); + } elsif (/^git-svn-id: /) { + # skip this, we regenerate the correct one + # on re-fetch anyways + } else { + print $msg $_ or croak $!; + } + } + command_close_pipe($msg_fh, $ctx); + } + close $msg or croak $!; + + if ($_edit || ($type eq 'tree')) { + my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi'; + system($editor, $commit_msg); + } + + # file_to_s removes all trailing newlines, so just use chomp() here: + open $msg, '<', $commit_msg or croak $!; + { local $/; chomp($log_msg{msg} = <$msg>); } + close $msg or croak $!; + + return \%log_msg; +} + +sub set_svn_commit_env { + if (defined $LC_ALL) { + $ENV{LC_ALL} = $LC_ALL; + } else { + delete $ENV{LC_ALL}; + } +} + +sub rev_list_raw { + my ($fh, $c) = command_output_pipe(qw/rev-list --pretty=raw/, @_); + return { fh => $fh, ctx => $c, 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} .= $_; + } + } + command_close_pipe($fh, $rl->{ctx}); + return ($x != $rl->{t}) ? $x : undef; +} + +sub s_to_file { + my ($str, $file, $mode) = @_; + open my $fd,'>',$file or croak $!; + print $fd $str,"\n" or croak $!; + close $fd or croak $!; + chmod ($mode &~ umask, $file) if (defined $mode); +} + +sub file_to_s { + my $file = shift; + open my $fd,'<',$file or croak "$!: file: $file\n"; + local $/; + my $ret = <$fd>; + close $fd or croak $!; + $ret =~ s/\s*$//s; + return $ret; +} + +sub assert_revision_unknown { + my $r = shift; + if (my $c = revdb_get($REVDB, $r)) { + croak "$r = $c already exists! Why are we refetching it?"; + } +} + +sub git_commit { + my ($log_msg, @parents) = @_; + assert_revision_unknown($log_msg->{revision}); + 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: + foreach my $p (@parents) { + next unless defined $p; + if ($p =~ /^(\d+)=($sha1_short)$/o) { + if ($1 == $log_msg->{revision}) { + push @tmp_parents, $2; + } + } else { + push @tmp_parents, $p if $p =~ /$sha1_short/o; + } + } + my $tree = $log_msg->{tree}; + if (!defined $tree) { + my $index = set_index($GIT_SVN_INDEX); + $tree = command_oneline('write-tree'); + croak $? if $?; + restore_index($index); + } + # just in case we clobber the existing ref, we still want that ref + # as our parent: + if (my $cur = verify_ref("refs/remotes/$GIT_SVN^0")) { + chomp $cur; + push @tmp_parents, $cur; + } + + if (exists $tree_map{$tree}) { + foreach my $p (@{$tree_map{$tree}}) { + my $skip; + foreach (@tmp_parents) { + # see if a common parent is found + my $mb = eval { command('merge-base', $_, $p) }; + next if ($@ || $?); + $skip = 1; + last; + } + next if $skip; + my ($url_p, $r_p, $uuid_p) = cmt_metadata($p); + next if (($SVN_UUID eq $uuid_p) && + ($log_msg->{revision} > $r_p)); + next if (defined $url_p && defined $SVN_URL && + ($SVN_UUID eq $uuid_p) && + ($url_p eq $SVN_URL)); + push @tmp_parents, $p; + } + } + 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; + } + + set_commit_env($log_msg); + my @exec = ('git-commit-tree', $tree); + push @exec, '-p', $_ foreach @exec_parents; + defined(my $pid = open3(my $msg_fh, my $out_fh, '>&STDERR', @exec)) + or croak $!; + print $msg_fh $log_msg->{msg} or croak $!; + unless ($_no_metadata) { + print $msg_fh "\ngit-svn-id: $SVN_URL\@$log_msg->{revision}", + " $SVN_UUID\n" or croak $!; + } + $msg_fh->flush == 0 or croak $!; + close $msg_fh or croak $!; + chomp(my $commit = do { local $/; <$out_fh> }); + close $out_fh or croak $!; + waitpid $pid, 0; + croak $? if $?; + if ($commit !~ /^$sha1$/o) { + die "Failed to commit, invalid sha1: $commit\n"; + } + command_noisy('update-ref',"refs/remotes/$GIT_SVN",$commit); + revdb_set($REVDB, $log_msg->{revision}, $commit); + + # this output is read via pipe, do not change: + print "r$log_msg->{revision} = $commit\n"; + return $commit; +} + +sub check_repack { + if ($_repack && (--$_repack_nr == 0)) { + $_repack_nr = $_repack; + # repack doesn't use any arguments with spaces in them, does it? + command_noisy('repack', split(/\s+/, $_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; + $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email; + $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date}; +} + +sub check_upgrade_needed { + if (!-r $REVDB) { + -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]); + open my $fh, '>>',$REVDB or croak $!; + close $fh; + } + return unless eval { + command([qw/rev-parse --verify/,"$GIT_SVN-HEAD^0"], + {STDERR => 0}); + }; + my $head = eval { command('rev-parse',"refs/remotes/$GIT_SVN") }; + if ($@ || !$head) { + print STDERR "Please run: $0 rebuild --upgrade\n"; + exit 1; + } +} + +# 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 $pipe = command_output_pipe(qw/rev-list + --topo-order --pretty=raw/, $br); + 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; + } +} + +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... + @_branch_from = command(qw/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"; + while (<$authors>) { + chomp; + next unless /^(\S+?|\(no author\))\s*=\s*(.+?)\s*<(.+)>\s*$/; + my ($user, $name, $email) = ($1, $2, $3); + $users{$user} = [$name, $email]; + } + 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 git_svn_each { + my $sub = shift; + foreach (command(qw/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 (command(qw/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"; + %tree_map = (); +} + +# convert GetOpt::Long specs for use by git-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-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}; + my %x; # real parents + delete $p->{$c}; # commits are not self-reproducing... + my $ch = command_output_pipe(qw/cat-file commit/, $c); + while (<$ch>) { + if (/^parent ($sha1)/) { + $x{$1} = $p->{$1} = 1; + } else { + last unless /^\S/; + } + } + close $ch; # breaking the pipe + + # if real parents are the only ones in the grafts, drop it + next if join(' ',sort keys %$p) eq join(' ',sort keys %x); + + my (@ip, @jp, $mb); + my %del = %x; + @ip = @jp = keys %$p; + foreach my $i (@ip) { + next if $del{$i} || $p->{$i} == 2; + foreach my $j (@jp) { + next if $i eq $j || $del{$j} || $p->{$j} == 2; + $mb = eval { command('merge-base', $i, $j) }; + next unless $mb; + chomp $mb; + next if $x{$mb}; + if ($mb eq $j) { + delete $p->{$i}; + $del{$i} = 1; + } elsif ($mb eq $i) { + delete $p->{$j}; + $del{$j} = 1; + } + } + } + + # if real parents are the only ones in the grafts, drop it + next if join(' ',sort keys %$p) eq join(' ',sort keys %x); + + print $fh $c, ' ', join(' ', sort keys %$p),"\n"; + } + if ($comments->{'END'}) { + print $fh $_ foreach @{$comments->{'END'}}; + } + close $fh or croak $!; +} + +sub read_url_paths_all { + my ($l_map, $pfx, $p) = @_; + my @dir; + foreach (<$p/*>) { + if (-r "$_/info/url") { + $pfx .= '/' if $pfx && $pfx !~ m!/$!; + my $id = $pfx . basename $_; + my $url = file_to_s("$_/info/url"); + my ($u, $p) = repo_path_split($url); + $l_map->{$u}->{$p} = $id; + } elsif (-d $_) { + push @dir, $_; + } + } + foreach (@dir) { + my $x = $_; + $x =~ s!^\Q$GIT_DIR\E/svn/!!o; + read_url_paths_all($l_map, $x, $_); + } +} + +# this one only gets ids that have been imported, not new ones +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 or return (undef, undef, undef); + my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+) + \s([a-f\d\-]+)$/x); + if (!defined $rev || !$uuid || !$url) { + # some of the original repositories I made had + # identifiers like this: + ($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)\@([a-f\d\-]+)/); + } + return ($url, $rev, $uuid); +} + +sub cmt_metadata { + return extract_metadata((grep(/^git-svn-id: /, + command(qw/cat-file commit/, shift)))[-1]); +} + +sub get_commit_time { + my $cmt = shift; + my $fh = command_output_pipe(qw/rev-list --pretty=raw -n1/, $cmt); + while (<$fh>) { + /^committer\s(?:.+) (\d+) ([\-\+]?\d+)$/ or next; + my ($s, $tz) = ($1, $2); + if ($tz =~ s/^\+//) { + $s += tz_to_s_offset($tz); + } elsif ($tz =~ s/^\-//) { + $s -= tz_to_s_offset($tz); + } + close $fh; + return $s; + } + die "Can't get commit time for commit: $cmt\n"; +} + +sub tz_to_s_offset { + my ($tz) = @_; + $tz =~ s/(\d\d)$//; + return ($1 * 60) + ($tz * 3600); +} + +# adapted from pager.c +sub config_pager { + $_pager ||= $ENV{GIT_PAGER} || $ENV{PAGER}; + if (!defined $_pager) { + $_pager = 'less'; + } elsif (length $_pager == 0 || $_pager eq 'cat') { + $_pager = undef; + } +} + +sub run_pager { + return unless -t *STDOUT; + 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} ||= 'FRSX'; + exec $_pager or croak "Can't run pager: $! ($_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_changed_paths { + my ($c) = @_; + return unless $c->{changed}; + print "Changed paths:\n", @{$c->{changed}}; +} + +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 > 0 + && $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"; + show_commit_changed_paths($c); + print "\n"; + print $_ foreach @$l; + } + } else { + print "1 line\n"; + show_commit_changed_paths($c); + print "\n"; + + } + foreach my $x (qw/raw diff/) { + if ($c->{$x}) { + print "\n"; + print $_ foreach @{$c->{$x}} + } + } +} + +sub _simple_prompt { + my ($cred, $realm, $default_username, $may_save, $pool) = @_; + $may_save = undef if $_no_auth_cache; + $default_username = $_username if defined $_username; + if (defined $default_username && length $default_username) { + if (defined $realm && length $realm) { + print STDERR "Authentication realm: $realm\n"; + STDERR->flush; + } + $cred->username($default_username); + } else { + _username_prompt($cred, $realm, $may_save, $pool); + } + $cred->password(_read_password("Password for '" . + $cred->username . "': ", $realm)); + $cred->may_save($may_save); + $SVN::_Core::SVN_NO_ERROR; +} + +sub _ssl_server_trust_prompt { + my ($cred, $realm, $failures, $cert_info, $may_save, $pool) = @_; + $may_save = undef if $_no_auth_cache; + print STDERR "Error validating server certificate for '$realm':\n"; + if ($failures & $SVN::Auth::SSL::UNKNOWNCA) { + print STDERR " - The certificate is not issued by a trusted ", + "authority. Use the\n", + " fingerprint to validate the certificate manually!\n"; + } + if ($failures & $SVN::Auth::SSL::CNMISMATCH) { + print STDERR " - The certificate hostname does not match.\n"; + } + if ($failures & $SVN::Auth::SSL::NOTYETVALID) { + print STDERR " - The certificate is not yet valid.\n"; + } + if ($failures & $SVN::Auth::SSL::EXPIRED) { + print STDERR " - The certificate has expired.\n"; + } + if ($failures & $SVN::Auth::SSL::OTHER) { + print STDERR " - The certificate has an unknown error.\n"; + } + printf STDERR + "Certificate information:\n". + " - Hostname: %s\n". + " - Valid: from %s until %s\n". + " - Issuer: %s\n". + " - Fingerprint: %s\n", + map $cert_info->$_, qw(hostname valid_from valid_until + issuer_dname fingerprint); + my $choice; +prompt: + print STDERR $may_save ? + "(R)eject, accept (t)emporarily or accept (p)ermanently? " : + "(R)eject or accept (t)emporarily? "; + STDERR->flush; + $choice = lc(substr(<STDIN> || 'R', 0, 1)); + if ($choice =~ /^t$/i) { + $cred->may_save(undef); + } elsif ($choice =~ /^r$/i) { + return -1; + } elsif ($may_save && $choice =~ /^p$/i) { + $cred->may_save($may_save); + } else { + goto prompt; + } + $cred->accepted_failures($failures); + $SVN::_Core::SVN_NO_ERROR; +} + +sub _ssl_client_cert_prompt { + my ($cred, $realm, $may_save, $pool) = @_; + $may_save = undef if $_no_auth_cache; + print STDERR "Client certificate filename: "; + STDERR->flush; + chomp(my $filename = <STDIN>); + $cred->cert_file($filename); + $cred->may_save($may_save); + $SVN::_Core::SVN_NO_ERROR; +} + +sub _ssl_client_cert_pw_prompt { + my ($cred, $realm, $may_save, $pool) = @_; + $may_save = undef if $_no_auth_cache; + $cred->password(_read_password("Password: ", $realm)); + $cred->may_save($may_save); + $SVN::_Core::SVN_NO_ERROR; +} + +sub _username_prompt { + my ($cred, $realm, $may_save, $pool) = @_; + $may_save = undef if $_no_auth_cache; + if (defined $realm && length $realm) { + print STDERR "Authentication realm: $realm\n"; + } + my $username; + if (defined $_username) { + $username = $_username; + } else { + print STDERR "Username: "; + STDERR->flush; + chomp($username = <STDIN>); + } + $cred->username($username); + $cred->may_save($may_save); + $SVN::_Core::SVN_NO_ERROR; +} + +sub _read_password { + my ($prompt, $realm) = @_; + print STDERR $prompt; + STDERR->flush; + require Term::ReadKey; + Term::ReadKey::ReadMode('noecho'); + my $password = ''; + while (defined(my $key = Term::ReadKey::ReadKey(0))) { + last if $key =~ /[\012\015]/; # \n\r + $password .= $key; + } + Term::ReadKey::ReadMode('restore'); + print STDERR "\n"; + STDERR->flush; + $password; +} + +sub libsvn_connect { + my ($url) = @_; + SVN::_Core::svn_config_ensure($_config_dir, undef); + my ($baton, $callbacks) = SVN::Core::auth_open_helper([ + SVN::Client::get_simple_provider(), + SVN::Client::get_ssl_server_trust_file_provider(), + SVN::Client::get_simple_prompt_provider( + \&_simple_prompt, 2), + SVN::Client::get_ssl_client_cert_prompt_provider( + \&_ssl_client_cert_prompt, 2), + SVN::Client::get_ssl_client_cert_pw_prompt_provider( + \&_ssl_client_cert_pw_prompt, 2), + SVN::Client::get_username_provider(), + SVN::Client::get_ssl_server_trust_prompt_provider( + \&_ssl_server_trust_prompt), + SVN::Client::get_username_prompt_provider( + \&_username_prompt, 2), + ]); + my $config = SVN::Core::config_get_config($_config_dir); + my $ra = SVN::Ra->new(url => $url, auth => $baton, + config => $config, + pool => SVN::Pool->new, + auth_provider_callbacks => $callbacks); + $ra->{svn_path} = $url; + $ra->{repos_root} = $ra->get_repos_root; + $ra->{svn_path} =~ s#^\Q$ra->{repos_root}\E/*##; + push @repo_path_split_cache, qr/^(\Q$ra->{repos_root}\E)/; + return $ra; +} + +sub libsvn_can_do_switch { + unless (defined $_svn_can_do_switch) { + my $pool = SVN::Pool->new; + my $rep = eval { + $SVN->do_switch(1, '', 0, $SVN->{url}, + SVN::Delta::Editor->new, $pool); + }; + if ($@) { + $_svn_can_do_switch = 0; + } else { + $rep->abort_report($pool); + $_svn_can_do_switch = 1; + } + $pool->clear; + } + $_svn_can_do_switch; +} + +sub libsvn_dup_ra { + my ($ra) = @_; + SVN::Ra->new(map { $_ => $ra->{$_} } qw/config url + auth auth_provider_callbacks repos_root svn_path/); +} + +sub uri_encode { + my ($f) = @_; + $f =~ s#([^a-zA-Z0-9\*!\:_\./\-])#uc sprintf("%%%02x",ord($1))#eg; + $f +} + +sub uri_decode { + my ($f) = @_; + $f =~ tr/+/ /; + $f =~ s/%([A-F0-9]{2})/chr hex($1)/ge; + $f +} + +sub libsvn_log_entry { + my ($rev, $author, $date, $msg, $parents, $untracked) = @_; + 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 $author && length $author > 0 && + defined $_authors && ! defined $users{$author}) { + die "Author: $author not defined in $_authors file\n"; + } + $msg = '' if ($rev == 0 && !defined $msg); + + open my $un, '>>', "$GIT_SVN_DIR/unhandled.log" or croak $!; + my $h; + print $un "r$rev\n" or croak $!; + $h = $untracked->{empty}; + foreach (sort keys %$h) { + my $act = $h->{$_} ? '+empty_dir' : '-empty_dir'; + print $un " $act: ", uri_encode($_), "\n" or croak $!; + warn "W: $act: $_\n"; + } + foreach my $t (qw/dir_prop file_prop/) { + $h = $untracked->{$t} or next; + foreach my $path (sort keys %$h) { + my $ppath = $path eq '' ? '.' : $path; + foreach my $prop (sort keys %{$h->{$path}}) { + next if $SKIP{$prop}; + my $v = $h->{$path}->{$prop}; + if (defined $v) { + print $un " +$t: ", + uri_encode($ppath), ' ', + uri_encode($prop), ' ', + uri_encode($v), "\n" + or croak $!; + } else { + print $un " -$t: ", + uri_encode($ppath), ' ', + uri_encode($prop), "\n" + or croak $!; + } + } + } + } + foreach my $t (qw/absent_file absent_directory/) { + $h = $untracked->{$t} or next; + foreach my $parent (sort keys %$h) { + foreach my $path (sort @{$h->{$parent}}) { + print $un " $t: ", + uri_encode("$parent/$path"), "\n" + or croak $!; + warn "W: $t: $parent/$path ", + "Insufficient permissions?\n"; + } + } + } + + # revprops (make this optional? it's an extra network trip...) + my $pool = SVN::Pool->new; + my $rp = $SVN->rev_proplist($rev, $pool); + foreach (sort keys %$rp) { + next if /^svn:(?:author|date|log)$/; + print $un " rev_prop: ", uri_encode($_), ' ', + uri_encode($rp->{$_}), "\n"; + } + $pool->clear; + close $un or croak $!; + + { revision => $rev, date => "+0000 $Y-$m-$d $H:$M:$S", + author => $author, msg => $msg."\n", parents => $parents || [], + revprops => $rp } +} + +sub process_rm { + my ($gui, $last_commit, $f, $q) = @_; + # remove entire directories. + if (command('ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) { + my ($ls, $ctx) = command_output_pipe(qw/ls-tree + -r --name-only -z/, + $last_commit,'--',$f); + local $/ = "\0"; + while (<$ls>) { + print $gui '0 ',0 x 40,"\t",$_ or croak $!; + print "\tD\t$_\n" unless $q; + } + print "\tD\t$f/\n" unless $q; + command_close_pipe($ls, $ctx); + return $SVN::Node::dir; + } else { + print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!; + print "\tD\t$f\n" unless $q; + return $SVN::Node::file; + } +} + +sub libsvn_fetch { + my ($last_commit, $paths, $rev, $author, $date, $msg) = @_; + my $pool = SVN::Pool->new; + my $ed = SVN::Git::Fetcher->new({ c => $last_commit, q => $_q }); + my $reporter = $SVN->do_update($rev, '', 1, $ed, $pool); + my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : (); + my (undef, $last_rev, undef) = cmt_metadata($last_commit); + $reporter->set_path('', $last_rev, 0, @lock, $pool); + $reporter->finish_report($pool); + $pool->clear; + unless ($ed->{git_commit_ok}) { + die "SVN connection failed somewhere...\n"; + } + libsvn_log_entry($rev, $author, $date, $msg, [$last_commit], $ed); +} + +sub svn_grab_base_rev { + my $c = eval { command_oneline([qw/rev-parse --verify/, + "refs/remotes/$GIT_SVN^0"], + { STDERR => 0 }) }; + if (defined $c && length $c) { + my ($url, $rev, $uuid) = cmt_metadata($c); + return ($rev, $c) if defined $rev; + } + if ($_no_metadata) { + my $offset = -41; # from tail + my $rl; + open my $fh, '<', $REVDB or + die "--no-metadata specified and $REVDB not readable\n"; + seek $fh, $offset, 2; + $rl = readline $fh; + defined $rl or return (undef, undef); + chomp $rl; + while ($c ne $rl && tell $fh != 0) { + $offset -= 41; + seek $fh, $offset, 2; + $rl = readline $fh; + defined $rl or return (undef, undef); + chomp $rl; + } + my $rev = tell $fh; + croak $! if ($rev < -1); + $rev = ($rev - 41) / 41; + close $fh or croak $!; + 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_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->{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; + # should be OK to use Pool here (r1 - r0) should be small + my $pool = SVN::Pool->new; + libsvn_get_log($SVN, [$path], $r0, $r1, + 0, 0, 1, sub {$nr++}, $pool); + $pool->clear; + return 0 if ($nr > 1); + return 1; +} + +sub libsvn_find_parent_branch { + my ($paths, $rev, $author, $date, $msg) = @_; + my $svn_path = '/'.$SVN->{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_all($l_map, '', "$GIT_DIR/svn"); + my $url = $SVN->{repos_root}; + defined $l_map->{$url} or return; + my $id = $l_map->{$url}->{$branch_from}; + if (!defined $id && $_follow_parent) { + print STDERR "Following parent: $branch_from\@$r\n"; + # auto create a new branch and follow it + $id = basename($branch_from); + $id .= '@'.$r if -r "$GIT_DIR/svn/$id"; + while (-r "$GIT_DIR/svn/$id") { + # just grow a tail if we're not unique enough :x + $id .= '-'; + } + } + return unless defined $id; + + my ($r0, $parent) = find_rev_before($r,$id,1); + if ($_follow_parent && (!defined $r0 || !defined $parent)) { + defined(my $pid = fork) or croak $!; + if (!$pid) { + $GIT_SVN = $ENV{GIT_SVN_ID} = $id; + init_vars(); + $SVN_URL = "$url/$branch_from"; + $SVN = undef; + setup_git_svn(); + # we can't assume SVN_URL exists at r+1: + $_revision = "0:$r"; + fetch_lib(); + exit 0; + } + waitpid $pid, 0; + croak $? if $?; + ($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: ($GIT_SVN) $parent\n"; + command_noisy('read-tree', $parent); + unless (libsvn_can_do_switch()) { + return _libsvn_new_tree($paths, $rev, $author, $date, + $msg, [$parent]); + } + # do_switch works with svn/trunk >= r22312, but that is not + # included with SVN 1.4.2 (the latest version at the moment), + # so we can't rely on it. + my $ra = libsvn_connect("$url/$branch_from"); + my $ed = SVN::Git::Fetcher->new({c => $parent, q => $_q }); + my $pool = SVN::Pool->new; + my $reporter = $ra->do_switch($rev, '', 1, $SVN->{url}, + $ed, $pool); + my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : (); + $reporter->set_path('', $r0, 0, @lock, $pool); + $reporter->finish_report($pool); + $pool->clear; + unless ($ed->{git_commit_ok}) { + die "SVN connection failed somewhere...\n"; + } + return libsvn_log_entry($rev, $author, $date, $msg, [$parent]); + } + print STDERR "Nope, branch point not imported or unknown\n"; + return undef; +} + +sub libsvn_get_log { + my ($ra, @args) = @_; + $args[4]-- if $args[4] && ! $_follow_parent; + if ($SVN::Core::VERSION le '1.2.0') { + splice(@args, 3, 1); + } + $ra->get_log(@args); +} + +sub libsvn_new_tree { + if (my $log_entry = libsvn_find_parent_branch(@_)) { + return $log_entry; + } + my ($paths, $rev, $author, $date, $msg) = @_; # $pool is last + _libsvn_new_tree($paths, $rev, $author, $date, $msg, []); +} + +sub _libsvn_new_tree { + my ($paths, $rev, $author, $date, $msg, $parents) = @_; + my $pool = SVN::Pool->new; + my $ed = SVN::Git::Fetcher->new({q => $_q}); + my $reporter = $SVN->do_update($rev, '', 1, $ed, $pool); + my @lock = $SVN::Core::VERSION ge '1.2.0' ? (undef) : (); + $reporter->set_path('', $rev, 1, @lock, $pool); + $reporter->finish_report($pool); + $pool->clear; + unless ($ed->{git_commit_ok}) { + die "SVN connection failed somewhere...\n"; + } + libsvn_log_entry($rev, $author, $date, $msg, $parents, $ed); +} + +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)) { + my ($url_b, undef, $uuid_b) = cmt_metadata($c); + my ($url_a, undef, $uuid_a) = cmt_metadata($parent); + next if ($url_a && $url_b && $url_a eq $url_b && + $uuid_b eq $uuid_a); + $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 = command('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 $ra = libsvn_connect($fullurl); + my @ret; + my $pool = SVN::Pool->new; + my $r = defined $_revision ? $_revision : $ra->get_latest_revnum; + my ($dirent, undef, undef) = $ra->get_dir('', $r, $pool); + foreach my $d (sort 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):// + # 175007 - http(s):// (this repo required authorization, too...) + # More codes may be discovered later... + if ($errno == 175007 || $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 (command('ls-remote', $origin, $ref)) { + command_noisy('fetch', $origin, "$ref:$ref"); + } elsif ($_cp_remote && !$_upgrade) { + die "Unable to find remote reference: ", + "refs/remotes/$GIT_SVN on $origin\n"; + } +} + +{ + 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. + $SVN::Auth::SSL::CNMISMATCH. + $SVN::Auth::SSL::NOTYETVALID. + $SVN::Auth::SSL::EXPIRED. + $SVN::Auth::SSL::UNKNOWNCA. + $SVN::Auth::SSL::OTHER; +} + +package SVN::Git::Fetcher; +use vars qw/@ISA/; +use strict; +use warnings; +use Carp qw/croak/; +use IO::File qw//; +use Git qw/command command_oneline command_noisy + command_output_pipe command_input_pipe command_close_pipe/; + +# file baton members: path, mode_a, mode_b, pool, fh, blob, base +sub new { + my ($class, $git_svn) = @_; + my $self = SVN::Delta::Editor->new; + bless $self, $class; + $self->{c} = $git_svn->{c} if exists $git_svn->{c}; + $self->{q} = $git_svn->{q}; + $self->{empty} = {}; + $self->{dir_prop} = {}; + $self->{file_prop} = {}; + $self->{absent_dir} = {}; + $self->{absent_file} = {}; + ($self->{gui}, $self->{ctx}) = command_input_pipe( + qw/update-index -z --index-info/); + require Digest::MD5; + $self; +} + +sub open_root { + { path => '' }; +} + +sub open_directory { + my ($self, $path, $pb, $rev) = @_; + { path => $path }; +} + +sub delete_entry { + my ($self, $path, $rev, $pb) = @_; + my $t = process_rm($self->{gui}, $self->{c}, $path, $self->{q}); + $self->{empty}->{$path} = 0 if $t == $SVN::Node::dir; + undef; +} + +sub open_file { + my ($self, $path, $pb, $rev) = @_; + my ($mode, $blob) = (command('ls-tree', $self->{c}, '--',$path) + =~ /^(\d{6}) blob ([a-f\d]{40})\t/); + unless (defined $mode && defined $blob) { + die "$path was not found in commit $self->{c} (r$rev)\n"; + } + { path => $path, mode_a => $mode, mode_b => $mode, blob => $blob, + pool => SVN::Pool->new, action => 'M' }; +} + +sub add_file { + my ($self, $path, $pb, $cp_path, $cp_rev) = @_; + my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#); + delete $self->{empty}->{$dir}; + { path => $path, mode_a => 100644, mode_b => 100644, + pool => SVN::Pool->new, action => 'A' }; +} + +sub add_directory { + my ($self, $path, $cp_path, $cp_rev) = @_; + my ($dir, $file) = ($path =~ m#^(.*?)/?([^/]+)$#); + delete $self->{empty}->{$dir}; + $self->{empty}->{$path} = 1; + { path => $path }; +} + +sub change_dir_prop { + my ($self, $db, $prop, $value) = @_; + $self->{dir_prop}->{$db->{path}} ||= {}; + $self->{dir_prop}->{$db->{path}}->{$prop} = $value; + undef; +} + +sub absent_directory { + my ($self, $path, $pb) = @_; + $self->{absent_dir}->{$pb->{path}} ||= []; + push @{$self->{absent_dir}->{$pb->{path}}}, $path; + undef; +} + +sub absent_file { + my ($self, $path, $pb) = @_; + $self->{absent_file}->{$pb->{path}} ||= []; + push @{$self->{absent_file}->{$pb->{path}}}, $path; + undef; +} + +sub change_file_prop { + my ($self, $fb, $prop, $value) = @_; + if ($prop eq 'svn:executable') { + if ($fb->{mode_b} != 120000) { + $fb->{mode_b} = defined $value ? 100755 : 100644; + } + } elsif ($prop eq 'svn:special') { + $fb->{mode_b} = defined $value ? 120000 : 100644; + } else { + $self->{file_prop}->{$fb->{path}} ||= {}; + $self->{file_prop}->{$fb->{path}}->{$prop} = $value; + } + undef; +} + +sub apply_textdelta { + my ($self, $fb, $exp) = @_; + my $fh = IO::File->new_tmpfile; + $fh->autoflush(1); + # $fh gets auto-closed() by SVN::TxDelta::apply(), + # (but $base does not,) so dup() it for reading in close_file + open my $dup, '<&', $fh or croak $!; + my $base = IO::File->new_tmpfile; + $base->autoflush(1); + if ($fb->{blob}) { + defined (my $pid = fork) or croak $!; + if (!$pid) { + open STDOUT, '>&', $base or croak $!; + print STDOUT 'link ' if ($fb->{mode_a} == 120000); + exec qw/git-cat-file blob/, $fb->{blob} or croak $!; + } + waitpid $pid, 0; + croak $? if $?; + + if (defined $exp) { + seek $base, 0, 0 or croak $!; + my $md5 = Digest::MD5->new; + $md5->addfile($base); + my $got = $md5->hexdigest; + die "Checksum mismatch: $fb->{path} $fb->{blob}\n", + "expected: $exp\n", + " got: $got\n" if ($got ne $exp); + } + } + seek $base, 0, 0 or croak $!; + $fb->{fh} = $dup; + $fb->{base} = $base; + [ SVN::TxDelta::apply($base, $fh, undef, $fb->{path}, $fb->{pool}) ]; +} + +sub close_file { + my ($self, $fb, $exp) = @_; + my $hash; + my $path = $fb->{path}; + if (my $fh = $fb->{fh}) { + seek($fh, 0, 0) or croak $!; + my $md5 = Digest::MD5->new; + $md5->addfile($fh); + my $got = $md5->hexdigest; + die "Checksum mismatch: $path\n", + "expected: $exp\n got: $got\n" if ($got ne $exp); + seek($fh, 0, 0) or croak $!; + if ($fb->{mode_b} == 120000) { + read($fh, my $buf, 5) == 5 or croak $!; + $buf eq 'link ' or die "$path has mode 120000", + "but is not a link\n"; + } + defined(my $pid = open my $out,'-|') or die "Can't fork: $!\n"; + if (!$pid) { + open STDIN, '<&', $fh or croak $!; + exec qw/git-hash-object -w --stdin/ or croak $!; + } + chomp($hash = do { local $/; <$out> }); + close $out or croak $!; + close $fh or croak $!; + $hash =~ /^[a-f\d]{40}$/ or die "not a sha1: $hash\n"; + close $fb->{base} or croak $!; + } else { + $hash = $fb->{blob} or die "no blob information\n"; + } + $fb->{pool}->clear; + my $gui = $self->{gui}; + print $gui "$fb->{mode_b} $hash\t$path\0" or croak $!; + print "\t$fb->{action}\t$path\n" if $fb->{action} && ! $self->{q}; + undef; +} + +sub abort_edit { + my $self = shift; + eval { command_close_pipe($self->{gui}, $self->{ctx}) }; + $self->SUPER::abort_edit(@_); +} + +sub close_edit { + my $self = shift; + command_close_pipe($self->{gui}, $self->{ctx}); + $self->{git_commit_ok} = 1; + $self->SUPER::close_edit(@_); +} + +package SVN::Git::Editor; +use vars qw/@ISA/; +use strict; +use warnings; +use Carp qw/croak/; +use IO::File; +use Git qw/command command_oneline command_noisy + command_output_pipe command_input_pipe command_close_pipe/; + +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]) ? $_[1] : '' +} + +sub url_path { + my ($self, $path) = @_; + $self->{ra}->{url} . '/' . $self->repo_path($path); +} + +sub rmdirs { + my ($self, $q) = @_; + 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; + + my ($fh, $ctx) = command_output_pipe( + qw/ls-tree --name-only -r -z/, $self->{c}); + local $/ = "\0"; + while (<$fh>) { + chomp; + my @dn = split m#/#, $_; + while (pop @dn) { + delete $rm->{join '/', @dn}; + } + unless (%$rm) { + close $fh; + return; + } + } + command_close_pipe($fh, $ctx); + + 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#^(.*?)/?(?:[^/]+)$#); + print "\tD+\t$d/\n" unless $q; + $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, $q) = @_; + 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); + print "\tA\t$m->{file_b}\n" unless $q; + $self->chg_file($fbat, $m); + $self->close_file($fbat,undef,$self->{pool}); +} + +sub C { + my ($self, $m, $q) = @_; + 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}); + print "\tC\t$m->{file_a} => $m->{file_b}\n" unless $q; + $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, $q) = @_; + 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}); + print "\tR\t$m->{file_a} => $m->{file_b}\n" unless $q; + $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, $q) = @_; + 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}); + print "\t$m->{chg}\t$m->{file_b}\n" unless $q; + $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 $pool = SVN::Pool->new; + my $atd = $self->apply_textdelta($fbat, undef, $pool); + my $got = SVN::TxDelta::send_stream($fh, @$atd, $pool); + die "Checksum mismatch\nexpected: $exp\ngot: $got\n" if ($got ne $exp); + $pool->clear; + + close $fh or croak $!; +} + +sub D { + my ($self, $m, $q) = @_; + my ($dir, $file) = split_path($m->{file_b}); + my $pbat = $self->ensure_path($dir); + print "\tD\t$m->{file_b}\n" unless $q; + $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: + +$log_msg hashref as returned by libsvn_log_entry() +{ + msg => 'whitespace-formatted log entry +', # trailing newline is preserved + revision => '8', # integer + date => '2004-02-24T17:01:44.108345Z', # commit date + author => 'committer name' +}; + +@mods = array of diff-index line hashes, each element represents one line + of diff-index output + +diff-index line ($m hash) +{ + mode_a => first column of diff-index output, no leading ':', + mode_b => second column of diff-index output, + sha1_b => sha1sum of the final blob, + chg => change type [MCRADT], + file_a => original file name of a file (iff chg is 'C' or 'R') + file_b => new/current file name of a file (any chg) +} +; + +# retval of read_url_paths{,_all}(); +$l_map = { + # repository root url + 'https://svn.musicpd.org' => { + # repository path # GIT_SVN_ID + 'mpd/trunk' => 'trunk', + 'mpd/tags/0.11.5' => 'tags/0.11.5', + }, +} + +Notes: + I don't trust the each() function on unless I created %hash myself + because the internal iterator may not have started at base. diff --git a/git-svnimport.perl b/git-svnimport.perl new file mode 100755 index 0000000000..3af8c7e110 --- /dev/null +++ b/git-svnimport.perl @@ -0,0 +1,995 @@ +#!/usr/bin/perl -w + +# This tool is copyright (c) 2005, Matthias Urlichs. +# It is released under the Gnu Public License, version 2. +# +# The basic idea is to pull and analyze SVN changes. +# +# Checking out the files is done by a single long-running SVN connection. +# +# The head revision is on branch "origin" by default. +# You can change that with the '-o' option. + +use strict; +use warnings; +use Getopt::Std; +use File::Copy; +use File::Spec; +use File::Temp qw(tempfile); +use File::Path qw(mkpath); +use File::Basename qw(basename dirname); +use Time::Local; +use IO::Pipe; +use POSIX qw(strftime dup2); +use IPC::Open2; +use SVN::Core; +use SVN::Ra; + +die "Need SVN:Core 1.2.1 or better" if $SVN::Core::VERSION lt "1.2.1"; + +$SIG{'PIPE'}="IGNORE"; +$ENV{'TZ'}="UTC"; + +our($opt_h,$opt_o,$opt_v,$opt_u,$opt_C,$opt_i,$opt_m,$opt_M,$opt_t,$opt_T, + $opt_b,$opt_r,$opt_I,$opt_A,$opt_s,$opt_l,$opt_d,$opt_D,$opt_S,$opt_F, + $opt_P,$opt_R); + +sub usage() { + print STDERR <<END; +Usage: ${\basename $0} # fetch/update GIT from SVN + [-o branch-for-HEAD] [-h] [-v] [-l max_rev] [-R repack_each_revs] + [-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname] + [-d|-D] [-i] [-u] [-r] [-I ignorefilename] [-s start_chg] + [-m] [-M regex] [-A author_file] [-S] [-F] [-P project_name] [SVN_URL] +END + exit(1); +} + +getopts("A:b:C:dDFhiI:l:mM:o:rs:t:T:SP:R:uv") or usage(); +usage if $opt_h; + +my $tag_name = $opt_t || "tags"; +my $trunk_name = $opt_T || "trunk"; +my $branch_name = $opt_b || "branches"; +my $project_name = $opt_P || ""; +$project_name = "/" . $project_name if ($project_name); +my $repack_after = $opt_R || 1000; + +@ARGV == 1 or @ARGV == 2 or usage(); + +$opt_o ||= "origin"; +$opt_s ||= 1; +my $git_tree = $opt_C; +$git_tree ||= "."; + +my $svn_url = $ARGV[0]; +my $svn_dir = $ARGV[1]; + +our @mergerx = (); +if ($opt_m) { + 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) { + unshift (@mergerx, qr/$opt_M/); +} + +# Absolutize filename now, since we will have chdir'ed by the time we +# get around to opening it. +$opt_A = File::Spec->rel2abs($opt_A) if $opt_A; + +our %users = (); +our $users_file = undef; +sub read_users($) { + $users_file = File::Spec->rel2abs(@_); + die "Cannot open $users_file\n" unless -f $users_file; + open(my $authors,$users_file); + while(<$authors>) { + chomp; + next unless /^(\S+?)\s*=\s*(.+?)\s*<(.+)>\s*$/; + (my $user,my $name,my $email) = ($1,$2,$3); + $users{$user} = [$name,$email]; + } + close($authors); +} + +select(STDERR); $|=1; select(STDOUT); + + +package SVNconn; +# Basic SVN connection. +# We're only interested in connecting and downloading, so ... + +use File::Spec; +use File::Temp qw(tempfile); +use POSIX qw(strftime dup2); +use Fcntl qw(SEEK_SET); + +sub new { + my($what,$repo) = @_; + $what=ref($what) if ref($what); + + my $self = {}; + $self->{'buffer'} = ""; + bless($self,$what); + + $repo =~ s#/+$##; + $self->{'fullrep'} = $repo; + $self->conn(); + + return $self; +} + +sub conn { + my $self = shift; + my $repo = $self->{'fullrep'}; + 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 = SVN::Ra->new(url => $repo, auth => $auth); + die "SVN connection to $repo: $!\n" unless defined $s; + $self->{'svn'} = $s; + $self->{'repo'} = $repo; + $self->{'maxrev'} = $s->get_latest_revnum(); +} + +sub file { + my($self,$path,$rev) = @_; + + my ($fh, $name) = tempfile('gitsvn.XXXXXX', + DIR => File::Spec->tmpdir(), UNLINK => 1); + + print "... $rev $path ...\n" if $opt_v; + my (undef, $properties); + my $pool = SVN::Pool->new(); + $path =~ s#^/*##; + eval { (undef, $properties) + = $self->{'svn'}->get_file($path,$rev,$fh,$pool); }; + $pool->clear; + if($@) { + return undef if $@ =~ /Attempted to get checksum/; + die $@; + } + my $mode; + if (exists $properties->{'svn:executable'}) { + $mode = '100755'; + } elsif (exists $properties->{'svn:special'}) { + my ($special_content, $filesize); + $filesize = tell $fh; + seek $fh, 0, SEEK_SET; + read $fh, $special_content, $filesize; + if ($special_content =~ s/^link //) { + $mode = '120000'; + seek $fh, 0, SEEK_SET; + truncate $fh, 0; + print $fh $special_content; + } else { + die "unexpected svn:special file encountered"; + } + } else { + $mode = '100644'; + } + close ($fh); + + return ($name, $mode); +} + +sub ignore { + my($self,$path,$rev) = @_; + + print "... $rev $path ...\n" if $opt_v; + $path =~ s#^/*##; + my (undef,undef,$properties) + = $self->{'svn'}->get_dir($path,$rev,undef); + if (exists $properties->{'svn:ignore'}) { + my ($fh, $name) = tempfile('gitsvn.XXXXXX', + DIR => File::Spec->tmpdir(), + UNLINK => 1); + print $fh $properties->{'svn:ignore'}; + close($fh); + return $name; + } else { + return undef; + } +} + +sub dir_list { + my($self,$path,$rev) = @_; + $path =~ s#^/*##; + my ($dirents,undef,$properties) + = $self->{'svn'}->get_dir($path,$rev,undef); + return $dirents; +} + +package main; +use URI; + +our $svn = $svn_url; +$svn .= "/$svn_dir" if defined $svn_dir; +my $svn2 = SVNconn->new($svn); +$svn = SVNconn->new($svn); + +my $lwp_ua; +if($opt_d or $opt_D) { + $svn_url = URI->new($svn_url)->canonical; + if($opt_D) { + $svn_dir =~ s#/*$#/#; + } else { + $svn_dir = ""; + } + if ($svn_url->scheme eq "http") { + use LWP::UserAgent; + $lwp_ua = LWP::UserAgent->new(keep_alive => 1, requests_redirectable => []); + } else { + print STDERR "Warning: not HTTP; turning off direct file access\n"; + $opt_d=0; + } +} + +sub pdate($) { + my($d) = @_; + $d =~ m#(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)# + or die "Unparseable date: $d\n"; + my $y=$1; $y-=1900 if $y>1900; + return timegm($6||0,$5,$4,$3,$2-1,$y); +} + +sub getwd() { + my $pwd = `pwd`; + chomp $pwd; + return $pwd; +} + + +sub get_headref($$) { + my $name = shift; + my $git_dir = shift; + my $sha; + + if (open(C,"$git_dir/refs/heads/$name")) { + chomp($sha = <C>); + close(C); + length($sha) == 40 + or die "Cannot get head id for $name ($sha): $!\n"; + } + return $sha; +} + + +-d $git_tree + or mkdir($git_tree,0777) + or die "Could not create $git_tree: $!"; +chdir($git_tree); + +my $orig_branch = ""; +my $forward_master = 0; +my %branches; + +my $git_dir = $ENV{"GIT_DIR"} || ".git"; +$git_dir = getwd()."/".$git_dir unless $git_dir =~ m#^/#; +$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 $maxnum = 0; +my $last_rev = ""; +my $last_branch; +my $current_rev = $opt_s || 1; +unless(-d $git_dir) { + system("git-init"); + die "Cannot init the GIT db at $git_tree: $?\n" if $?; + system("git-read-tree"); + die "Cannot init an empty tree: $?\n" if $?; + + $last_branch = $opt_o; + $orig_branch = ""; +} else { + -f "$git_dir/refs/heads/$opt_o" + or die "Branch '$opt_o' does not exist.\n". + "Either use the correct '-o branch' option,\n". + "or import to a new repository.\n"; + + -f "$git_dir/svn2git" + or die "'$git_dir/svn2git' does not exist.\n". + "You need that file for incremental imports.\n"; + open(F, "git-symbolic-ref HEAD |") or + die "Cannot run git-symbolic-ref: $!\n"; + chomp ($last_branch = <F>); + $last_branch = basename($last_branch); + close(F); + unless($last_branch) { + warn "Cannot read the last branch name: $! -- assuming 'master'\n"; + $last_branch = "master"; + } + $orig_branch = $last_branch; + $last_rev = get_headref($orig_branch, $git_dir); + if (-f "$git_dir/SVN2GIT_HEAD") { + die <<EOM; +SVN2GIT_HEAD exists. +Make sure your working directory corresponds to HEAD and remove SVN2GIT_HEAD. +You may need to run + + git-read-tree -m -u SVN2GIT_HEAD HEAD +EOM + } + system('cp', "$git_dir/HEAD", "$git_dir/SVN2GIT_HEAD"); + + $forward_master = + $opt_o ne 'master' && -f "$git_dir/refs/heads/master" && + system('cmp', '-s', "$git_dir/refs/heads/master", + "$git_dir/refs/heads/$opt_o") == 0; + + # populate index + system('git-read-tree', $last_rev); + die "read-tree failed: $?\n" if $?; + + # Get the last import timestamps + open my $B,"<", "$git_dir/svn2git"; + while(<$B>) { + chomp; + my($num,$branch,$ref) = split; + $branches{$branch}{$num} = $ref; + $branches{$branch}{"LAST"} = $ref; + $current_rev = $num+1 if $current_rev <= $num; + } + close($B); +} +-d $git_dir + or die "Could not create git subdir ($git_dir).\n"; + +my $default_authors = "$git_dir/svn-authors"; +if ($opt_A) { + read_users($opt_A); + copy($opt_A,$default_authors) or die "Copy failed: $!"; +} else { + read_users($default_authors) if -f $default_authors; +} + +open BRANCHES,">>", "$git_dir/svn2git"; + +sub node_kind($$) { + my ($svnpath, $revision) = @_; + my $pool=SVN::Pool->new; + $svnpath =~ s#^/*##; + my $kind = $svn->{'svn'}->check_path($svnpath,$revision,$pool); + $pool->clear; + return $kind; +} + +sub get_file($$$) { + my($svnpath,$rev,$path) = @_; + + # now get it + my ($name,$mode); + if($opt_d) { + my($req,$res); + + # /svn/!svn/bc/2/django/trunk/django-docs/build.py + my $url=$svn_url->clone(); + $url->path($url->path."/!svn/bc/$rev/$svn_dir$svnpath"); + print "... $path...\n" if $opt_v; + $req = HTTP::Request->new(GET => $url); + $res = $lwp_ua->request($req); + if ($res->is_success) { + my $fh; + ($fh, $name) = tempfile('gitsvn.XXXXXX', + DIR => File::Spec->tmpdir(), UNLINK => 1); + print $fh $res->content; + close($fh) or die "Could not write $name: $!\n"; + } else { + return undef if $res->code == 301; # directory? + die $res->status_line." at $url\n"; + } + $mode = '0644'; # can't obtain mode via direct http request? + } else { + ($name,$mode) = $svn->file("$svnpath",$rev); + return undef unless defined $name; + } + + my $pid = open(my $F, '-|'); + die $! unless defined $pid; + if (!$pid) { + exec("git-hash-object", "-w", $name) + or die "Cannot create object: $!\n"; + } + my $sha = <$F>; + chomp $sha; + close $F; + unlink $name; + return [$mode, $sha, $path]; +} + +sub get_ignore($$$$$) { + my($new,$old,$rev,$path,$svnpath) = @_; + + return unless $opt_I; + my $name = $svn->ignore("$svnpath",$rev); + if ($path eq '/') { + $path = $opt_I; + } else { + $path = File::Spec->catfile($path,$opt_I); + } + if (defined $name) { + my $pid = open(my $F, '-|'); + die $! unless defined $pid; + if (!$pid) { + exec("git-hash-object", "-w", $name) + or die "Cannot create object: $!\n"; + } + my $sha = <$F>; + chomp $sha; + close $F; + unlink $name; + push(@$new,['0644',$sha,$path]); + } elsif (defined $old) { + push(@$old,$path); + } +} + +sub project_path($$) +{ + my ($path, $project) = @_; + + $path = "/".$path unless ($path =~ m#^\/#) ; + return $1 if ($path =~ m#^$project\/(.*)$#); + + $path =~ s#\.#\\\.#g; + $path =~ s#\+#\\\+#g; + return "/" if ($project =~ m#^$path.*$#); + + return undef; +} + +sub split_path($$) { + my($rev,$path) = @_; + my $branch; + + if($path =~ s#^/\Q$tag_name\E/([^/]+)/?##) { + $branch = "/$1"; + } elsif($path =~ s#^/\Q$trunk_name\E/?##) { + $branch = "/"; + } elsif($path =~ s#^/\Q$branch_name\E/([^/]+)/?##) { + $branch = $1; + } else { + my %no_error = ( + "/" => 1, + "/$tag_name" => 1, + "/$branch_name" => 1 + ); + print STDERR "$rev: Unrecognized path: $path\n" unless (defined $no_error{$path}); + return () + } + if ($path eq "") { + $path = "/"; + } elsif ($project_name) { + $path = project_path($path, $project_name); + } + return ($branch,$path); +} + +sub branch_rev($$) { + + my ($srcbranch,$uptorev) = @_; + + my $bbranches = $branches{$srcbranch}; + my @revs = reverse sort { ($a eq 'LAST' ? 0 : $a) <=> ($b eq 'LAST' ? 0 : $b) } keys %$bbranches; + my $therev; + foreach my $arev(@revs) { + next if ($arev eq 'LAST'); + if ($arev <= $uptorev) { + $therev = $arev; + last; + } + } + return $therev; +} + +sub expand_svndir($$$); + +sub expand_svndir($$$) +{ + my ($svnpath, $rev, $path) = @_; + my @list; + get_ignore(\@list, undef, $rev, $path, $svnpath); + my $dirents = $svn->dir_list($svnpath, $rev); + foreach my $p(keys %$dirents) { + my $kind = node_kind($svnpath.'/'.$p, $rev); + if ($kind eq $SVN::Node::file) { + my $f = get_file($svnpath.'/'.$p, $rev, $path.'/'.$p); + push(@list, $f) if $f; + } elsif ($kind eq $SVN::Node::dir) { + push(@list, + expand_svndir($svnpath.'/'.$p, $rev, $path.'/'.$p)); + } + } + return @list; +} + +sub copy_path($$$$$$$$) { + # Somebody copied a whole subdirectory. + # We need to find the index entries from the old version which the + # SVN log entry points to, and add them to the new place. + + my($newrev,$newbranch,$path,$oldpath,$rev,$node_kind,$new,$parents) = @_; + + my($srcbranch,$srcpath) = split_path($rev,$oldpath); + unless(defined $srcbranch && defined $srcpath) { + print "Path not found when copying from $oldpath @ $rev.\n". + "Will try to copy from original SVN location...\n" + if $opt_v; + push (@$new, expand_svndir($oldpath, $rev, $path)); + return; + } + my $therev = branch_rev($srcbranch, $rev); + my $gitrev = $branches{$srcbranch}{$therev}; + unless($gitrev) { + print STDERR "$newrev:$newbranch: could not find $oldpath \@ $rev\n"; + return; + } + if ($srcbranch ne $newbranch) { + push(@$parents, $branches{$srcbranch}{'LAST'}); + } + print "$newrev:$newbranch:$path: copying from $srcbranch:$srcpath @ $rev\n" if $opt_v; + if ($node_kind eq $SVN::Node::dir) { + $srcpath =~ s#/*$#/#; + } + + my $pid = open my $f,'-|'; + die $! unless defined $pid; + if (!$pid) { + exec("git-ls-tree","-r","-z",$gitrev,$srcpath) + or die $!; + } + local $/ = "\0"; + while(<$f>) { + chomp; + my($m,$p) = split(/\t/,$_,2); + my($mode,$type,$sha1) = split(/ /,$m); + next if $type ne "blob"; + if ($node_kind eq $SVN::Node::dir) { + $p = $path . substr($p,length($srcpath)-1); + } else { + $p = $path; + } + push(@$new,[$mode,$sha1,$p]); + } + close($f) or + print STDERR "$newrev:$newbranch: could not list files in $oldpath \@ $rev\n"; +} + +sub commit { + my($branch, $changed_paths, $revision, $author, $date, $message) = @_; + my($committer_name,$committer_email,$dest); + my($author_name,$author_email); + my(@old,@new,@parents); + + if (not defined $author or $author eq "") { + $committer_name = $committer_email = "unknown"; + } elsif (defined $users_file) { + die "User $author is not listed in $users_file\n" + unless exists $users{$author}; + ($committer_name,$committer_email) = @{$users{$author}}; + } elsif ($author =~ /^(.*?)\s+<(.*)>$/) { + ($committer_name, $committer_email) = ($1, $2); + } else { + $author =~ s/^<(.*)>$/$1/; + $committer_name = $committer_email = $author; + } + + if ($opt_F && $message =~ /From:\s+(.*?)\s+<(.*)>\s*\n/) { + ($author_name, $author_email) = ($1, $2); + print "Author from From: $1 <$2>\n" if ($opt_v);; + } elsif ($opt_S && $message =~ /Signed-off-by:\s+(.*?)\s+<(.*)>\s*\n/) { + ($author_name, $author_email) = ($1, $2); + print "Author from Signed-off-by: $1 <$2>\n" if ($opt_v);; + } else { + $author_name = $committer_name; + $author_email = $committer_email; + } + + $date = pdate($date); + + my $tag; + my $parent; + if($branch eq "/") { # trunk + $parent = $opt_o; + } elsif($branch =~ m#^/(.+)#) { # tag + $tag = 1; + $parent = $1; + } else { # "normal" branch + # nothing to do + $parent = $branch; + } + $dest = $parent; + + my $prev = $changed_paths->{"/"}; + if($prev and $prev->[0] eq "A") { + delete $changed_paths->{"/"}; + my $oldpath = $prev->[1]; + my $rev; + if(defined $oldpath) { + my $p; + ($parent,$p) = split_path($revision,$oldpath); + if(defined $parent) { + if($parent eq "/") { + $parent = $opt_o; + } else { + $parent =~ s#^/##; # if it's a tag + } + } + } else { + $parent = undef; + } + } + + my $rev; + if($revision > $opt_s and defined $parent) { + open(H,"git-rev-parse --verify $parent |"); + $rev = <H>; + close(H) or do { + print STDERR "$revision: cannot find commit '$parent'!\n"; + return; + }; + chop $rev; + if(length($rev) != 40) { + print STDERR "$revision: cannot find commit '$parent'!\n"; + return; + } + $rev = $branches{($parent eq $opt_o) ? "/" : $parent}{"LAST"}; + if($revision != $opt_s and not $rev) { + print STDERR "$revision: do not know ancestor for '$parent'!\n"; + return; + } + } else { + $rev = undef; + } + +# if($prev and $prev->[0] eq "A") { +# if(not $tag) { +# unless(open(H,"> $git_dir/refs/heads/$branch")) { +# print STDERR "$revision: Could not create branch $branch: $!\n"; +# $state=11; +# next; +# } +# print H "$rev\n" +# or die "Could not write branch $branch: $!"; +# close(H) +# or die "Could not write branch $branch: $!"; +# } +# } + if(not defined $rev) { + unlink($git_index); + } elsif ($rev ne $last_rev) { + print "Switching from $last_rev to $rev ($branch)\n" if $opt_v; + system("git-read-tree", $rev); + die "read-tree failed for $rev: $?\n" if $?; + $last_rev = $rev; + } + + push (@parents, $rev) if defined $rev; + + my $cid; + if($tag and not %$changed_paths) { + $cid = $rev; + } else { + my @paths = sort keys %$changed_paths; + foreach my $path(@paths) { + my $action = $changed_paths->{$path}; + + if ($action->[0] eq "R") { + # refer to a file/tree in an earlier commit + push(@old,$path); # remove any old stuff + } + if(($action->[0] eq "A") || ($action->[0] eq "R")) { + my $node_kind = node_kind($action->[3], $revision); + if ($node_kind eq $SVN::Node::file) { + my $f = get_file($action->[3], + $revision, $path); + if ($f) { + push(@new,$f) if $f; + } else { + my $opath = $action->[3]; + print STDERR "$revision: $branch: could not fetch '$opath'\n"; + } + } elsif ($node_kind eq $SVN::Node::dir) { + if($action->[1]) { + copy_path($revision, $branch, + $path, $action->[1], + $action->[2], $node_kind, + \@new, \@parents); + } else { + get_ignore(\@new, \@old, $revision, + $path, $action->[3]); + } + } + } elsif ($action->[0] eq "D") { + push(@old,$path); + } elsif ($action->[0] eq "M") { + my $node_kind = node_kind($action->[3], $revision); + if ($node_kind eq $SVN::Node::file) { + my $f = get_file($action->[3], + $revision, $path); + push(@new,$f) if $f; + } elsif ($node_kind eq $SVN::Node::dir) { + get_ignore(\@new, \@old, $revision, + $path, $action->[3]); + } + } else { + die "$revision: unknown action '".$action->[0]."' for $path\n"; + } + } + + while(@old) { + my @o1; + if(@old > 55) { + @o1 = splice(@old,0,50); + } else { + @o1 = @old; + @old = (); + } + my $pid = open my $F, "-|"; + die "$!" unless defined $pid; + if (!$pid) { + exec("git-ls-files", "-z", @o1) or die $!; + } + @o1 = (); + local $/ = "\0"; + while(<$F>) { + chomp; + push(@o1,$_); + } + close($F); + + while(@o1) { + my @o2; + if(@o1 > 55) { + @o2 = splice(@o1,0,50); + } else { + @o2 = @o1; + @o1 = (); + } + system("git-update-index","--force-remove","--",@o2); + die "Cannot remove files: $?\n" if $?; + } + } + while(@new) { + my @n2; + if(@new > 12) { + @n2 = splice(@new,0,10); + } else { + @n2 = @new; + @new = (); + } + system("git-update-index","--add", + (map { ('--cacheinfo', @$_) } @n2)); + die "Cannot add files: $?\n" if $?; + } + + my $pid = open(C,"-|"); + die "Cannot fork: $!" unless defined $pid; + unless($pid) { + exec("git-write-tree"); + die "Cannot exec git-write-tree: $!\n"; + } + chomp(my $tree = <C>); + length($tree) == 40 + or die "Cannot get tree id ($tree): $!\n"; + close(C) + or die "Error running git-write-tree: $?\n"; + print "Tree ID $tree\n" if $opt_v; + + my $pr = IO::Pipe->new() or die "Cannot open pipe: $!\n"; + my $pw = IO::Pipe->new() or die "Cannot open pipe: $!\n"; + $pid = fork(); + die "Fork: $!\n" unless defined $pid; + unless($pid) { + $pr->writer(); + $pw->reader(); + open(OUT,">&STDOUT"); + dup2($pw->fileno(),0); + dup2($pr->fileno(),1); + $pr->close(); + $pw->close(); + + my @par = (); + + # loose detection of merges + # based on the commit msg + foreach my $rx (@mergerx) { + if ($message =~ $rx) { + my $mparent = $1; + if ($mparent eq 'HEAD') { $mparent = $opt_o }; + if ( -e "$git_dir/refs/heads/$mparent") { + $mparent = get_headref($mparent, $git_dir); + push (@parents, $mparent); + print OUT "Merge parent branch: $mparent\n" if $opt_v; + } + } + } + my %seen_parents = (); + my @unique_parents = grep { ! $seen_parents{$_} ++ } @parents; + foreach my $bparent (@unique_parents) { + push @par, '-p', $bparent; + print OUT "Merge parent branch: $bparent\n" if $opt_v; + } + + exec("env", + "GIT_AUTHOR_NAME=$author_name", + "GIT_AUTHOR_EMAIL=$author_email", + "GIT_AUTHOR_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)), + "GIT_COMMITTER_NAME=$committer_name", + "GIT_COMMITTER_EMAIL=$committer_email", + "GIT_COMMITTER_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)), + "git-commit-tree", $tree,@par); + die "Cannot exec git-commit-tree: $!\n"; + } + $pw->writer(); + $pr->reader(); + + $message =~ s/[\s\n]+\z//; + $message = "r$revision: $message" if $opt_r; + + print $pw "$message\n" + or die "Error writing to git-commit-tree: $!\n"; + $pw->close(); + + print "Committed change $revision:$branch ".strftime("%Y-%m-%d %H:%M:%S",gmtime($date)).")\n" if $opt_v; + chomp($cid = <$pr>); + length($cid) == 40 + or die "Cannot get commit id ($cid): $!\n"; + print "Commit ID $cid\n" if $opt_v; + $pr->close(); + + waitpid($pid,0); + die "Error running git-commit-tree: $?\n" if $?; + } + + if (not defined $cid) { + $cid = $branches{"/"}{"LAST"}; + } + + if(not defined $dest) { + print "... no known parent\n" if $opt_v; + } elsif(not $tag) { + print "Writing to refs/heads/$dest\n" if $opt_v; + open(C,">$git_dir/refs/heads/$dest") and + print C ("$cid\n") and + close(C) + or die "Cannot write branch $dest for update: $!\n"; + } + + if($tag) { + my($in, $out) = ('',''); + $last_rev = "-" if %$changed_paths; + # the tag was 'complex', i.e. did not refer to a "real" revision + + $dest =~ tr/_/\./ if $opt_u; + $branch = $dest; + + my $pid = open2($in, $out, 'git-mktag'); + print $out ("object $cid\n". + "type commit\n". + "tag $dest\n". + "tagger $committer_name <$committer_email> 0 +0000\n") and + close($out) + or die "Cannot create tag object $dest: $!\n"; + + my $tagobj = <$in>; + chomp $tagobj; + + if ( !close($in) or waitpid($pid, 0) != $pid or + $? != 0 or $tagobj !~ /^[0123456789abcdef]{40}$/ ) { + die "Cannot create tag object $dest: $!\n"; + } + + open(C,">$git_dir/refs/tags/$dest") and + print C ("$tagobj\n") and + close(C) + or die "Cannot create tag $branch: $!\n"; + + print "Created tag '$dest' on '$branch'\n" if $opt_v; + } + $branches{$branch}{"LAST"} = $cid; + $branches{$branch}{$revision} = $cid; + $last_rev = $cid; + print BRANCHES "$revision $branch $cid\n"; + print "DONE: $revision $dest $cid\n" if $opt_v; +} + +sub commit_all { + # Recursive use of the SVN connection does not work + local $svn = $svn2; + + my ($changed_paths, $revision, $author, $date, $message, $pool) = @_; + my %p; + while(my($path,$action) = each %$changed_paths) { + $p{$path} = [ $action->action,$action->copyfrom_path, $action->copyfrom_rev, $path ]; + } + $changed_paths = \%p; + + my %done; + my @col; + my $pref; + my $branch; + + while(my($path,$action) = each %$changed_paths) { + ($branch,$path) = split_path($revision,$path); + next if not defined $branch; + next if not defined $path; + $done{$branch}{$path} = $action; + } + while(($branch,$changed_paths) = each %done) { + commit($branch, $changed_paths, $revision, $author, $date, $message); + } +} + +$opt_l = $svn->{'maxrev'} if not defined $opt_l or $opt_l > $svn->{'maxrev'}; + +if ($opt_l < $current_rev) { + print "Up to date: no new revisions to fetch!\n" if $opt_v; + unlink("$git_dir/SVN2GIT_HEAD"); + exit; +} + +print "Processing from $current_rev to $opt_l ...\n" if $opt_v; + +my $from_rev; +my $to_rev = $current_rev - 1; + +while ($to_rev < $opt_l) { + $from_rev = $to_rev + 1; + $to_rev = $from_rev + $repack_after; + $to_rev = $opt_l if $opt_l < $to_rev; + print "Fetching from $from_rev to $to_rev ...\n" if $opt_v; + my $pool=SVN::Pool->new; + $svn->{'svn'}->get_log("/",$from_rev,$to_rev,0,1,1,\&commit_all,$pool); + $pool->clear; + my $pid = fork(); + die "Fork: $!\n" unless defined $pid; + unless($pid) { + exec("git-repack", "-d") + or die "Cannot repack: $!\n"; + } + waitpid($pid, 0); +} + + +unlink($git_index); + +if (defined $orig_git_index) { + $ENV{GIT_INDEX_FILE} = $orig_git_index; +} else { + delete $ENV{GIT_INDEX_FILE}; +} + +# Now switch back to the branch we were in before all of this happened +if($orig_branch) { + print "DONE\n" if $opt_v and (not defined $opt_l or $opt_l > 0); + system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master") + if $forward_master; + unless ($opt_i) { + system('git-read-tree', '-m', '-u', 'SVN2GIT_HEAD', 'HEAD'); + die "read-tree failed: $?\n" if $?; + } +} else { + $orig_branch = "master"; + print "DONE; creating $orig_branch branch\n" if $opt_v and (not defined $opt_l or $opt_l > 0); + system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master") + unless -f "$git_dir/refs/heads/master"; + system('git-update-ref', 'HEAD', "$orig_branch"); + unless ($opt_i) { + system('git checkout'); + die "checkout failed: $?\n" if $?; + } +} +unlink("$git_dir/SVN2GIT_HEAD"); +close(BRANCHES); diff --git a/git-tag.sh b/git-tag.sh new file mode 100755 index 0000000000..4a0a7b6607 --- /dev/null +++ b/git-tag.sh @@ -0,0 +1,153 @@ +#!/bin/sh +# Copyright (c) 2005 Linus Torvalds + +USAGE='-l [<pattern>] | [-a | -s | -u <key-id>] [-f | -d | -v] [-m <msg>] <tagname> [<head>]' +SUBDIRECTORY_OK='Yes' +. git-sh-setup + +message_given= +annotate= +signed= +force= +message= +username= +list= +verify= +while case "$#" in 0) break ;; esac +do + case "$1" in + -a) + annotate=1 + ;; + -s) + annotate=1 + signed=1 + ;; + -f) + force=1 + ;; + -l) + case "$#" in + 1) + set x . ;; + esac + shift + git rev-parse --symbolic --tags | sort | grep "$@" + exit $? + ;; + -m) + annotate=1 + shift + message="$1" + if test "$#" = "0"; then + die "error: option -m needs an argument" + else + message_given=1 + fi + ;; + -F) + annotate=1 + shift + if test "$#" = "0"; then + die "error: option -F needs an argument" + else + message="$(cat "$1")" + message_given=1 + fi + ;; + -u) + annotate=1 + signed=1 + shift + username="$1" + ;; + -d) + shift + had_error=0 + for tag + do + cur=$(git-show-ref --verify --hash -- "refs/tags/$tag") || { + echo >&2 "Seriously, what tag are you talking about?" + had_error=1 + continue + } + git-update-ref -m 'tag: delete' -d "refs/tags/$tag" "$cur" || { + had_error=1 + continue + } + echo "Deleted tag $tag." + done + exit $had_error + ;; + -v) + shift + tag_name="$1" + tag=$(git-show-ref --verify --hash -- "refs/tags/$tag_name") || + die "Seriously, what tag are you talking about?" + git-verify-tag -v "$tag" + exit $? + ;; + -*) + usage + ;; + *) + break + ;; + esac + shift +done + +name="$1" +[ "$name" ] || usage +prev=0000000000000000000000000000000000000000 +if git-show-ref --verify --quiet -- "refs/tags/$name" +then + test -n "$force" || die "tag '$name' already exists" + prev=`git rev-parse "refs/tags/$name"` +fi +shift +git-check-ref-format "tags/$name" || + die "we do not like '$name' as a tag name." + +object=$(git-rev-parse --verify --default HEAD "$@") || exit 1 +type=$(git-cat-file -t $object) || exit 1 +tagger=$(git-var GIT_COMMITTER_IDENT) || exit 1 + +test -n "$username" || + username=$(git-repo-config user.signingkey) || + username=$(expr "z$tagger" : 'z\(.*>\)') + +trap 'rm -f "$GIT_DIR"/TAG_TMP* "$GIT_DIR"/TAG_FINALMSG "$GIT_DIR"/TAG_EDITMSG' 0 + +if [ "$annotate" ]; then + if [ -z "$message_given" ]; then + ( echo "#" + echo "# Write a tag message" + echo "#" ) > "$GIT_DIR"/TAG_EDITMSG + ${VISUAL:-${EDITOR:-vi}} "$GIT_DIR"/TAG_EDITMSG || exit + else + echo "$message" >"$GIT_DIR"/TAG_EDITMSG + fi + + grep -v '^#' <"$GIT_DIR"/TAG_EDITMSG | + git-stripspace >"$GIT_DIR"/TAG_FINALMSG + + [ -s "$GIT_DIR"/TAG_FINALMSG -o -n "$message_given" ] || { + echo >&2 "No tag message?" + exit 1 + } + + ( printf 'object %s\ntype %s\ntag %s\ntagger %s\n\n' \ + "$object" "$type" "$name" "$tagger"; + cat "$GIT_DIR"/TAG_FINALMSG ) >"$GIT_DIR"/TAG_TMP + rm -f "$GIT_DIR"/TAG_TMP.asc "$GIT_DIR"/TAG_FINALMSG + if [ "$signed" ]; then + gpg -bsa -u "$username" "$GIT_DIR"/TAG_TMP && + cat "$GIT_DIR"/TAG_TMP.asc >>"$GIT_DIR"/TAG_TMP || + die "failed to sign the tag with GPG." + fi + object=$(git-mktag < "$GIT_DIR"/TAG_TMP) +fi + +git update-ref "refs/tags/$name" "$object" "$prev" + diff --git a/git-verify-tag.sh b/git-verify-tag.sh new file mode 100755 index 0000000000..8db7dd0b7d --- /dev/null +++ b/git-verify-tag.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +USAGE='<tag>' +SUBDIRECTORY_OK='Yes' +. git-sh-setup + +verbose= +while case $# in 0) break;; esac +do + case "$1" in + -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) + verbose=t ;; + *) + break ;; + esac + shift +done + +if [ "$#" != "1" ] +then + usage +fi + +type="$(git-cat-file -t "$1" 2>/dev/null)" || + die "$1: no such object." + +test "$type" = tag || + die "$1: cannot verify a non-tag object of type $type." + +case "$verbose" in +t) + git-cat-file -p "$1" | + sed -n -e '/^-----BEGIN PGP SIGNATURE-----/q' -e p + ;; +esac + +trap 'rm -f "$GIT_DIR/.tmp-vtag"' 0 + +git-cat-file tag "$1" >"$GIT_DIR/.tmp-vtag" || exit 1 + +cat "$GIT_DIR/.tmp-vtag" | +sed '/-----BEGIN PGP/Q' | +gpg --verify "$GIT_DIR/.tmp-vtag" - || exit 1 +rm -f "$GIT_DIR/.tmp-vtag" + @@ -0,0 +1,414 @@ +#include "builtin.h" +#include "exec_cmd.h" +#include "cache.h" +#include "quote.h" + +const char git_usage_string[] = + "git [--version] [--exec-path[=GIT_EXEC_PATH]] [-p|--paginate] [--bare] [--git-dir=GIT_DIR] [--help] COMMAND [ARGS]"; + +static void prepend_to_path(const char *dir, int len) +{ + const char *old_path = getenv("PATH"); + char *path; + int path_len = len; + + if (!old_path) + old_path = "/usr/local/bin:/usr/bin:/bin"; + + path_len = len + strlen(old_path) + 1; + + path = xmalloc(path_len + 1); + + memcpy(path, dir, len); + path[len] = ':'; + memcpy(path + len + 1, old_path, path_len - len); + + setenv("PATH", path, 1); + + free(path); +} + +static int handle_options(const char*** argv, int* argc) +{ + int handled = 0; + + while (*argc > 0) { + const char *cmd = (*argv)[0]; + if (cmd[0] != '-') + break; + + /* + * For legacy reasons, the "version" and "help" + * commands can be written with "--" prepended + * to make them look like flags. + */ + if (!strcmp(cmd, "--help") || !strcmp(cmd, "--version")) + break; + + /* + * Check remaining flags. + */ + if (!strncmp(cmd, "--exec-path", 11)) { + cmd += 11; + if (*cmd == '=') + git_set_exec_path(cmd + 1); + else { + puts(git_exec_path()); + exit(0); + } + } else if (!strcmp(cmd, "-p") || !strcmp(cmd, "--paginate")) { + setup_pager(); + } else if (!strcmp(cmd, "--git-dir")) { + if (*argc < 2) { + fprintf(stderr, "No directory given for --git-dir.\n" ); + usage(git_usage_string); + } + setenv(GIT_DIR_ENVIRONMENT, (*argv)[1], 1); + (*argv)++; + (*argc)--; + } else if (!strncmp(cmd, "--git-dir=", 10)) { + setenv(GIT_DIR_ENVIRONMENT, cmd + 10, 1); + } else if (!strcmp(cmd, "--bare")) { + static char git_dir[PATH_MAX+1]; + setenv(GIT_DIR_ENVIRONMENT, getcwd(git_dir, sizeof(git_dir)), 1); + } else { + fprintf(stderr, "Unknown option: %s\n", cmd); + usage(git_usage_string); + } + + (*argv)++; + (*argc)--; + handled++; + } + return handled; +} + +static const char *alias_command; +static char *alias_string; + +static int git_alias_config(const char *var, const char *value) +{ + if (!strncmp(var, "alias.", 6) && !strcmp(var + 6, alias_command)) { + alias_string = xstrdup(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 = xrealloc(*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, saved_errno = errno; + const char *subdir; + int count, option_count; + const char** new_argv; + + subdir = setup_git_directory_gently(&nongit); + + alias_command = (*argv)[0]; + git_config(git_alias_config); + if (alias_string) { + if (alias_string[0] == '!') { + trace_printf("trace: alias to shell cmd: %s => %s\n", + alias_command, alias_string + 1); + ret = system(alias_string + 1); + if (ret >= 0 && WIFEXITED(ret) && + WEXITSTATUS(ret) != 127) + exit(WEXITSTATUS(ret)); + die("Failed to run '%s' when expanding alias '%s'\n", + alias_string + 1, alias_command); + } + count = split_cmdline(alias_string, &new_argv); + option_count = handle_options(&new_argv, &count); + memmove(new_argv - option_count, new_argv, + count * sizeof(char *)); + new_argv -= option_count; + + if (count < 1) + die("empty alias for %s", alias_command); + + if (!strcmp(alias_command, new_argv[0])) + die("recursive alias: %s", alias_command); + + trace_argv_printf(new_argv, count, + "trace: alias expansion: %s =>", + alias_command); + + new_argv = xrealloc(new_argv, sizeof(char*) * + (count + *argcp + 1)); + /* insert after command name */ + memcpy(new_argv + count, *argv + 1, sizeof(char*) * *argcp); + new_argv[count+*argcp] = NULL; + + *argv = new_argv; + *argcp += count - 1; + + ret = 1; + } + + if (subdir) + chdir(subdir); + + errno = saved_errno; + + return ret; +} + +const char git_version_string[] = GIT_VERSION; + +#define RUN_SETUP (1<<0) +#define USE_PAGER (1<<1) +/* + * require working tree to be present -- anything uses this needs + * RUN_SETUP for reading from the configuration file. + */ +#define NOT_BARE (1<<2) + +static void handle_internal_command(int argc, const char **argv, char **envp) +{ + const char *cmd = argv[0]; + static struct cmd_struct { + const char *cmd; + int (*fn)(int, const char **, const char *); + int option; + } commands[] = { + { "add", cmd_add, RUN_SETUP | NOT_BARE }, + { "annotate", cmd_annotate, USE_PAGER }, + { "apply", cmd_apply }, + { "archive", cmd_archive }, + { "blame", cmd_blame, RUN_SETUP }, + { "branch", cmd_branch, RUN_SETUP }, + { "cat-file", cmd_cat_file, RUN_SETUP }, + { "checkout-index", cmd_checkout_index, RUN_SETUP }, + { "check-ref-format", cmd_check_ref_format }, + { "cherry", cmd_cherry, RUN_SETUP }, + { "commit-tree", cmd_commit_tree, RUN_SETUP }, + { "config", cmd_config }, + { "count-objects", cmd_count_objects, RUN_SETUP }, + { "describe", cmd_describe, RUN_SETUP }, + { "diff", cmd_diff, RUN_SETUP | USE_PAGER }, + { "diff-files", cmd_diff_files, RUN_SETUP }, + { "diff-index", cmd_diff_index, RUN_SETUP }, + { "diff-stages", cmd_diff_stages, RUN_SETUP }, + { "diff-tree", cmd_diff_tree, RUN_SETUP }, + { "fmt-merge-msg", cmd_fmt_merge_msg, RUN_SETUP }, + { "for-each-ref", cmd_for_each_ref, RUN_SETUP }, + { "format-patch", cmd_format_patch, RUN_SETUP }, + { "fsck", cmd_fsck, RUN_SETUP }, + { "fsck-objects", cmd_fsck, RUN_SETUP }, + { "get-tar-commit-id", cmd_get_tar_commit_id }, + { "grep", cmd_grep, RUN_SETUP }, + { "help", cmd_help }, + { "init", cmd_init_db }, + { "init-db", cmd_init_db }, + { "log", cmd_log, RUN_SETUP | USE_PAGER }, + { "ls-files", cmd_ls_files, RUN_SETUP }, + { "ls-tree", cmd_ls_tree, RUN_SETUP }, + { "mailinfo", cmd_mailinfo }, + { "mailsplit", cmd_mailsplit }, + { "merge-file", cmd_merge_file }, + { "mv", cmd_mv, RUN_SETUP | NOT_BARE }, + { "name-rev", cmd_name_rev, RUN_SETUP }, + { "pack-objects", cmd_pack_objects, RUN_SETUP }, + { "pickaxe", cmd_blame, RUN_SETUP | USE_PAGER }, + { "prune", cmd_prune, RUN_SETUP }, + { "prune-packed", cmd_prune_packed, RUN_SETUP }, + { "push", cmd_push, RUN_SETUP }, + { "read-tree", cmd_read_tree, RUN_SETUP }, + { "reflog", cmd_reflog, RUN_SETUP }, + { "repo-config", cmd_config }, + { "rerere", cmd_rerere, RUN_SETUP }, + { "rev-list", cmd_rev_list, RUN_SETUP }, + { "rev-parse", cmd_rev_parse, RUN_SETUP }, + { "rm", cmd_rm, RUN_SETUP | NOT_BARE }, + { "runstatus", cmd_runstatus, RUN_SETUP | NOT_BARE }, + { "shortlog", cmd_shortlog, RUN_SETUP | USE_PAGER }, + { "show-branch", cmd_show_branch, RUN_SETUP }, + { "show", cmd_show, RUN_SETUP | USE_PAGER }, + { "stripspace", cmd_stripspace }, + { "symbolic-ref", cmd_symbolic_ref, RUN_SETUP }, + { "tar-tree", cmd_tar_tree }, + { "unpack-objects", cmd_unpack_objects, RUN_SETUP }, + { "update-index", cmd_update_index, RUN_SETUP }, + { "update-ref", cmd_update_ref, RUN_SETUP }, + { "upload-archive", cmd_upload_archive }, + { "version", cmd_version }, + { "whatchanged", cmd_whatchanged, RUN_SETUP | USE_PAGER }, + { "write-tree", cmd_write_tree, RUN_SETUP }, + { "verify-pack", cmd_verify_pack }, + { "show-ref", cmd_show_ref, RUN_SETUP }, + { "pack-refs", cmd_pack_refs, RUN_SETUP }, + }; + int i; + + /* Turn "git cmd --help" into "git help cmd" */ + if (argc > 1 && !strcmp(argv[1], "--help")) { + argv[1] = argv[0]; + argv[0] = cmd = "help"; + } + + for (i = 0; i < ARRAY_SIZE(commands); i++) { + struct cmd_struct *p = commands+i; + const char *prefix; + if (strcmp(p->cmd, cmd)) + continue; + + prefix = NULL; + if (p->option & RUN_SETUP) + prefix = setup_git_directory(); + if (p->option & USE_PAGER) + setup_pager(); + if ((p->option & NOT_BARE) && + (is_bare_repository() || is_inside_git_dir())) + die("%s must be run in a work tree", cmd); + trace_argv_printf(argv, argc, "trace: built-in: git"); + + exit(p->fn(argc, argv, prefix)); + } +} + +int main(int argc, const char **argv, char **envp) +{ + const char *cmd = argv[0] ? argv[0] : "git-help"; + char *slash = strrchr(cmd, '/'); + const char *exec_path = NULL; + int done_alias = 0; + + /* + * Take the basename of argv[0] as the command + * name, and the dirname as the default exec_path + * if it's an absolute path and we don't have + * anything better. + */ + if (slash) { + *slash++ = 0; + if (*cmd == '/') + exec_path = cmd; + cmd = slash; + } + + /* + * "git-xxxx" is the same as "git xxxx", but we obviously: + * + * - cannot take flags in between the "git" and the "xxxx". + * - cannot execute it externally (since it would just do + * the same thing over again) + * + * So we just directly call the internal command handler, and + * die if that one cannot handle it. + */ + if (!strncmp(cmd, "git-", 4)) { + cmd += 4; + argv[0] = cmd; + handle_internal_command(argc, argv, envp); + die("cannot handle %s internally", cmd); + } + + /* Look for flags.. */ + argv++; + argc--; + handle_options(&argv, &argc); + if (argc > 0) { + if (!strncmp(argv[0], "--", 2)) + argv[0] += 2; + } else { + /* Default command: "help" */ + argv[0] = "help"; + argc = 1; + } + cmd = argv[0]; + + /* + * We search for git commands in the following order: + * - git_exec_path() + * - the path of the "git" command if we could find it + * in $0 + * - the regular PATH. + */ + if (exec_path) + prepend_to_path(exec_path, strlen(exec_path)); + exec_path = git_exec_path(); + prepend_to_path(exec_path, strlen(exec_path)); + + while (1) { + /* See if it's an internal command */ + handle_internal_command(argc, argv, envp); + + /* .. 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) { + if (done_alias) { + fprintf(stderr, "Expansion of alias '%s' failed; " + "'%s' is not a git-command\n", + cmd, argv[0]); + exit(1); + } + help_unknown_cmd(cmd); + } + + fprintf(stderr, "Failed to run command '%s': %s\n", + cmd, strerror(errno)); + + return 1; +} diff --git a/git.spec.in b/git.spec.in new file mode 100644 index 0000000000..1213325e57 --- /dev/null +++ b/git.spec.in @@ -0,0 +1,208 @@ +# Pass --without docs to rpmbuild if you don't want the documentation +Name: git +Version: @@VERSION@@ +Release: 1%{?dist} +Summary: Git core and tools +License: GPL +Group: Development/Tools +URL: http://kernel.org/pub/software/scm/git/ +Source: http://kernel.org/pub/software/scm/git/%{name}-%{version}.tar.gz +BuildRequires: zlib-devel >= 1.2, openssl-devel, curl-devel, expat-devel %{!?_without_docs:, xmlto, asciidoc > 6.0.3} +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) +Requires: git-core, git-svn, git-cvs, git-arch, git-email, gitk, perl-Git + +%description +This is a stupid (but extremely fast) directory content manager. It +doesn't do a whole lot, but what it _does_ do is track directory +contents efficiently. It is intended to be the base of an efficient, +distributed source code management system. This package includes +rudimentary tools that can be used as a SCM, but you should look +elsewhere for tools for ordinary humans layered on top of this. + +This is a dummy package which brings in all subpackages. + +%package core +Summary: Core git tools +Group: Development/Tools +Requires: zlib >= 1.2, rsync, curl, less, openssh-clients, expat +%description core +This is a stupid (but extremely fast) directory content manager. It +doesn't do a whole lot, but what it _does_ do is track directory +contents efficiently. It is intended to be the base of an efficient, +distributed source code management system. This package includes +rudimentary tools that can be used as a SCM, but you should look +elsewhere for tools for ordinary humans layered on top of this. + +These are the core tools with minimal dependencies. + +%package svn +Summary: Git tools for importing Subversion repositories +Group: Development/Tools +Requires: git-core = %{version}-%{release}, subversion +%description svn +Git tools for importing Subversion repositories. + +%package cvs +Summary: Git tools for importing CVS repositories +Group: Development/Tools +Requires: git-core = %{version}-%{release}, cvs, cvsps +%description cvs +Git tools for importing CVS repositories. + +%package arch +Summary: Git tools for importing Arch repositories +Group: Development/Tools +Requires: git-core = %{version}-%{release}, tla +%description arch +Git tools for importing Arch repositories. + +%package email +Summary: Git tools for sending email +Group: Development/Tools +Requires: git-core = %{version}-%{release} +%description email +Git tools for sending email. + +%package -n gitk +Summary: Git revision tree visualiser ('gitk') +Group: Development/Tools +Requires: git-core = %{version}-%{release}, tk >= 8.4 +%description -n gitk +Git revision tree visualiser ('gitk') + +%package -n perl-Git +Summary: Perl interface to Git +Group: Development/Libraries +Requires: git-core = %{version}-%{release} +Requires: perl(:MODULE_COMPAT_%(eval "`%{__perl} -V:version`"; echo $version)) +BuildRequires: perl(Error) + +%description -n perl-Git +Perl interface to Git + +%prep +%setup -q + +%build +make %{_smp_mflags} CFLAGS="$RPM_OPT_FLAGS" WITH_OWN_SUBPROCESS_PY=YesPlease \ + prefix=%{_prefix} all %{!?_without_docs: doc} + +%install +rm -rf $RPM_BUILD_ROOT +make %{_smp_mflags} CFLAGS="$RPM_OPT_FLAGS" DESTDIR=$RPM_BUILD_ROOT \ + WITH_OWN_SUBPROCESS_PY=YesPlease \ + prefix=%{_prefix} mandir=%{_mandir} INSTALLDIRS=vendor \ + install %{!?_without_docs: install-doc} +find $RPM_BUILD_ROOT -type f -name .packlist -exec rm -f {} ';' +find $RPM_BUILD_ROOT -type f -name '*.bs' -empty -exec rm -f {} ';' +find $RPM_BUILD_ROOT -type f -name perllocal.pod -exec rm -f {} ';' + +(find $RPM_BUILD_ROOT%{_bindir} -type f | grep -vE "archimport|svn|cvs|email|gitk" | sed -e s@^$RPM_BUILD_ROOT@@) > bin-man-doc-files +(find $RPM_BUILD_ROOT%{perl_vendorlib} -type f | sed -e s@^$RPM_BUILD_ROOT@@) >> perl-files +%if %{!?_without_docs:1}0 +(find $RPM_BUILD_ROOT%{_mandir} $RPM_BUILD_ROOT/Documentation -type f | grep -vE "archimport|svn|git-cvs|email|gitk" | sed -e s@^$RPM_BUILD_ROOT@@ -e 's/$/*/' ) >> bin-man-doc-files +%else +rm -rf $RPM_BUILD_ROOT%{_mandir} +%endif + +%clean +rm -rf $RPM_BUILD_ROOT + +%files +# These are no files in the root package + +%files svn +%defattr(-,root,root) +%{_bindir}/*svn* +%doc Documentation/*svn*.txt +%{!?_without_docs: %{_mandir}/man1/*svn*.1*} +%{!?_without_docs: %doc Documentation/*svn*.html } + +%files cvs +%defattr(-,root,root) +%doc Documentation/*git-cvs*.txt +%{_bindir}/*cvs* +%{!?_without_docs: %{_mandir}/man1/*cvs*.1*} +%{!?_without_docs: %doc Documentation/*git-cvs*.html } + +%files arch +%defattr(-,root,root) +%doc Documentation/git-archimport.txt +%{_bindir}/git-archimport +%{!?_without_docs: %{_mandir}/man1/git-archimport.1*} +%{!?_without_docs: %doc Documentation/git-archimport.html } + +%files email +%defattr(-,root,root) +%doc Documentation/*email*.txt +%{_bindir}/*email* +%{!?_without_docs: %{_mandir}/man1/*email*.1*} +%{!?_without_docs: %doc Documentation/*email*.html } + +%files -n gitk +%defattr(-,root,root) +%doc Documentation/*gitk*.txt +%{_bindir}/*gitk* +%{!?_without_docs: %{_mandir}/man1/*gitk*.1*} +%{!?_without_docs: %doc Documentation/*gitk*.html } + +%files -n perl-Git -f perl-files +%defattr(-,root,root) + +%files core -f bin-man-doc-files +%defattr(-,root,root) +%{_datadir}/git-core/ +%doc README COPYING Documentation/*.txt +%{!?_without_docs: %doc Documentation/*.html } + +%changelog +* Mon Nov 14 2005 H. Peter Anvin <hpa@zytor.com> 0.99.9j-1 +- Change subpackage names to git-<name> instead of git-core-<name> +- Create empty root package which brings in all subpackages +- Rename git-tk -> gitk + +* Thu Nov 10 2005 Chris Wright <chrisw@osdl.org> 0.99.9g-1 +- zlib dependency fix +- Minor cleanups from split +- Move arch import to separate package as well + +* Tue Sep 27 2005 Jim Radford <radford@blackbean.org> +- Move programs with non-standard dependencies (svn, cvs, email) + into separate packages + +* Tue Sep 27 2005 H. Peter Anvin <hpa@zytor.com> +- parallelize build +- COPTS -> CFLAGS + +* Fri Sep 16 2005 Chris Wright <chrisw@osdl.org> 0.99.6-1 +- update to 0.99.6 + +* Fri Sep 16 2005 Horst H. von Brand <vonbrand@inf.utfsm.cl> +- Linus noticed that less is required, added to the dependencies + +* Sun Sep 11 2005 Horst H. von Brand <vonbrand@inf.utfsm.cl> +- Updated dependencies +- Don't assume manpages are gzipped + +* Thu Aug 18 2005 Chris Wright <chrisw@osdl.org> 0.99.4-4 +- drop sh_utils, sh-utils, diffutils, mktemp, and openssl Requires +- use RPM_OPT_FLAGS in spec file, drop patch0 + +* Wed Aug 17 2005 Tom "spot" Callaway <tcallawa@redhat.com> 0.99.4-3 +- use dist tag to differentiate between branches +- use rpm optflags by default (patch0) +- own %{_datadir}/git-core/ + +* Mon Aug 15 2005 Chris Wright <chrisw@osdl.org> +- update spec file to fix Buildroot, Requires, and drop Vendor + +* Sun Aug 07 2005 Horst H. von Brand <vonbrand@inf.utfsm.cl> +- Redid the description +- Cut overlong make line, loosened changelog a bit +- I think Junio (or perhaps OSDL?) should be vendor... + +* Thu Jul 14 2005 Eric Biederman <ebiederm@xmission.com> +- Add the man pages, and the --without docs build option + +* Wed Jul 7 2005 Chris Wright <chris@osdl.org> +- initial git spec file @@ -0,0 +1,6349 @@ +#!/bin/sh +# Tcl ignores the next line -*- tcl -*- \ +exec wish "$0" -- "$@" + +# Copyright (C) 2005-2006 Paul Mackerras. All rights reserved. +# This program is free software; it may be used, copied, modified +# and distributed under the terms of the GNU General Public Licence, +# either version 2, or (at your option) any later version. + +proc gitdir {} { + global env + if {[info exists env(GIT_DIR)]} { + return $env(GIT_DIR) + } else { + return [exec git rev-parse --git-dir] + } +} + +proc start_rev_list {view} { + global startmsecs nextupdate + global commfd leftover tclencoding datemode + global viewargs viewfiles commitidx + + set startmsecs [clock clicks -milliseconds] + set nextupdate [expr {$startmsecs + 100}] + set commitidx($view) 0 + set args $viewargs($view) + if {$viewfiles($view) ne {}} { + set args [concat $args "--" $viewfiles($view)] + } + set order "--topo-order" + if {$datemode} { + set order "--date-order" + } + if {[catch { + set fd [open [concat | git rev-list --header $order \ + --parents --boundary --default HEAD $args] r] + } err]} { + puts stderr "Error executing git rev-list: $err" + exit 1 + } + set commfd($view) $fd + set leftover($view) {} + fconfigure $fd -blocking 0 -translation lf + if {$tclencoding != {}} { + fconfigure $fd -encoding $tclencoding + } + fileevent $fd readable [list getcommitlines $fd $view] + nowbusy $view +} + +proc stop_rev_list {} { + global commfd curview + + if {![info exists commfd($curview)]} return + set fd $commfd($curview) + catch { + set pid [pid $fd] + exec kill $pid + } + catch {close $fd} + unset commfd($curview) +} + +proc getcommits {} { + global phase canv mainfont curview + + set phase getcommits + initlayout + start_rev_list $curview + show_status "Reading commits..." +} + +proc getcommitlines {fd view} { + global commitlisted nextupdate + global leftover commfd + global displayorder commitidx commitrow commitdata + global parentlist childlist children curview hlview + global vparentlist vchildlist vdisporder vcmitlisted + + set stuff [read $fd 500000] + if {$stuff == {}} { + if {![eof $fd]} return + global viewname + unset commfd($view) + notbusy $view + # set it blocking so we wait for the process to terminate + fconfigure $fd -blocking 1 + if {[catch {close $fd} err]} { + set fv {} + if {$view != $curview} { + set fv " for the \"$viewname($view)\" view" + } + if {[string range $err 0 4] == "usage"} { + set err "Gitk: error reading commits$fv:\ + bad arguments to git rev-list." + if {$viewname($view) eq "Command line"} { + append err \ + " (Note: arguments to gitk are passed to git rev-list\ + to allow selection of commits to be displayed.)" + } + } else { + set err "Error reading commits$fv: $err" + } + error_popup $err + } + if {$view == $curview} { + after idle finishcommits + } + return + } + set start 0 + set gotsome 0 + while 1 { + set i [string first "\0" $stuff $start] + if {$i < 0} { + append leftover($view) [string range $stuff $start end] + break + } + if {$start == 0} { + set cmit $leftover($view) + append cmit [string range $stuff 0 [expr {$i - 1}]] + set leftover($view) {} + } else { + set cmit [string range $stuff $start [expr {$i - 1}]] + } + set start [expr {$i + 1}] + set j [string first "\n" $cmit] + set ok 0 + set listed 1 + if {$j >= 0} { + set ids [string range $cmit 0 [expr {$j - 1}]] + if {[string range $ids 0 0] == "-"} { + set listed 0 + set ids [string range $ids 1 end] + } + set ok 1 + foreach id $ids { + if {[string length $id] != 40} { + set ok 0 + break + } + } + } + if {!$ok} { + set shortcmit $cmit + if {[string length $shortcmit] > 80} { + set shortcmit "[string range $shortcmit 0 80]..." + } + error_popup "Can't parse git rev-list output: {$shortcmit}" + exit 1 + } + set id [lindex $ids 0] + if {$listed} { + set olds [lrange $ids 1 end] + set i 0 + foreach p $olds { + if {$i == 0 || [lsearch -exact $olds $p] >= $i} { + lappend children($view,$p) $id + } + incr i + } + } else { + set olds {} + } + if {![info exists children($view,$id)]} { + set children($view,$id) {} + } + set commitdata($id) [string range $cmit [expr {$j + 1}] end] + set commitrow($view,$id) $commitidx($view) + incr commitidx($view) + if {$view == $curview} { + lappend parentlist $olds + lappend childlist $children($view,$id) + lappend displayorder $id + lappend commitlisted $listed + } else { + lappend vparentlist($view) $olds + lappend vchildlist($view) $children($view,$id) + lappend vdisporder($view) $id + lappend vcmitlisted($view) $listed + } + set gotsome 1 + } + if {$gotsome} { + if {$view == $curview} { + while {[layoutmore $nextupdate]} doupdate + } elseif {[info exists hlview] && $view == $hlview} { + vhighlightmore + } + } + if {[clock clicks -milliseconds] >= $nextupdate} { + doupdate + } +} + +proc doupdate {} { + global commfd nextupdate numcommits + + foreach v [array names commfd] { + fileevent $commfd($v) readable {} + } + update + set nextupdate [expr {[clock clicks -milliseconds] + 100}] + foreach v [array names commfd] { + set fd $commfd($v) + fileevent $fd readable [list getcommitlines $fd $v] + } +} + +proc readcommit {id} { + 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 selectedline thickerline + + if {$phase ne {}} { + stop_rev_list + set phase {} + } + set n $curview + foreach id $displayorder { + catch {unset children($n,$id)} + catch {unset commitrow($n,$id)} + } + set curview -1 + catch {unset selectedline} + catch {unset thickerline} + catch {unset viewdata($n)} + discardallcommits + readrefs + showview $n +} + +proc parsecommit {id contents listed} { + global commitinfo cdate + + set inhdr 1 + set comment {} + set headline {} + set auname {} + set audate {} + set comname {} + set comdate {} + set hdrend [string first "\n\n" $contents] + if {$hdrend < 0} { + # should never happen... + set hdrend [string length $contents] + } + set header [string range $contents 0 [expr {$hdrend - 1}]] + set comment [string range $contents [expr {$hdrend + 2}] end] + foreach line [split $header "\n"] { + set tag [lindex $line 0] + if {$tag == "author"} { + set audate [lindex $line end-1] + set auname [lrange $line 1 end-2] + } elseif {$tag == "committer"} { + set comdate [lindex $line end-1] + set comname [lrange $line 1 end-2] + } + } + set headline {} + # take the first line of the comment as the headline + set i [string first "\n" $comment] + if {$i >= 0} { + set headline [string trim [string range $comment 0 $i]] + } else { + 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 + set newcomment {} + foreach line [split $comment "\n"] { + append newcomment " " + append newcomment $line + append newcomment "\n" + } + set comment $newcomment + } + if {$comdate != {}} { + set cdate($id) $comdate + } + set commitinfo($id) [list $headline $auname $audate \ + $comname $comdate $comment] +} + +proc getcommit {id} { + global commitdata commitinfo + + if {[info exists commitdata($id)]} { + parsecommit $id $commitdata($id) 1 + } else { + readcommit $id + if {![info exists commitinfo($id)]} { + set commitinfo($id) {"No commit information available"} + } + } + return 1 +} + +proc readrefs {} { + global tagids idtags headids idheads tagcontents + global otherrefids idotherrefs mainhead + + foreach v {tagids idtags headids idheads otherrefids idotherrefs} { + catch {unset $v} + } + set refd [open [list | git show-ref] r] + while {0 <= [set n [gets $refd line]]} { + if {![regexp {^([0-9a-f]{40}) refs/([^^]*)$} $line \ + match id path]} { + continue + } + if {[regexp {^remotes/.*/HEAD$} $path match]} { + continue + } + if {![regexp {^(tags|heads)/(.*)$} $path match type name]} { + set type others + set name $path + } + if {[regexp {^remotes/} $path match]} { + set type heads + } + if {$type == "tags"} { + set tagids($name) $id + lappend idtags($id) $name + set obj {} + set type {} + set tag {} + catch { + 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] + } + } elseif { $type == "heads" } { + set headids($name) $id + lappend idheads($id) $name + } else { + set otherrefids($name) $id + lappend idotherrefs($id) $name + } + } + close $refd + set mainhead {} + catch { + set thehead [exec git symbolic-ref HEAD] + if {[string match "refs/heads/*" $thehead]} { + set mainhead [string range $thehead 11 end] + } + } +} + +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 $top" + pack $w.ok -side bottom -fill x + 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 $w $msg +} + +proc confirm_popup msg { + global confirm_ok + set confirm_ok 0 + set w .confirm + toplevel $w + wm transient $w . + 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 "set confirm_ok 1; destroy $w" + pack $w.ok -side left -fill x + button $w.cancel -text Cancel -command "destroy $w" + pack $w.cancel -side right -fill x + bind $w <Visibility> "grab $w; focus $w" + tkwait window $w + return $confirm_ok +} + +proc makewindow {} { + global canv canv2 canv3 linespc charspc ctext cflist + global textfont mainfont uifont + global findtype findtypemenu findloc findstring fstring geometry + global entries sha1entry sha1string sha1but + global maincursor textcursor curtextcursor + global rowctxmenu mergemax wrapcomment + global highlight_files gdttype + global searchstring sstring + global bgcolor fgcolor bglist fglist diffcolors + global headctxmenu + + menu .bar + .bar add cascade -label "File" -menu .bar.file + .bar configure -font $uifont + menu .bar.file + .bar.file add command -label "Update" -command updatecommits + .bar.file add command -label "Reread references" -command rereadrefs + .bar.file add command -label "Quit" -command doquit + .bar.file configure -font $uifont + menu .bar.edit + .bar add cascade -label "Edit" -menu .bar.edit + .bar.edit add command -label "Preferences" -command doprefs + .bar.edit configure -font $uifont + + menu .bar.view -font $uifont + .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 separator + .bar.view add radiobutton -label "All files" -command {showview 0} \ + -variable selectedview -value 0 + + menu .bar.help + .bar add cascade -label "Help" -menu .bar.help + .bar.help add command -label "About gitk" -command about + .bar.help add command -label "Key bindings" -command keys + .bar.help configure -font $uifont + . configure -menu .bar + + # the gui has upper and lower half, parts of a paned window. + panedwindow .ctop -orient vertical + + # possibly use assumed geometry + if {![info exists geometry(topheight)]} { + set geometry(topheight) [expr {15 * $linespc}] + set geometry(topwidth) [expr {80 * $charspc}] + set geometry(botheight) [expr {15 * $linespc}] + set geometry(botwidth) [expr {50 * $charspc}] + set geometry(canv) [expr {40 * $charspc}] + set geometry(canv2) [expr {20 * $charspc}] + set geometry(canv3) [expr {20 * $charspc}] + } + + # the upper half will have a paned window, a scroll bar to the right, and some stuff below + frame .tf -height $geometry(topheight) -width $geometry(topwidth) + frame .tf.histframe + panedwindow .tf.histframe.pwclist -orient horizontal -sashpad 0 -handlesize 4 + + # create three canvases + set cscroll .tf.histframe.csb + set canv .tf.histframe.pwclist.canv + canvas $canv -width $geometry(canv) \ + -background $bgcolor -bd 0 \ + -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll" + .tf.histframe.pwclist add $canv + set canv2 .tf.histframe.pwclist.canv2 + canvas $canv2 -width $geometry(canv2) \ + -background $bgcolor -bd 0 -yscrollincr $linespc + .tf.histframe.pwclist add $canv2 + set canv3 .tf.histframe.pwclist.canv3 + canvas $canv3 -width $geometry(canv3) \ + -background $bgcolor -bd 0 -yscrollincr $linespc + .tf.histframe.pwclist add $canv3 + + # a scroll bar to rule them + scrollbar $cscroll -command {allcanvs yview} -highlightthickness 0 + pack $cscroll -side right -fill y + bind .tf.histframe.pwclist <Configure> {resizeclistpanes %W %w} + lappend bglist $canv $canv2 $canv3 + pack .tf.histframe.pwclist -fill both -expand 1 -side left + + # we have two button bars at bottom of top frame. Bar 1 + frame .tf.bar + frame .tf.lbar -height 15 + + set sha1entry .tf.bar.sha1 + set entries $sha1entry + set sha1but .tf.bar.sha1label + button $sha1but -text "SHA1 ID: " -state disabled -relief flat \ + -command gotocommit -width 8 -font $uifont + $sha1but conf -disabledforeground [$sha1but cget -foreground] + pack .tf.bar.sha1label -side left + entry $sha1entry -width 40 -font $textfont -textvariable sha1string + trace add variable sha1string write sha1change + pack $sha1entry -side left -pady 2 + + image create bitmap bm-left -data { + #define left_width 16 + #define left_height 16 + static unsigned char left_bits[] = { + 0x00, 0x00, 0xc0, 0x01, 0xe0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1c, 0x00, + 0x0e, 0x00, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x0e, 0x00, 0x1c, 0x00, + 0x38, 0x00, 0x70, 0x00, 0xe0, 0x00, 0xc0, 0x01}; + } + image create bitmap bm-right -data { + #define right_width 16 + #define right_height 16 + static unsigned char right_bits[] = { + 0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0e, 0x00, 0x1c, + 0x00, 0x38, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0x00, 0x38, 0x00, 0x1c, + 0x00, 0x0e, 0x00, 0x07, 0x80, 0x03, 0xc0, 0x01}; + } + button .tf.bar.leftbut -image bm-left -command goback \ + -state disabled -width 26 + pack .tf.bar.leftbut -side left -fill y + button .tf.bar.rightbut -image bm-right -command goforw \ + -state disabled -width 26 + pack .tf.bar.rightbut -side left -fill y + + button .tf.bar.findbut -text "Find" -command dofind -font $uifont + pack .tf.bar.findbut -side left + set findstring {} + set fstring .tf.bar.findstring + lappend entries $fstring + entry $fstring -width 30 -font $textfont -textvariable findstring + trace add variable findstring write find_change + pack $fstring -side left -expand 1 -fill x -in .tf.bar + set findtype Exact + set findtypemenu [tk_optionMenu .tf.bar.findtype \ + findtype Exact IgnCase Regexp] + trace add variable findtype write find_change + .tf.bar.findtype configure -font $uifont + .tf.bar.findtype.menu configure -font $uifont + set findloc "All fields" + tk_optionMenu .tf.bar.findloc findloc "All fields" Headline \ + Comments Author Committer + trace add variable findloc write find_change + .tf.bar.findloc configure -font $uifont + .tf.bar.findloc.menu configure -font $uifont + pack .tf.bar.findloc -side right + pack .tf.bar.findtype -side right + + # build up the bottom bar of upper window + label .tf.lbar.flabel -text "Highlight: Commits " \ + -font $uifont + pack .tf.lbar.flabel -side left -fill y + set gdttype "touching paths:" + set gm [tk_optionMenu .tf.lbar.gdttype gdttype "touching paths:" \ + "adding/removing string:"] + trace add variable gdttype write hfiles_change + $gm conf -font $uifont + .tf.lbar.gdttype conf -font $uifont + pack .tf.lbar.gdttype -side left -fill y + entry .tf.lbar.fent -width 25 -font $textfont \ + -textvariable highlight_files + trace add variable highlight_files write hfiles_change + lappend entries .tf.lbar.fent + pack .tf.lbar.fent -side left -fill x -expand 1 + label .tf.lbar.vlabel -text " OR in view" -font $uifont + pack .tf.lbar.vlabel -side left -fill y + global viewhlmenu selectedhlview + set viewhlmenu [tk_optionMenu .tf.lbar.vhl selectedhlview None] + $viewhlmenu entryconf None -command delvhighlight + $viewhlmenu conf -font $uifont + .tf.lbar.vhl conf -font $uifont + pack .tf.lbar.vhl -side left -fill y + label .tf.lbar.rlabel -text " OR " -font $uifont + pack .tf.lbar.rlabel -side left -fill y + global highlight_related + set m [tk_optionMenu .tf.lbar.relm highlight_related None \ + "Descendent" "Not descendent" "Ancestor" "Not ancestor"] + $m conf -font $uifont + .tf.lbar.relm conf -font $uifont + trace add variable highlight_related write vrel_change + pack .tf.lbar.relm -side left -fill y + + # Finish putting the upper half of the viewer together + pack .tf.lbar -in .tf -side bottom -fill x + pack .tf.bar -in .tf -side bottom -fill x + pack .tf.histframe -fill both -side top -expand 1 + .ctop add .tf + + # now build up the bottom + panedwindow .pwbottom -orient horizontal + + # lower left, a text box over search bar, scroll bar to the right + # if we know window height, then that will set the lower text height, otherwise + # we set lower text height which will drive window height + if {[info exists geometry(main)]} { + frame .bleft -width $geometry(botwidth) + } else { + frame .bleft -width $geometry(botwidth) -height $geometry(botheight) + } + frame .bleft.top + + button .bleft.top.search -text "Search" -command dosearch \ + -font $uifont + pack .bleft.top.search -side left -padx 5 + set sstring .bleft.top.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 .bleft.ctext + text $ctext -background $bgcolor -foreground $fgcolor \ + -state disabled -font $textfont \ + -yscrollcommand scrolltext -wrap none + scrollbar .bleft.sb -command "$ctext yview" + pack .bleft.top -side top -fill x + pack .bleft.sb -side right -fill y + pack $ctext -side left -fill both -expand 1 + lappend bglist $ctext + lappend fglist $ctext + + $ctext tag conf comment -wrap $wrapcomment + $ctext tag conf filesep -font [concat $textfont bold] -back "#aaaaaa" + $ctext tag conf hunksep -fore [lindex $diffcolors 2] + $ctext tag conf d0 -fore [lindex $diffcolors 0] + $ctext tag conf d1 -fore [lindex $diffcolors 1] + $ctext tag conf m0 -fore red + $ctext tag conf m1 -fore blue + $ctext tag conf m2 -fore green + $ctext tag conf m3 -fore purple + $ctext tag conf m4 -fore brown + $ctext tag conf m5 -fore "#009090" + $ctext tag conf m6 -fore magenta + $ctext tag conf m7 -fore "#808000" + $ctext tag conf m8 -fore "#009000" + $ctext tag conf m9 -fore "#ff0080" + $ctext tag conf m10 -fore cyan + $ctext tag conf m11 -fore "#b07070" + $ctext tag conf m12 -fore "#70b0f0" + $ctext tag conf m13 -fore "#70f0b0" + $ctext tag conf m14 -fore "#f0b070" + $ctext tag conf m15 -fore "#ff70b0" + $ctext tag conf mmax -fore darkgrey + set mergemax 16 + $ctext tag conf mresult -font [concat $textfont bold] + $ctext tag conf msep -font [concat $textfont bold] + $ctext tag conf found -back yellow + + .pwbottom add .bleft + + # lower right + frame .bright + frame .bright.mode + radiobutton .bright.mode.patch -text "Patch" \ + -command reselectline -variable cmitmode -value "patch" + radiobutton .bright.mode.tree -text "Tree" \ + -command reselectline -variable cmitmode -value "tree" + grid .bright.mode.patch .bright.mode.tree -sticky ew + pack .bright.mode -side top -fill x + set cflist .bright.cfiles + set indent [font measure $mainfont "nn"] + text $cflist \ + -background $bgcolor -foreground $fgcolor \ + -font $mainfont \ + -tabs [list $indent [expr {2 * $indent}]] \ + -yscrollcommand ".bright.sb set" \ + -cursor [. cget -cursor] \ + -spacing1 1 -spacing3 1 + lappend bglist $cflist + lappend fglist $cflist + scrollbar .bright.sb -command "$cflist yview" + pack .bright.sb -side right -fill y + pack $cflist -side left -fill both -expand 1 + $cflist tag configure highlight \ + -background [$cflist cget -selectbackground] + $cflist tag configure bold -font [concat $mainfont bold] + + .pwbottom add .bright + .ctop add .pwbottom + + # restore window position if known + if {[info exists geometry(main)]} { + wm geometry . "$geometry(main)" + } + + bind .pwbottom <Configure> {resizecdetpanes %W %w} + pack .ctop -fill both -expand 1 + bindall <1> {selcanvline %W %x %y} + #bindall <B1-Motion> {selcanvline %W %x %y} + bindall <ButtonRelease-4> "allcanvs yview scroll -5 units" + bindall <ButtonRelease-5> "allcanvs yview scroll 5 units" + bindall <2> "canvscan mark %W %x %y" + bindall <B2-Motion> "canvscan dragto %W %x %y" + bindkey <Home> selfirstline + 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 . <Key-Next> "selnextpage 1" + bind . <Control-Home> "allcanvs yview moveto 0.0" + bind . <Control-End> "allcanvs yview moveto 1.0" + bind . <Control-Key-Up> "allcanvs yview scroll -1 units" + bind . <Control-Key-Down> "allcanvs yview scroll 1 units" + bind . <Control-Key-Prior> "allcanvs yview scroll -1 pages" + bind . <Control-Key-Next> "allcanvs yview scroll 1 pages" + bindkey <Key-Delete> "$ctext yview scroll -1 pages" + bindkey <Key-BackSpace> "$ctext yview scroll -1 pages" + bindkey <Key-space> "$ctext yview scroll 1 pages" + bindkey p "selnextline -1" + bindkey n "selnextline 1" + bindkey z "goback" + bindkey x "goforw" + bindkey i "selnextline -1" + bindkey k "selnextline 1" + bindkey j "goback" + bindkey l "goforw" + bindkey b "$ctext yview scroll -1 pages" + bindkey d "$ctext yview scroll 18 units" + bindkey u "$ctext yview scroll -18 units" + bindkey / {findnext 1} + bindkey <Key-Return> {findnext 0} + bindkey ? findprev + bindkey f nextfile + bind . <Control-q> doquit + bind . <Control-f> dofind + bind . <Control-g> {findnext 0} + bind . <Control-r> dosearchback + bind . <Control-s> dosearch + bind . <Control-equal> {incrfont 1} + bind . <Control-KP_Add> {incrfont 1} + bind . <Control-minus> {incrfont -1} + bind . <Control-KP_Subtract> {incrfont -1} + wm protocol . WM_DELETE_WINDOW doquit + bind . <Button-1> "click %W" + bind $fstring <Key-Return> dofind + bind $sha1entry <Key-Return> gotocommit + bind $sha1entry <<PasteSelection>> clearsha1 + bind $cflist <1> {sel_flist %W %x %y; break} + bind $cflist <B1-Motion> {sel_flist %W %x %y; break} + bind $cflist <ButtonRelease-1> {treeclick %W %x %y} + + set maincursor [. cget -cursor] + set textcursor [$ctext cget -cursor] + set curtextcursor $textcursor + + set rowctxmenu .rowctxmenu + menu $rowctxmenu -tearoff 0 + $rowctxmenu add command -label "Diff this -> selected" \ + -command {diffvssel 0} + $rowctxmenu add command -label "Diff selected -> this" \ + -command {diffvssel 1} + $rowctxmenu add command -label "Make patch" -command mkpatch + $rowctxmenu add command -label "Create tag" -command mktag + $rowctxmenu add command -label "Write commit to file" -command writecommit + $rowctxmenu add command -label "Create new branch" -command mkbranch + $rowctxmenu add command -label "Cherry-pick this commit" \ + -command cherrypick + + set headctxmenu .headctxmenu + menu $headctxmenu -tearoff 0 + $headctxmenu add command -label "Check out this branch" \ + -command cobranch + $headctxmenu add command -label "Remove this branch" \ + -command rmbranch +} + +# mouse-2 makes all windows scan vertically, but only the one +# the cursor is in scans horizontally +proc canvscan {op w x y} { + global canv canv2 canv3 + foreach c [list $canv $canv2 $canv3] { + if {$c == $w} { + $c scan $op $x $y + } else { + $c scan $op 0 $y + } + } +} + +proc scrollcanv {cscroll f0 f1} { + $cscroll set $f0 $f1 + drawfrac $f0 $f1 + flushhighlights +} + +# when we make a key binding for the toplevel, make sure +# it doesn't get triggered when that key is pressed in the +# find string entry widget. +proc bindkey {ev script} { + global entries + bind . $ev $script + set escript [bind Entry $ev] + if {$escript == {}} { + set escript [bind Entry <Key>] + } + foreach e $entries { + bind $e $ev "$escript; break" + } +} + +# set the focus back to the toplevel for any click outside +# the entry widgets +proc click {w} { + global entries + foreach e $entries { + if {$w == $e} return + } + focus . +} + +proc savestuff {w} { + global canv canv2 canv3 ctext cflist mainfont textfont uifont + global stuffsaved findmergefiles maxgraphpct + global maxwidth showneartags + global viewname viewfiles viewargs viewperm nextviewnum + global cmitmode wrapcomment + global colors bgcolor fgcolor diffcolors + + if {$stuffsaved} return + if {![winfo viewable .]} return + catch { + set f [open "~/.gitk-new" w] + puts $f [list set mainfont $mainfont] + puts $f [list set textfont $textfont] + puts $f [list set uifont $uifont] + puts $f [list set findmergefiles $findmergefiles] + 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 [list set bgcolor $bgcolor] + puts $f [list set fgcolor $fgcolor] + puts $f [list set colors $colors] + puts $f [list set diffcolors $diffcolors] + + puts $f "set geometry(main) [wm geometry .]" + puts $f "set geometry(topwidth) [winfo width .tf]" + puts $f "set geometry(topheight) [winfo height .tf]" + puts $f "set geometry(canv) [winfo width $canv]" + puts $f "set geometry(canv2) [winfo width $canv2]" + puts $f "set geometry(canv3) [winfo width $canv3]" + puts $f "set geometry(botwidth) [winfo width .bleft]" + puts $f "set geometry(botheight) [winfo height .bleft]" + + puts -nonewline $f "set permviews {" + for {set v 0} {$v < $nextviewnum} {incr v} { + if {$viewperm($v)} { + puts $f "{[list $viewname($v) $viewfiles($v) $viewargs($v)]}" + } + } + puts $f "}" + close $f + file rename -force "~/.gitk-new" "~/.gitk" + } + set stuffsaved 1 +} + +proc resizeclistpanes {win w} { + global oldwidth + if {[info exists oldwidth($win)]} { + set s0 [$win sash coord 0] + set s1 [$win sash coord 1] + if {$w < 60} { + set sash0 [expr {int($w/2 - 2)}] + set sash1 [expr {int($w*5/6 - 2)}] + } else { + set factor [expr {1.0 * $w / $oldwidth($win)}] + set sash0 [expr {int($factor * [lindex $s0 0])}] + set sash1 [expr {int($factor * [lindex $s1 0])}] + if {$sash0 < 30} { + set sash0 30 + } + if {$sash1 < $sash0 + 20} { + set sash1 [expr {$sash0 + 20}] + } + if {$sash1 > $w - 10} { + set sash1 [expr {$w - 10}] + if {$sash0 > $sash1 - 20} { + set sash0 [expr {$sash1 - 20}] + } + } + } + $win sash place 0 $sash0 [lindex $s0 1] + $win sash place 1 $sash1 [lindex $s1 1] + } + set oldwidth($win) $w +} + +proc resizecdetpanes {win w} { + global oldwidth + if {[info exists oldwidth($win)]} { + set s0 [$win sash coord 0] + if {$w < 60} { + set sash0 [expr {int($w*3/4 - 2)}] + } else { + set factor [expr {1.0 * $w / $oldwidth($win)}] + set sash0 [expr {int($factor * [lindex $s0 0])}] + if {$sash0 < 45} { + set sash0 45 + } + if {$sash0 > $w - 15} { + set sash0 [expr {$w - 15}] + } + } + $win sash place 0 $sash0 [lindex $s0 1] + } + set oldwidth($win) $w +} + +proc allcanvs args { + global canv canv2 canv3 + eval $canv $args + eval $canv2 $args + eval $canv3 $args +} + +proc bindall {event action} { + global canv canv2 canv3 + bind $canv $event $action + bind $canv2 $event $action + bind $canv3 $event $action +} + +proc about {} { + set w .about + if {[winfo exists $w]} { + raise $w + return + } + toplevel $w + wm title $w "About gitk" + message $w.m -text { +Gitk - a commit viewer for git + +Copyright © 2005-2006 Paul Mackerras + +Use and redistribute under the terms of the GNU General Public License} \ + -justify center -aspect 400 + pack $w.m -side top -fill x -padx 20 -pady 20 + button $w.ok -text Close -command "destroy $w" + pack $w.ok -side bottom +} + +proc keys {} { + set w .keys + if {[winfo exists $w]} { + raise $w + return + } + toplevel $w + wm title $w "Gitk key bindings" + message $w.m -text { +Gitk key bindings: + +<Ctrl-Q> Quit +<Home> Move to first commit +<End> Move to last commit +<Up>, p, i Move up one commit +<Down>, n, k Move down one commit +<Left>, z, j Go back in history list +<Right>, x, l Go forward in history list +<PageUp> Move up one page in commit list +<PageDown> Move down one page in commit list +<Ctrl-Home> Scroll to top of commit list +<Ctrl-End> Scroll to bottom of commit list +<Ctrl-Up> Scroll commit list up one line +<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 +u Scroll diff view up 18 lines +d Scroll diff view down 18 lines +<Ctrl-F> Find +<Ctrl-G> Move to next 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 +<Ctrl-minus> Decrease font size +} \ + -justify left -bg white -border 2 -relief sunken + pack $w.m -side top -fill both + button $w.ok -text Close -command "destroy $w" + pack $w.ok -side bottom +} + +# Procedures for manipulating the file list window at the +# bottom right of the overall window. + +proc treeview {w l openlevs} { + global treecontents treediropen treeheight treeparent treeindex + + set ix 0 + set treeindex() 0 + set lev 0 + set prefix {} + set prefixend -1 + set prefendstack {} + set htstack {} + set ht 0 + set treecontents() {} + $w conf -state normal + foreach f $l { + while {[string range $f 0 $prefixend] ne $prefix} { + if {$lev <= $openlevs} { + $w mark set e:$treeindex($prefix) "end -1c" + $w mark gravity e:$treeindex($prefix) left + } + set treeheight($prefix) $ht + incr ht [lindex $htstack end] + set htstack [lreplace $htstack end end] + set prefixend [lindex $prefendstack end] + set prefendstack [lreplace $prefendstack end end] + set prefix [string range $prefix 0 $prefixend] + incr lev -1 + } + set tail [string range $f [expr {$prefixend+1}] end] + while {[set slash [string first "/" $tail]] >= 0} { + lappend htstack $ht + set ht 0 + lappend prefendstack $prefixend + incr prefixend [expr {$slash + 1}] + set d [string range $tail 0 $slash] + lappend treecontents($prefix) $d + set oldprefix $prefix + append prefix $d + set treecontents($prefix) {} + set treeindex($prefix) [incr ix] + set treeparent($prefix) $oldprefix + set tail [string range $tail [expr {$slash+1}] end] + if {$lev <= $openlevs} { + set ht 1 + set treediropen($prefix) [expr {$lev < $openlevs}] + set bm [expr {$lev == $openlevs? "tri-rt": "tri-dn"}] + $w mark set d:$ix "end -1c" + $w mark gravity d:$ix left + set str "\n" + for {set i 0} {$i < $lev} {incr i} {append str "\t"} + $w insert end $str + $w image create end -align center -image $bm -padx 1 \ + -name a:$ix + $w insert end $d [highlight_tag $prefix] + $w mark set s:$ix "end -1c" + $w mark gravity s:$ix left + } + incr lev + } + if {$tail ne {}} { + if {$lev <= $openlevs} { + incr ht + set str "\n" + for {set i 0} {$i < $lev} {incr i} {append str "\t"} + $w insert end $str + $w insert end $tail [highlight_tag $f] + } + lappend treecontents($prefix) $tail + } + } + while {$htstack ne {}} { + set treeheight($prefix) $ht + incr ht [lindex $htstack end] + set htstack [lreplace $htstack end end] + } + $w conf -state disabled +} + +proc linetoelt {l} { + global treeheight treecontents + + set y 2 + set prefix {} + while {1} { + foreach e $treecontents($prefix) { + if {$y == $l} { + return "$prefix$e" + } + set n 1 + if {[string index $e end] eq "/"} { + set n $treeheight($prefix$e) + if {$y + $n > $l} { + append prefix $e + incr y + break + } + } + incr y $n + } + } +} + +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 + + set ix $treeindex($dir) + $w conf -state normal + $w delete s:$ix e:$ix + set treediropen($dir) 0 + $w image configure a:$ix -image tri-rt + $w conf -state disabled + set n [expr {1 - $treeheight($dir)}] + while {$dir ne {}} { + incr treeheight($dir) $n + set dir $treeparent($dir) + } +} + +proc treeopendir {w dir} { + global treediropen treeheight treeparent treecontents treeindex + + set ix $treeindex($dir) + $w conf -state normal + $w image configure a:$ix -image tri-dn + $w mark set e:$ix s:$ix + $w mark gravity e:$ix right + set lev 0 + set str "\n" + set n [llength $treecontents($dir)] + for {set x $dir} {$x ne {}} {set x $treeparent($x)} { + incr lev + append str "\t" + incr treeheight($x) $n + } + foreach e $treecontents($dir) { + set de $dir$e + if {[string index $e end] eq "/"} { + set iy $treeindex($de) + $w mark set d:$iy e:$ix + $w mark gravity d:$iy left + $w insert e:$ix $str + set treediropen($de) 0 + $w image create e:$ix -align center -image tri-rt -padx 1 \ + -name a:$iy + $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 [highlight_tag $de] + } + } + $w mark gravity e:$ix left + $w conf -state disabled + set treediropen($dir) 1 + set top [lindex [split [$w index @0,0] .] 0] + set ht [$w cget -height] + set l [lindex [split [$w index s:$ix] .] 0] + if {$l < $top} { + $w yview $l.0 + } elseif {$l + $n + 1 > $top + $ht} { + set top [expr {$l + $n + 2 - $ht}] + if {$l < $top} { + set top $l + } + $w yview $top.0 + } +} + +proc treeclick {w x y} { + global treediropen cmitmode ctext cflist cflist_top + + if {$cmitmode ne "tree"} return + if {![info exists cflist_top]} return + set l [lindex [split [$w index "@$x,$y"] "."] 0] + $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend" + $cflist tag add highlight $l.0 "$l.0 lineend" + set cflist_top $l + if {$l == 1} { + $ctext yview 1.0 + return + } + set e [linetoelt $l] + if {[string index $e end] ne "/"} { + showfile $e + } elseif {$treediropen($e)} { + treeclosedir $w $e + } else { + treeopendir $w $e + } +} + +proc setfilelist {id} { + global treefilelist cflist + + treeview $cflist $treefilelist($id) 0 +} + +image create bitmap tri-rt -background black -foreground blue -data { + #define tri-rt_width 13 + #define tri-rt_height 13 + static unsigned char tri-rt_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x70, 0x00, 0xf0, 0x00, + 0xf0, 0x01, 0xf0, 0x00, 0x70, 0x00, 0x30, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00}; +} -maskdata { + #define tri-rt-mask_width 13 + #define tri-rt-mask_height 13 + static unsigned char tri-rt-mask_bits[] = { + 0x08, 0x00, 0x18, 0x00, 0x38, 0x00, 0x78, 0x00, 0xf8, 0x00, 0xf8, 0x01, + 0xf8, 0x03, 0xf8, 0x01, 0xf8, 0x00, 0x78, 0x00, 0x38, 0x00, 0x18, 0x00, + 0x08, 0x00}; +} +image create bitmap tri-dn -background black -foreground blue -data { + #define tri-dn_width 13 + #define tri-dn_height 13 + static unsigned char tri-dn_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x07, 0xf8, 0x03, + 0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00}; +} -maskdata { + #define tri-dn-mask_width 13 + #define tri-dn-mask_height 13 + static unsigned char tri-dn-mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x1f, 0xfe, 0x0f, 0xfc, 0x07, + 0xf8, 0x03, 0xf0, 0x01, 0xe0, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00}; +} + +proc init_flist {first} { + global cflist cflist_top selectedline difffilestart + + $cflist conf -state normal + $cflist delete 0.0 end + if {$first ne {}} { + $cflist insert end $first + set cflist_top 1 + $cflist tag add highlight 1.0 "1.0 lineend" + } else { + catch {unset cflist_top} + } + $cflist conf -state disabled + set difffilestart {} +} + +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 {$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 ctext difffilestart cflist cflist_top cmitmode + + if {$cmitmode eq "tree"} return + if {![info exists cflist_top]} return + set l [lindex [split [$w index "@$x,$y"] "."] 0] + $cflist tag remove highlight $cflist_top.0 "$cflist_top.0 lineend" + $cflist tag add highlight $l.0 "$l.0 lineend" + set cflist_top $l + if {$l == 1} { + $ctext yview 1.0 + } else { + catch {$ctext yview [lindex $difffilestart [expr {$l - 2}]]} + } +} + +# Functions for adding and removing shell-type quoting + +proc shellquote {str} { + if {![string match "*\['\"\\ \t]*" $str]} { + return $str + } + if {![string match "*\['\"\\]*" $str]} { + return "\"$str\"" + } + if {![string match "*'*" $str]} { + return "'$str'" + } + return "\"[string map {\" \\\" \\ \\\\} $str]\"" +} + +proc shellarglist {l} { + set str {} + foreach a $l { + if {$str ne {}} { + append str " " + } + append str [shellquote $a] + } + return $str +} + +proc shelldequote {str} { + set ret {} + set used -1 + while {1} { + incr used + if {![regexp -start $used -indices "\['\"\\\\ \t]" $str first]} { + append ret [string range $str $used end] + set used [string length $str] + break + } + set first [lindex $first 0] + set ch [string index $str $first] + if {$first > $used} { + append ret [string range $str $used [expr {$first - 1}]] + set used $first + } + if {$ch eq " " || $ch eq "\t"} break + incr used + if {$ch eq "'"} { + set first [string first "'" $str $used] + if {$first < 0} { + error "unmatched single-quote" + } + append ret [string range $str $used [expr {$first - 1}]] + set used $first + continue + } + if {$ch eq "\\"} { + if {$used >= [string length $str]} { + error "trailing backslash" + } + append ret [string index $str $used] + continue + } + # here ch == "\"" + while {1} { + if {![regexp -start $used -indices "\[\"\\\\]" $str first]} { + error "unmatched double-quote" + } + set first [lindex $first 0] + set ch [string index $str $first] + if {$first > $used} { + append ret [string range $str $used [expr {$first - 1}]] + set used $first + } + if {$ch eq "\""} break + incr used + append ret [string index $str $used] + incr used + } + } + return [list $used $ret] +} + +proc shellsplit {str} { + set l {} + while {1} { + set str [string trimleft $str] + if {$str eq {}} break + set dq [shelldequote $str] + set n [lindex $dq 0] + set word [lindex $dq 1] + set str [string range $str $n end] + lappend l $word + } + return $l +} + +# Code to implement multiple views + +proc newview {ishighlight} { + global nextviewnum newviewname newviewperm uifont newishighlight + global newviewargs revtreeargs + + set newishighlight $ishighlight + set top .gitkview + if {[winfo exists $top]} { + raise $top + return + } + set newviewname($nextviewnum) "View $nextviewnum" + set newviewperm($nextviewnum) 0 + set newviewargs($nextviewnum) [shellarglist $revtreeargs] + vieweditor $top $nextviewnum "Gitk view definition" +} + +proc editview {} { + global curview + global viewname viewperm newviewname newviewperm + global viewargs newviewargs + + set top .gitkvedit-$curview + if {[winfo exists $top]} { + raise $top + return + } + set newviewname($curview) $viewname($curview) + set newviewperm($curview) $viewperm($curview) + set newviewargs($curview) [shellarglist $viewargs($curview)] + vieweditor $top $curview "Gitk: edit view $viewname($curview)" +} + +proc vieweditor {top n title} { + global newviewname newviewperm viewfiles + global uifont + + toplevel $top + wm title $top $title + label $top.nl -text "Name" -font $uifont + entry $top.name -width 20 -textvariable newviewname($n) + grid $top.nl $top.name -sticky w -pady 5 + 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):" + grid $top.al - -sticky w -pady 5 + entry $top.args -width 50 -textvariable newviewargs($n) \ + -background white + grid $top.args - -sticky ew -padx 5 + message $top.l -aspect 1000 -font $uifont \ + -text "Enter files and directories to include, one per line:" + grid $top.l - -sticky w + text $top.t -width 40 -height 10 -background white + if {[info exists viewfiles($n)]} { + foreach f $viewfiles($n) { + $top.t insert end $f + $top.t insert end "\n" + } + $top.t delete {end - 1c} end + $top.t mark set insert 0.0 + } + grid $top.t - -sticky ew -padx 5 + frame $top.buts + button $top.buts.ok -text "OK" -command [list newviewok $top $n] + button $top.buts.can -text "Cancel" -command [list destroy $top] + grid $top.buts.ok $top.buts.can + grid columnconfigure $top.buts 0 -weight 1 -uniform a + grid columnconfigure $top.buts 1 -weight 1 -uniform a + grid $top.buts - -pady 10 -sticky ew + focus $top.t +} + +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 $argv + break + } + } +} + +proc allviewmenus {n op args} { + global viewhlmenu + + doviewmenu .bar.view 5 [list showview $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 viewhlmenu + + if {[catch { + set newargs [shellsplit $newviewargs($n)] + } err]} { + error_popup "Error in commit selection arguments: $err" + wm raise $top + focus $top + return + } + set files {} + foreach f [split [$top.t get 0.0 end] "\n"] { + set ft [string trim $f] + if {$ft ne {}} { + lappend files $ft + } + } + if {![info exists viewfiles($n)]} { + # creating a new view + incr nextviewnum + set viewname($n) $newviewname($n) + set viewperm($n) $newviewperm($n) + set viewfiles($n) $files + set viewargs($n) $newargs + addviewmenu $n + if {!$newishighlight} { + after idle showview $n + } else { + 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) + doviewmenu .bar.view 5 [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 + set viewargs($n) $newargs + if {$curview == $n} { + after idle updatecommits + } + } + } + catch {destroy $top} +} + +proc delview {} { + 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 + showview 0 +} + +proc addviewmenu {n} { + global viewname viewhlmenu + + .bar.view add radiobutton -label $viewname($n) \ + -command [list showview $n] -variable selectedview -value $n + $viewhlmenu add radiobutton -label $viewname($n) \ + -command [list addvhighlight $n] -variable selectedhlview +} + +proc flatten {var} { + global $var + + set ret {} + foreach i [array names $var] { + lappend ret $i [set $var\($i\)] + } + return $ret +} + +proc unflatten {var l} { + global $var + + catch {unset $var} + foreach {i v} $l { + set $var\($i\) $v + } +} + +proc showview {n} { + global curview viewdata viewfiles + global displayorder parentlist childlist rowidlist rowoffsets + global colormap rowtextx commitrow nextcolor canvxmax + global numcommits rowrangelist commitlisted idrowranges + global selectedline currentid canv canvy0 + global matchinglines treediffs + global pending_select phase + global commitidx rowlaidout rowoptim linesegends + global commfd nextupdate + global selectedview + global vparentlist vchildlist vdisporder vcmitlisted + global hlview selectedhlview + + if {$n == $curview} return + set selid {} + if {[info exists selectedline]} { + set selid $currentid + set y [yc $selectedline] + set ymax [lindex [$canv cget -scrollregion] 3] + set span [$canv yview] + set ytop [expr {[lindex $span 0] * $ymax}] + set ybot [expr {[lindex $span 1] * $ymax}] + if {$ytop < $y && $y < $ybot} { + set yscreen [expr {$y - $ytop}] + } else { + set yscreen [expr {($ybot - $ytop) / 2}] + } + } + unselectline + normalline + stopfindproc + if {$curview >= 0} { + set vparentlist($curview) $parentlist + set vchildlist($curview) $childlist + set vdisporder($curview) $displayorder + set vcmitlisted($curview) $commitlisted + if {$phase ne {}} { + set viewdata($curview) \ + [list $phase $rowidlist $rowoffsets $rowrangelist \ + [flatten idrowranges] [flatten idinlist] \ + $rowlaidout $rowoptim $numcommits $linesegends] + } elseif {![info exists viewdata($curview)] + || [lindex $viewdata($curview) 0] ne {}} { + set viewdata($curview) \ + [list {} $rowidlist $rowoffsets $rowrangelist] + } + } + 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 + .bar.view entryconf Edit* -state [expr {$n == 0? "disabled": "normal"}] + .bar.view entryconf Delete* -state [expr {$n == 0? "disabled": "normal"}] + + if {![info exists viewdata($n)]} { + set pending_select $selid + getcommits + return + } + + set v $viewdata($n) + set phase [lindex $v 0] + set displayorder $vdisporder($n) + set parentlist $vparentlist($n) + set childlist $vchildlist($n) + set commitlisted $vcmitlisted($n) + set rowidlist [lindex $v 1] + set rowoffsets [lindex $v 2] + set rowrangelist [lindex $v 3] + if {$phase eq {}} { + set numcommits [llength $displayorder] + catch {unset idrowranges} + } else { + unflatten idrowranges [lindex $v 4] + unflatten idinlist [lindex $v 5] + set rowlaidout [lindex $v 6] + set rowoptim [lindex $v 7] + set numcommits [lindex $v 8] + set linesegends [lindex $v 9] + } + + catch {unset colormap} + catch {unset rowtextx} + set nextcolor 0 + set canvxmax [$canv cget -width] + set curview $n + set row 0 + setcanvscroll + set yf 0 + set row 0 + if {$selid ne {} && [info exists commitrow($n,$selid)]} { + set row $commitrow($n,$selid) + # try to get the selected row in the same position on the screen + set ymax [lindex [$canv cget -scrollregion] 3] + set ytop [expr {[yc $row] - $yscreen}] + if {$ytop < 0} { + set ytop 0 + } + set yf [expr {$ytop * 1.0 / $ymax}] + } + allcanvs yview moveto $yf + drawvisible + selectline $row 0 + if {$phase ne {}} { + if {$phase eq "getcommits"} { + show_status "Reading commits..." + } + if {[info exists commfd($n)]} { + layoutmore {} + } else { + finishcommits + } + } elseif {$numcommits == 0} { + show_status "No commits selected" + } +} + +# 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]} { + delvhighlight + } + set hlview $n + if {$n != $curview && ![info exists viewdata($n)]} { + set viewdata($n) [list getcommits {{}} {{}} {} {} {} 0 0 0 {}] + set vparentlist($n) {} + set vchildlist($n) {} + set vdisporder($n) {} + set vcmitlisted($n) {} + start_rev_list $n + } + set vhl_done $commitidx($hlview) + if {$vhl_done > 0} { + drawvisible + } +} + +proc delvhighlight {} { + global hlview vhighlights + + if {![info exists hlview]} return + unset hlview + catch {unset vhighlights} + unbolden +} + +proc vhighlightmore {} { + global hlview vhl_done commitidx vhighlights + global displayorder vdisporder curview mainfont + + set font [concat $mainfont bold] + set max $commitidx($hlview) + if {$hlview == $curview} { + set disp $displayorder + } else { + set disp $vdisporder($hlview) + } + 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 {$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 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 + +proc shortids {ids} { + set res {} + foreach id $ids { + if {[llength $id] > 1} { + lappend res [shortids $id] + } elseif {[regexp {^[0-9a-f]{40}$} $id]} { + lappend res [string range $id 0 7] + } else { + lappend res $id + } + } + return $res +} + +proc incrange {l x o} { + set n [llength $l] + while {$x < $n} { + set e [lindex $l $x] + if {$e ne {}} { + lset l $x [expr {$e + $o}] + } + incr x + } + return $l +} + +proc ntimes {n o} { + set ret {} + for {} {$n > 0} {incr n -1} { + lappend ret $o + } + return $ret +} + +proc usedinrange {id l1 l2} { + global children commitrow childlist curview + + if {[info exists commitrow($curview,$id)]} { + set r $commitrow($curview,$id) + if {$l1 <= $r && $r <= $l2} { + return [expr {$r - $l1 + 1}] + } + set kids [lindex $childlist $r] + } else { + set kids $children($curview,$id) + } + foreach c $kids { + set r $commitrow($curview,$c) + if {$l1 <= $r && $r <= $l2} { + return [expr {$r - $l1 + 1}] + } + } + return 0 +} + +proc sanity {row {full 0}} { + global rowidlist rowoffsets + + set col -1 + set ids [lindex $rowidlist $row] + foreach id $ids { + incr col + if {$id eq {}} continue + if {$col < [llength $ids] - 1 && + [lsearch -exact -start [expr {$col+1}] $ids $id] >= 0} { + puts "oops: [shortids $id] repeated in row $row col $col: {[shortids [lindex $rowidlist $row]]}" + } + set o [lindex $rowoffsets $row $col] + set y $row + set x $col + while {$o ne {}} { + incr y -1 + incr x $o + if {[lindex $rowidlist $y $x] != $id} { + puts "oops: rowoffsets wrong at row [expr {$y+1}] col [expr {$x-$o}]" + puts " id=[shortids $id] check started at row $row" + for {set i $row} {$i >= $y} {incr i -1} { + puts " row $i ids={[shortids [lindex $rowidlist $i]]} offs={[lindex $rowoffsets $i]}" + } + break + } + if {!$full} break + set o [lindex $rowoffsets $y $x] + } + } +} + +proc makeuparrow {oid x y z} { + global rowidlist rowoffsets uparrowlen idrowranges + + for {set i 1} {$i < $uparrowlen && $y > 1} {incr i} { + incr y -1 + incr x $z + set off0 [lindex $rowoffsets $y] + for {set x0 $x} {1} {incr x0} { + if {$x0 >= [llength $off0]} { + set x0 [llength [lindex $rowoffsets [expr {$y-1}]]] + break + } + set z [lindex $off0 $x0] + if {$z ne {}} { + incr x0 $z + break + } + } + set z [expr {$x0 - $x}] + lset rowidlist $y [linsert [lindex $rowidlist $y] $x $oid] + lset rowoffsets $y [linsert [lindex $rowoffsets $y] $x $z] + } + set tmp [lreplace [lindex $rowoffsets $y] $x $x {}] + lset rowoffsets $y [incrange $tmp [expr {$x+1}] -1] + lappend idrowranges($oid) $y +} + +proc initlayout {} { + global rowidlist rowoffsets displayorder commitlisted + global rowlaidout rowoptim + global idinlist rowchk rowrangelist idrowranges + global numcommits canvxmax canv + global nextcolor + global parentlist childlist children + global colormap rowtextx + global linesegends + + set numcommits 0 + set displayorder {} + set commitlisted {} + set parentlist {} + set childlist {} + set rowrangelist {} + set nextcolor 0 + set rowidlist {{}} + set rowoffsets {{}} + catch {unset idinlist} + catch {unset rowchk} + set rowlaidout 0 + set rowoptim 0 + set canvxmax [$canv cget -width] + catch {unset colormap} + catch {unset rowtextx} + catch {unset idrowranges} + set linesegends {} +} + +proc setcanvscroll {} { + global canv canv2 canv3 numcommits linespc canvxmax canvy0 + + set ymax [expr {$canvy0 + ($numcommits - 0.5) * $linespc + 2}] + $canv conf -scrollregion [list 0 0 $canvxmax $ymax] + $canv2 conf -scrollregion [list 0 0 0 $ymax] + $canv3 conf -scrollregion [list 0 0 0 $ymax] +} + +proc visiblerows {} { + global canv numcommits linespc + + set ymax [lindex [$canv cget -scrollregion] 3] + if {$ymax eq {} || $ymax == 0} return + set f [$canv yview] + set y0 [expr {int([lindex $f 0] * $ymax)}] + set r0 [expr {int(($y0 - 3) / $linespc) - 1}] + if {$r0 < 0} { + set r0 0 + } + set y1 [expr {int([lindex $f 1] * $ymax)}] + set r1 [expr {int(($y1 - 3) / $linespc) + 1}] + if {$r1 >= $numcommits} { + set r1 [expr {$numcommits - 1}] + } + return [list $r0 $r1] +} + +proc layoutmore {tmax} { + global rowlaidout rowoptim commitidx numcommits optim_delay + global uparrowlen curview + + while {1} { + if {$rowoptim - $optim_delay > $numcommits} { + showstuff [expr {$rowoptim - $optim_delay}] + } elseif {$rowlaidout - $uparrowlen - 1 > $rowoptim} { + set nr [expr {$rowlaidout - $uparrowlen - 1 - $rowoptim}] + if {$nr > 100} { + set nr 100 + } + optimize_rows $rowoptim 0 [expr {$rowoptim + $nr}] + incr rowoptim $nr + } elseif {$commitidx($curview) > $rowlaidout} { + set nr [expr {$commitidx($curview) - $rowlaidout}] + # may need to increase this threshold if uparrowlen or + # mingaplen are increased... + if {$nr > 150} { + set nr 150 + } + set row $rowlaidout + set rowlaidout [layoutrows $row [expr {$row + $nr}] 0] + if {$rowlaidout == $row} { + return 0 + } + } else { + return 0 + } + if {$tmax ne {} && [clock clicks -milliseconds] >= $tmax} { + return 1 + } + } +} + +proc showstuff {canshow} { + global numcommits commitrow pending_select selectedline + global linesegends idrowranges idrangedrawn curview + + if {$numcommits == 0} { + global phase + set phase "incrdraw" + allcanvs delete all + } + set row $numcommits + set numcommits $canshow + setcanvscroll + set rows [visiblerows] + set r0 [lindex $rows 0] + set r1 [lindex $rows 1] + set selrow -1 + for {set r $row} {$r < $canshow} {incr r} { + foreach id [lindex $linesegends [expr {$r+1}]] { + set i -1 + foreach {s e} [rowranges $id] { + incr i + if {$e ne {} && $e < $numcommits && $s <= $r1 && $e >= $r0 + && ![info exists idrangedrawn($id,$i)]} { + drawlineseg $id $i + set idrangedrawn($id,$i) 1 + } + } + } + } + if {$canshow > $r1} { + set canshow $r1 + } + while {$row < $canshow} { + drawcmitrow $row + incr row + } + if {[info exists pending_select] && + [info exists commitrow($curview,$pending_select)] && + $commitrow($curview,$pending_select) < $numcommits} { + selectline $commitrow($curview,$pending_select) 1 + } + if {![info exists selectedline] && ![info exists pending_select]} { + selectline 0 1 + } +} + +proc layoutrows {row endrow last} { + global rowidlist rowoffsets displayorder + global uparrowlen downarrowlen maxwidth mingaplen + global childlist parentlist + global idrowranges linesegends + global commitidx curview + global idinlist rowchk rowrangelist + + set idlist [lindex $rowidlist $row] + set offs [lindex $rowoffsets $row] + while {$row < $endrow} { + set id [lindex $displayorder $row] + set oldolds {} + set newolds {} + foreach p [lindex $parentlist $row] { + if {![info exists idinlist($p)]} { + lappend newolds $p + } elseif {!$idinlist($p)} { + lappend oldolds $p + } + } + set lse {} + set nev [expr {[llength $idlist] + [llength $newolds] + + [llength $oldolds] - $maxwidth + 1}] + if {$nev > 0} { + if {!$last && + $row + $uparrowlen + $mingaplen >= $commitidx($curview)} break + for {set x [llength $idlist]} {[incr x -1] >= 0} {} { + set i [lindex $idlist $x] + if {![info exists rowchk($i)] || $row >= $rowchk($i)} { + set r [usedinrange $i [expr {$row - $downarrowlen}] \ + [expr {$row + $uparrowlen + $mingaplen}]] + if {$r == 0} { + set idlist [lreplace $idlist $x $x] + set offs [lreplace $offs $x $x] + set offs [incrange $offs $x 1] + set idinlist($i) 0 + set rm1 [expr {$row - 1}] + lappend lse $i + lappend idrowranges($i) $rm1 + if {[incr nev -1] <= 0} break + continue + } + set rowchk($id) [expr {$row + $r}] + } + } + lset rowidlist $row $idlist + lset rowoffsets $row $offs + } + lappend linesegends $lse + set col [lsearch -exact $idlist $id] + if {$col < 0} { + set col [llength $idlist] + lappend idlist $id + lset rowidlist $row $idlist + set z {} + if {[lindex $childlist $row] ne {}} { + set z [expr {[llength [lindex $rowidlist [expr {$row-1}]]] - $col}] + unset idinlist($id) + } + lappend offs $z + lset rowoffsets $row $offs + if {$z ne {}} { + makeuparrow $id $col $row $z + } + } else { + unset idinlist($id) + } + set ranges {} + if {[info exists idrowranges($id)]} { + set ranges $idrowranges($id) + lappend ranges $row + unset idrowranges($id) + } + lappend rowrangelist $ranges + incr row + set offs [ntimes [llength $idlist] 0] + set l [llength $newolds] + set idlist [eval lreplace \$idlist $col $col $newolds] + set o 0 + if {$l != 1} { + set offs [lrange $offs 0 [expr {$col - 1}]] + foreach x $newolds { + lappend offs {} + incr o -1 + } + incr o + set tmp [expr {[llength $idlist] - [llength $offs]}] + if {$tmp > 0} { + set offs [concat $offs [ntimes $tmp $o]] + } + } else { + lset offs $col {} + } + foreach i $newolds { + set idinlist($i) 1 + set idrowranges($i) $row + } + incr col $l + foreach oid $oldolds { + set idinlist($oid) 1 + set idlist [linsert $idlist $col $oid] + set offs [linsert $offs $col $o] + makeuparrow $oid $col $row $o + incr col + } + lappend rowidlist $idlist + lappend rowoffsets $offs + } + return $row +} + +proc addextraid {id row} { + global displayorder commitrow commitinfo + global commitidx commitlisted + global parentlist childlist children curview + + incr commitidx($curview) + lappend displayorder $id + lappend commitlisted 0 + lappend parentlist {} + set commitrow($curview,$id) $row + readcommit $id + if {![info exists commitinfo($id)]} { + set commitinfo($id) {"No commit information available"} + } + if {![info exists children($curview,$id)]} { + set children($curview,$id) {} + } + lappend childlist $children($curview,$id) +} + +proc layouttail {} { + global rowidlist rowoffsets idinlist commitidx curview + global idrowranges rowrangelist + + set row $commitidx($curview) + set idlist [lindex $rowidlist $row] + while {$idlist ne {}} { + set col [expr {[llength $idlist] - 1}] + set id [lindex $idlist $col] + addextraid $id $row + unset idinlist($id) + lappend idrowranges($id) $row + lappend rowrangelist $idrowranges($id) + unset idrowranges($id) + incr row + set offs [ntimes $col 0] + set idlist [lreplace $idlist $col $col] + lappend rowidlist $idlist + lappend rowoffsets $offs + } + + foreach id [array names idinlist] { + addextraid $id $row + lset rowidlist $row [list $id] + lset rowoffsets $row 0 + makeuparrow $id 0 $row 0 + lappend idrowranges($id) $row + lappend rowrangelist $idrowranges($id) + unset idrowranges($id) + incr row + lappend rowidlist {} + lappend rowoffsets {} + } +} + +proc insert_pad {row col npad} { + global rowidlist rowoffsets + + set pad [ntimes $npad {}] + lset rowidlist $row [eval linsert [list [lindex $rowidlist $row]] $col $pad] + set tmp [eval linsert [list [lindex $rowoffsets $row]] $col $pad] + lset rowoffsets $row [incrange $tmp [expr {$col + $npad}] [expr {-$npad}]] +} + +proc optimize_rows {row col endrow} { + global rowidlist rowoffsets idrowranges displayorder + + for {} {$row < $endrow} {incr row} { + set idlist [lindex $rowidlist $row] + set offs [lindex $rowoffsets $row] + set haspad 0 + for {} {$col < [llength $offs]} {incr col} { + if {[lindex $idlist $col] eq {}} { + set haspad 1 + continue + } + set z [lindex $offs $col] + if {$z eq {}} continue + set isarrow 0 + set x0 [expr {$col + $z}] + set y0 [expr {$row - 1}] + set z0 [lindex $rowoffsets $y0 $x0] + if {$z0 eq {}} { + set id [lindex $idlist $col] + set ranges [rowranges $id] + if {$ranges ne {} && $y0 > [lindex $ranges 0]} { + set isarrow 1 + } + } + if {$z < -1 || ($z < 0 && $isarrow)} { + set npad [expr {-1 - $z + $isarrow}] + set offs [incrange $offs $col $npad] + insert_pad $y0 $x0 $npad + if {$y0 > 0} { + optimize_rows $y0 $x0 $row + } + set z [lindex $offs $col] + set x0 [expr {$col + $z}] + set z0 [lindex $rowoffsets $y0 $x0] + } elseif {$z > 1 || ($z > 0 && $isarrow)} { + set npad [expr {$z - 1 + $isarrow}] + set y1 [expr {$row + 1}] + set offs2 [lindex $rowoffsets $y1] + set x1 -1 + foreach z $offs2 { + incr x1 + if {$z eq {} || $x1 + $z < $col} continue + if {$x1 + $z > $col} { + incr npad + } + lset rowoffsets $y1 [incrange $offs2 $x1 $npad] + break + } + set pad [ntimes $npad {}] + set idlist [eval linsert \$idlist $col $pad] + set tmp [eval linsert \$offs $col $pad] + incr col $npad + set offs [incrange $tmp $col [expr {-$npad}]] + set z [lindex $offs $col] + set haspad 1 + } + if {$z0 eq {} && !$isarrow} { + # this line links to its first child on row $row-2 + set rm2 [expr {$row - 2}] + set id [lindex $displayorder $rm2] + set xc [lsearch -exact [lindex $rowidlist $rm2] $id] + if {$xc >= 0} { + set z0 [expr {$xc - $x0}] + } + } + if {$z0 ne {} && $z < 0 && $z0 > 0} { + insert_pad $y0 $x0 1 + set offs [incrange $offs $col 1] + optimize_rows $y0 [expr {$x0 + 1}] $row + } + } + if {!$haspad} { + set o {} + for {set col [llength $idlist]} {[incr col -1] >= 0} {} { + set o [lindex $offs $col] + if {$o eq {}} { + # check if this is the link to the first child + set id [lindex $idlist $col] + set ranges [rowranges $id] + if {$ranges ne {} && $row == [lindex $ranges 0]} { + # it is, work out offset to child + set y0 [expr {$row - 1}] + set id [lindex $displayorder $y0] + set x0 [lsearch -exact [lindex $rowidlist $y0] $id] + if {$x0 >= 0} { + set o [expr {$x0 - $col}] + } + } + } + if {$o eq {} || $o <= 0} break + } + if {$o ne {} && [incr col] < [llength $idlist]} { + set y1 [expr {$row + 1}] + set offs2 [lindex $rowoffsets $y1] + set x1 -1 + foreach z $offs2 { + incr x1 + if {$z eq {} || $x1 + $z < $col} continue + lset rowoffsets $y1 [incrange $offs2 $x1 1] + break + } + set idlist [linsert $idlist $col {}] + set tmp [linsert $offs $col {}] + incr col + set offs [incrange $tmp $col -1] + } + } + lset rowidlist $row $idlist + lset rowoffsets $row $offs + set col 0 + } +} + +proc xc {row col} { + global canvx0 linespc + return [expr {$canvx0 + $col * $linespc}] +} + +proc yc {row} { + global canvy0 linespc + return [expr {$canvy0 + $row * $linespc}] +} + +proc linewidth {id} { + global thickerline lthickness + + set wid $lthickness + if {[info exists thickerline] && $id eq $thickerline} { + set wid [expr {2 * $lthickness}] + } + return $wid +} + +proc rowranges {id} { + global phase idrowranges commitrow rowlaidout rowrangelist curview + + set ranges {} + if {$phase eq {} || + ([info exists commitrow($curview,$id)] + && $commitrow($curview,$id) < $rowlaidout)} { + set ranges [lindex $rowrangelist $commitrow($curview,$id)] + } elseif {[info exists idrowranges($id)]} { + set ranges $idrowranges($id) + } + return $ranges +} + +proc drawlineseg {id i} { + global rowoffsets rowidlist + global displayorder + global canv colormap linespc + global numcommits commitrow curview + + set ranges [rowranges $id] + set downarrow 1 + if {[info exists commitrow($curview,$id)] + && $commitrow($curview,$id) < $numcommits} { + set downarrow [expr {$i < [llength $ranges] / 2 - 1}] + } else { + set downarrow 1 + } + set startrow [lindex $ranges [expr {2 * $i}]] + set row [lindex $ranges [expr {2 * $i + 1}]] + if {$startrow == $row} return + assigncolor $id + set coords {} + set col [lsearch -exact [lindex $rowidlist $row] $id] + if {$col < 0} { + puts "oops: drawline: id $id not on row $row" + return + } + set lasto {} + set ns 0 + while {1} { + set o [lindex $rowoffsets $row $col] + if {$o eq {}} break + if {$o ne $lasto} { + # changing direction + set x [xc $row $col] + set y [yc $row] + lappend coords $x $y + set lasto $o + } + incr col $o + incr row -1 + } + set x [xc $row $col] + set y [yc $row] + lappend coords $x $y + if {$i == 0} { + # draw the link to the first child as part of this line + incr row -1 + set child [lindex $displayorder $row] + set ccol [lsearch -exact [lindex $rowidlist $row] $child] + if {$ccol >= 0} { + set x [xc $row $ccol] + set y [yc $row] + if {$ccol < $col - 1} { + lappend coords [xc $row [expr {$col - 1}]] [yc $row] + } elseif {$ccol > $col + 1} { + lappend coords [xc $row [expr {$col + 1}]] [yc $row] + } + lappend coords $x $y + } + } + if {[llength $coords] < 4} return + if {$downarrow} { + # This line has an arrow at the lower end: check if the arrow is + # on a diagonal segment, and if so, work around the Tk 8.4 + # refusal to draw arrows on diagonal lines. + set x0 [lindex $coords 0] + set x1 [lindex $coords 2] + if {$x0 != $x1} { + set y0 [lindex $coords 1] + set y1 [lindex $coords 3] + if {$y0 - $y1 <= 2 * $linespc && $x1 == [lindex $coords 4]} { + # we have a nearby vertical segment, just trim off the diag bit + set coords [lrange $coords 2 end] + } else { + set slope [expr {($x0 - $x1) / ($y0 - $y1)}] + set xi [expr {$x0 - $slope * $linespc / 2}] + set yi [expr {$y0 - $linespc / 2}] + set coords [lreplace $coords 0 1 $xi $y0 $xi $yi] + } + } + } + set arrow [expr {2 * ($i > 0) + $downarrow}] + set arrow [lindex {none first last both} $arrow] + set t [$canv create line $coords -width [linewidth $id] \ + -fill $colormap($id) -tags lines.$id -arrow $arrow] + $canv lower $t + bindline $t $id +} + +proc drawparentlinks {id row col olds} { + global rowidlist canv colormap + + set row2 [expr {$row + 1}] + set x [xc $row $col] + set y [yc $row] + set y2 [yc $row2] + set ids [lindex $rowidlist $row2] + # rmx = right-most X coord used + set rmx 0 + foreach p $olds { + set i [lsearch -exact $ids $p] + if {$i < 0} { + puts "oops, parent $p of $id not in list" + continue + } + set x2 [xc $row2 $i] + if {$x2 > $rmx} { + set rmx $x2 + } + set ranges [rowranges $p] + if {$ranges ne {} && $row2 == [lindex $ranges 0] + && $row2 < [lindex $ranges 1]} { + # drawlineseg will do this one for us + continue + } + assigncolor $p + # should handle duplicated parents here... + set coords [list $x $y] + if {$i < $col - 1} { + lappend coords [xc $row [expr {$i + 1}]] $y + } elseif {$i > $col + 1} { + lappend coords [xc $row [expr {$i - 1}]] $y + } + lappend coords $x2 $y2 + set t [$canv create line $coords -width [linewidth $p] \ + -fill $colormap($p) -tags lines.$p] + $canv lower $t + bindline $t $p + } + return $rmx +} + +proc drawlines {id} { + global colormap canv + global idrangedrawn + global children iddrawn commitrow rowidlist curview + + $canv delete lines.$id + set nr [expr {[llength [rowranges $id]] / 2}] + for {set i 0} {$i < $nr} {incr i} { + if {[info exists idrangedrawn($id,$i)]} { + drawlineseg $id $i + } + } + foreach child $children($curview,$id) { + if {[info exists iddrawn($child)]} { + set row $commitrow($curview,$child) + set col [lsearch -exact [lindex $rowidlist $row] $child] + if {$col >= 0} { + drawparentlinks $child $row $col [list $id] + } + } + } +} + +proc drawcmittext {id row col rmx} { + global linespc canv canv2 canv3 canvy0 fgcolor + global commitlisted commitinfo rowidlist + global rowtextx idpos idtags idheads idotherrefs + global linehtag linentag linedtag + global mainfont canvxmax boldrows boldnamerows fgcolor + + set ofill [expr {[lindex $commitlisted $row]? "blue": "white"}] + set x [xc $row $col] + set y [yc $row] + set orad [expr {$linespc / 3}] + set t [$canv create oval [expr {$x - $orad}] [expr {$y - $orad}] \ + [expr {$x + $orad - 1}] [expr {$y + $orad - 1}] \ + -fill $ofill -outline $fgcolor -width 1 -tags circle] + $canv raise $t + $canv bind $t <1> {selcanvline {} %x %y} + set xt [xc $row [llength [lindex $rowidlist $row]]] + if {$xt < $rmx} { + set xt $rmx + } + set rowtextx($row) $xt + set idpos($id) [list $x $xt $y] + if {[info exists idtags($id)] || [info exists idheads($id)] + || [info exists idotherrefs($id)]} { + set xt [drawtags $id $x $xt $y] + } + set headline [lindex $commitinfo($id) 0] + set name [lindex $commitinfo($id) 1] + set date [lindex $commitinfo($id) 2] + set date [formatdate $date] + set font $mainfont + set nfont $mainfont + set isbold [ishighlighted $row] + if {$isbold > 0} { + lappend boldrows $row + lappend font bold + if {$isbold > 1} { + lappend boldnamerows $row + lappend nfont bold + } + } + set linehtag($row) [$canv create text $xt $y -anchor w -fill $fgcolor \ + -text $headline -font $font -tags text] + $canv bind $linehtag($row) <Button-3> "rowmenu %X %Y $id" + set linentag($row) [$canv2 create text 3 $y -anchor w -fill $fgcolor \ + -text $name -font $nfont -tags text] + set linedtag($row) [$canv3 create text 3 $y -anchor w -fill $fgcolor \ + -text $date -font $mainfont -tags text] + set xr [expr {$xt + [font measure $mainfont $headline]}] + if {$xr > $canvxmax} { + set canvxmax $xr + setcanvscroll + } +} + +proc drawcmitrow {row} { + 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] { + if {$id eq {}} continue + set i -1 + foreach {s e} [rowranges $id] { + incr i + if {$row < $s} continue + if {$e eq {}} break + if {$row <= $e} { + if {$e < $numcommits && ![info exists idrangedrawn($id,$i)]} { + drawlineseg $id $i + set idrangedrawn($id,$i) 1 + } + break + } + } + } + + 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} { + puts "oops, row $row id $id not in list" + return + } + if {![info exists commitinfo($id)]} { + getcommit $id + } + assigncolor $id + set olds [lindex $parentlist $row] + if {$olds ne {}} { + set rmx [drawparentlinks $id $row $col $olds] + } else { + set rmx 0 + } + drawcmittext $id $row $col $rmx + set iddrawn($id) 1 +} + +proc drawfrac {f0 f1} { + global numcommits canv + global linespc + + set ymax [lindex [$canv cget -scrollregion] 3] + if {$ymax eq {} || $ymax == 0} return + set y0 [expr {int($f0 * $ymax)}] + set row [expr {int(($y0 - 3) / $linespc) - 1}] + if {$row < 0} { + set row 0 + } + set y1 [expr {int($f1 * $ymax)}] + set endrow [expr {int(($y1 - 3) / $linespc) + 1}] + if {$endrow >= $numcommits} { + set endrow [expr {$numcommits - 1}] + } + for {} {$row <= $endrow} {incr row} { + drawcmitrow $row + } +} + +proc drawvisible {} { + global canv + eval drawfrac [$canv yview] +} + +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} { + global rowidlist parentlist numcommits rowoffsets displayorder + + set cross {} + set ccross {} + foreach {s e} [rowranges $id] { + if {$e >= $numcommits} { + set e [expr {$numcommits - 1}] + } + if {$e <= $s} continue + set x [lsearch -exact [lindex $rowidlist $e] $id] + if {$x < 0} { + puts "findcrossings: oops, no [shortids $id] in row $e" + continue + } + for {set row $e} {[incr row -1] >= $s} {} { + set olds [lindex $parentlist $row] + set kid [lindex $displayorder $row] + set kidx [lsearch -exact [lindex $rowidlist $row] $kid] + if {$kidx < 0} continue + set nextrow [lindex $rowidlist [expr {$row + 1}]] + foreach p $olds { + set px [lsearch -exact $nextrow $p] + if {$px < 0} continue + if {($kidx < $x && $x < $px) || ($px < $x && $x < $kidx)} { + if {[lsearch -exact $ccross $p] >= 0} continue + if {$x == $px + ($kidx < $px? -1: 1)} { + lappend ccross $p + } elseif {[lsearch -exact $cross $p] < 0} { + lappend cross $p + } + } + } + set inc [lindex $rowoffsets $row $x] + if {$inc eq {}} break + incr x $inc + } + } + return [concat $ccross {{}} $cross] +} + +proc assigncolor {id} { + global colormap colors nextcolor + global commitrow parentlist children children curview + + if {[info exists colormap($id)]} return + set ncolors [llength $colors] + if {[info exists children($curview,$id)]} { + set kids $children($curview,$id) + } else { + set kids {} + } + if {[llength $kids] == 1} { + set child [lindex $kids 0] + if {[info exists colormap($child)] + && [llength [lindex $parentlist $commitrow($curview,$child)]] == 1} { + set colormap($id) $colormap($child) + return + } + } + set badcolors {} + set origbad {} + foreach x [findcrossings $id] { + if {$x eq {}} { + # delimiter between corner crossings and other crossings + if {[llength $badcolors] >= $ncolors - 1} break + set origbad $badcolors + } + if {[info exists colormap($x)] + && [lsearch -exact $badcolors $colormap($x)] < 0} { + lappend badcolors $colormap($x) + } + } + if {[llength $badcolors] >= $ncolors} { + set badcolors $origbad + } + set origbad $badcolors + if {[llength $badcolors] < $ncolors - 1} { + foreach child $kids { + if {[info exists colormap($child)] + && [lsearch -exact $badcolors $colormap($child)] < 0} { + lappend badcolors $colormap($child) + } + foreach p [lindex $parentlist $commitrow($curview,$child)] { + if {[info exists colormap($p)] + && [lsearch -exact $badcolors $colormap($p)] < 0} { + lappend badcolors $colormap($p) + } + } + } + if {[llength $badcolors] >= $ncolors} { + set badcolors $origbad + } + } + for {set i 0} {$i <= $ncolors} {incr i} { + set c [lindex $colors $nextcolor] + if {[incr nextcolor] >= $ncolors} { + set nextcolor 0 + } + if {[lsearch -exact $badcolors $c]} break + } + set colormap($id) $c +} + +proc bindline {t id} { + global canv + + $canv bind $t <Enter> "lineenter %x %y $id" + $canv bind $t <Motion> "linemotion %x %y $id" + $canv bind $t <Leave> "lineleave $id" + $canv bind $t <Button-1> "lineclick %x %y $id 1" +} + +proc drawtags {id x xt y1} { + global idtags idheads idotherrefs mainhead + global linespc lthickness + global canv mainfont commitrow rowtextx curview fgcolor bgcolor + + set marks {} + set ntags 0 + set nheads 0 + if {[info exists idtags($id)]} { + set marks $idtags($id) + set ntags [llength $marks] + } + if {[info exists idheads($id)]} { + set marks [concat $marks $idheads($id)] + set nheads [llength $idheads($id)] + } + if {[info exists idotherrefs($id)]} { + set marks [concat $marks $idotherrefs($id)] + } + if {$marks eq {}} { + return $xt + } + + set delta [expr {int(0.5 * ($linespc - $lthickness))}] + set yt [expr {$y1 - 0.5 * $linespc}] + set yb [expr {$yt + $linespc - 1}] + set xvals {} + set wvals {} + set i -1 + foreach tag $marks { + incr i + if {$i >= $ntags && $i < $ntags + $nheads && $tag eq $mainhead} { + set wid [font measure [concat $mainfont bold] $tag] + } else { + set wid [font measure $mainfont $tag] + } + lappend xvals $xt + lappend wvals $wid + set xt [expr {$xt + $delta + $wid + $lthickness + $linespc}] + } + set t [$canv create line $x $y1 [lindex $xvals end] $y1 \ + -width $lthickness -fill black -tags tag.$id] + $canv lower $t + foreach tag $marks x $xvals wid $wvals { + set xl [expr {$x + $delta}] + set xr [expr {$x + $delta + $wid + $lthickness}] + set font $mainfont + if {[incr ntags -1] >= 0} { + # draw a tag + set t [$canv create polygon $x [expr {$yt + $delta}] $xl $yt \ + $xr $yt $xr $yb $xl $yb $x [expr {$yb - $delta}] \ + -width 1 -outline black -fill yellow -tags tag.$id] + $canv bind $t <1> [list showtag $tag 1] + set rowtextx($commitrow($curview,$id)) [expr {$xr + $linespc}] + } else { + # draw a head or other ref + if {[incr nheads -1] >= 0} { + set col green + if {$tag eq $mainhead} { + lappend font bold + } + } else { + set col "#ddddff" + } + set xl [expr {$xl - $delta/2}] + $canv create polygon $x $yt $xr $yt $xr $yb $x $yb \ + -width 1 -outline black -fill $col -tags tag.$id + if {[regexp {^(remotes/.*/|remotes/)} $tag match remoteprefix]} { + set rwid [font measure $mainfont $remoteprefix] + set xi [expr {$x + 1}] + set yti [expr {$yt + 1}] + set xri [expr {$x + $rwid}] + $canv create polygon $xi $yti $xri $yti $xri $yb $xi $yb \ + -width 0 -fill "#ffddaa" -tags tag.$id + } + } + set t [$canv create text $xl $y1 -anchor w -text $tag -fill $fgcolor \ + -font $font -tags [list tag.$id text]] + if {$ntags >= 0} { + $canv bind $t <1> [list showtag $tag 1] + } elseif {$nheads >= 0} { + $canv bind $t <Button-3> [list headmenu %X %Y $id $tag] + } + } + return $xt +} + +proc xcoord {i level ln} { + global canvx0 xspc1 xspc2 + + set x [expr {$canvx0 + $i * $xspc1($ln)}] + if {$i > 0 && $i == $level} { + set x [expr {$x + 0.5 * ($xspc2 - $xspc1($ln))}] + } elseif {$i > $level} { + set x [expr {$x + $xspc2 - $xspc1($ln)}] + } + return $x +} + +proc show_status {msg} { + global canv mainfont fgcolor + + clear_display + $canv create text 3 3 -anchor nw -text $msg -font $mainfont \ + -tags text -fill $fgcolor +} + +proc finishcommits {} { + global commitidx phase curview + global pending_select + + if {$commitidx($curview) > 0} { + drawrest + } else { + show_status "No commits selected" + } + set phase {} + catch {unset pending_select} +} + +# Insert a new commit as the child of the commit on row $row. +# The new commit will be displayed on row $row and the commits +# on that row and below will move down one row. +proc insertrow {row newcmit} { + global displayorder parentlist childlist commitlisted + global commitrow curview rowidlist rowoffsets numcommits + global rowrangelist idrowranges rowlaidout rowoptim numcommits + global linesegends selectedline + + if {$row >= $numcommits} { + puts "oops, inserting new row $row but only have $numcommits rows" + return + } + set p [lindex $displayorder $row] + set displayorder [linsert $displayorder $row $newcmit] + set parentlist [linsert $parentlist $row $p] + set kids [lindex $childlist $row] + lappend kids $newcmit + lset childlist $row $kids + set childlist [linsert $childlist $row {}] + set commitlisted [linsert $commitlisted $row 1] + set l [llength $displayorder] + for {set r $row} {$r < $l} {incr r} { + set id [lindex $displayorder $r] + set commitrow($curview,$id) $r + } + + set idlist [lindex $rowidlist $row] + set offs [lindex $rowoffsets $row] + set newoffs {} + foreach x $idlist { + if {$x eq {} || ($x eq $p && [llength $kids] == 1)} { + lappend newoffs {} + } else { + lappend newoffs 0 + } + } + if {[llength $kids] == 1} { + set col [lsearch -exact $idlist $p] + lset idlist $col $newcmit + } else { + set col [llength $idlist] + lappend idlist $newcmit + lappend offs {} + lset rowoffsets $row $offs + } + set rowidlist [linsert $rowidlist $row $idlist] + set rowoffsets [linsert $rowoffsets [expr {$row+1}] $newoffs] + + set rowrangelist [linsert $rowrangelist $row {}] + set l [llength $rowrangelist] + for {set r 0} {$r < $l} {incr r} { + set ranges [lindex $rowrangelist $r] + if {$ranges ne {} && [lindex $ranges end] >= $row} { + set newranges {} + foreach x $ranges { + if {$x >= $row} { + lappend newranges [expr {$x + 1}] + } else { + lappend newranges $x + } + } + lset rowrangelist $r $newranges + } + } + if {[llength $kids] > 1} { + set rp1 [expr {$row + 1}] + set ranges [lindex $rowrangelist $rp1] + if {$ranges eq {}} { + set ranges [list $row $rp1] + } elseif {[lindex $ranges end-1] == $rp1} { + lset ranges end-1 $row + } + lset rowrangelist $rp1 $ranges + } + foreach id [array names idrowranges] { + set ranges $idrowranges($id) + if {$ranges ne {} && [lindex $ranges end] >= $row} { + set newranges {} + foreach x $ranges { + if {$x >= $row} { + lappend newranges [expr {$x + 1}] + } else { + lappend newranges $x + } + } + set idrowranges($id) $newranges + } + } + + set linesegends [linsert $linesegends $row {}] + + incr rowlaidout + incr rowoptim + incr numcommits + + if {[info exists selectedline] && $selectedline >= $row} { + incr selectedline + } + redisplay +} + +# Don't change the text pane cursor if it is currently the hand cursor, +# showing that we are over a sha1 ID link. +proc settextcursor {c} { + global ctext curtextcursor + + if {[$ctext cget -cursor] == $curtextcursor} { + $ctext config -cursor $c + } + set curtextcursor $c +} + +proc nowbusy {what} { + global isbusy + + if {[array names isbusy] eq {}} { + . config -cursor watch + settextcursor watch + } + set isbusy($what) 1 +} + +proc notbusy {what} { + global isbusy maincursor textcursor + + catch {unset isbusy($what)} + if {[array names isbusy] eq {}} { + . config -cursor $maincursor + settextcursor $textcursor + } +} + +proc drawrest {} { + global startmsecs + global rowlaidout commitidx curview + global pending_select + + set row $rowlaidout + layoutrows $rowlaidout $commitidx($curview) 1 + layouttail + optimize_rows $row 0 $commitidx($curview) + showstuff $commitidx($curview) + if {[info exists pending_select]} { + selectline 0 1 + } + + set drawmsecs [expr {[clock clicks -milliseconds] - $startmsecs}] + #global numcommits + #puts "overall $drawmsecs ms for $numcommits commits" +} + +proc findmatches {f} { + global findtype foundstring foundstrlen + if {$findtype == "Regexp"} { + set matches [regexp -indices -all -inline $foundstring $f] + } else { + if {$findtype == "IgnCase"} { + set str [string tolower $f] + } else { + set str $f + } + set matches {} + set i 0 + while {[set j [string first $foundstring $str $i]] >= 0} { + lappend matches [list $j [expr {$j+$foundstrlen-1}]] + set i [expr {$j + $foundstrlen}] + } + } + return $matches +} + +proc dofind {} { + global findtype findloc findstring markedmatches commitinfo + global numcommits displayorder linehtag linentag linedtag + global mainfont canv canv2 canv3 selectedline + global matchinglines foundstring foundstrlen matchstring + global commitdata + + stopfindproc + unmarkmatches + cancel_next_highlight + focus . + set matchinglines {} + if {$findtype == "IgnCase"} { + set foundstring [string tolower $findstring] + } else { + set foundstring $findstring + } + set foundstrlen [string length $findstring] + if {$foundstrlen == 0} return + regsub -all {[*?\[\\]} $foundstring {\\&} matchstring + set matchstring "*$matchstring*" + if {![info exists selectedline]} { + set oldsel -1 + } else { + set oldsel $selectedline + } + set didsel 0 + set fldtypes {Headline Author Date Committer CDate Comments} + set l -1 + foreach id $displayorder { + set d $commitdata($id) + incr l + if {$findtype == "Regexp"} { + set doesmatch [regexp $foundstring $d] + } elseif {$findtype == "IgnCase"} { + set doesmatch [string match -nocase $matchstring $d] + } else { + set doesmatch [string match $matchstring $d] + } + if {!$doesmatch} continue + if {![info exists commitinfo($id)]} { + getcommit $id + } + set info $commitinfo($id) + set doesmatch 0 + foreach f $info ty $fldtypes { + if {$findloc != "All fields" && $findloc != $ty} { + continue + } + set matches [findmatches $f] + if {$matches == {}} continue + set doesmatch 1 + if {$ty == "Headline"} { + drawcmitrow $l + markmatches $canv $l $f $linehtag($l) $matches $mainfont + } elseif {$ty == "Author"} { + drawcmitrow $l + markmatches $canv2 $l $f $linentag($l) $matches $mainfont + } elseif {$ty == "Date"} { + drawcmitrow $l + markmatches $canv3 $l $f $linedtag($l) $matches $mainfont + } + } + if {$doesmatch} { + lappend matchinglines $l + if {!$didsel && $l > $oldsel} { + findselectline $l + set didsel 1 + } + } + } + if {$matchinglines == {}} { + bell + } elseif {!$didsel} { + findselectline [lindex $matchinglines 0] + } +} + +proc findselectline {l} { + global findloc commentend ctext + selectline $l 1 + if {$findloc == "All fields" || $findloc == "Comments"} { + # highlight the matches in the comments + set f [$ctext get 1.0 $commentend] + set matches [findmatches $f] + foreach match $matches { + set start [lindex $match 0] + set end [expr {[lindex $match 1] + 1}] + $ctext tag add found "1.0 + $start c" "1.0 + $end c" + } + } +} + +proc findnext {restart} { + global matchinglines selectedline + if {![info exists matchinglines]} { + if {$restart} { + dofind + } + return + } + if {![info exists selectedline]} return + foreach l $matchinglines { + if {$l > $selectedline} { + findselectline $l + return + } + } + bell +} + +proc findprev {} { + global matchinglines selectedline + if {![info exists matchinglines]} { + dofind + return + } + if {![info exists selectedline]} return + set prev {} + foreach l $matchinglines { + if {$l >= $selectedline} break + set prev $l + } + if {$prev != {}} { + findselectline $prev + } else { + bell + } +} + +proc stopfindproc {{done 0}} { + global findprocpid findprocfile findids + global ctext findoldcursor phase maincursor textcursor + global findinprogress + + catch {unset findids} + if {[info exists findprocpid]} { + if {!$done} { + catch {exec kill $findprocpid} + } + catch {close $findprocfile} + unset findprocpid + } + catch {unset findinprogress} + notbusy find +} + +# mark a commit as matching by putting a yellow background +# behind the headline +proc markheadline {l id} { + global canv mainfont linehtag + + drawcmitrow $l + set bbox [$canv bbox $linehtag($l)] + set t [$canv create rect $bbox -outline {} -tags matches -fill yellow] + $canv lower $t +} + +# mark the bits of a headline, author or date that match a find string +proc markmatches {canv l str tag matches font} { + set bbox [$canv bbox $tag] + set x0 [lindex $bbox 0] + set y0 [lindex $bbox 1] + set y1 [lindex $bbox 3] + foreach match $matches { + set start [lindex $match 0] + set end [lindex $match 1] + if {$start > $end} continue + set xoff [font measure $font [string range $str 0 [expr {$start-1}]]] + set xlen [font measure $font [string range $str 0 [expr {$end}]]] + set t [$canv create rect [expr {$x0+$xoff}] $y0 \ + [expr {$x0+$xlen+2}] $y1 \ + -outline {} -tags matches -fill yellow] + $canv lower $t + } +} + +proc unmarkmatches {} { + global matchinglines findids + allcanvs delete matches + catch {unset matchinglines} + catch {unset findids} +} + +proc selcanvline {w x y} { + global canv canvy0 ctext linespc + global rowtextx + set ymax [lindex [$canv cget -scrollregion] 3] + if {$ymax == {}} return + set yfrac [lindex [$canv yview] 0] + set y [expr {$y + $yfrac * $ymax}] + set l [expr {int(($y - $canvy0) / $linespc + 0.5)}] + if {$l < 0} { + set l 0 + } + if {$w eq $canv} { + if {![info exists rowtextx($l)] || $x < $rowtextx($l)} return + } + unmarkmatches + selectline $l 1 +} + +proc commit_descriptor {p} { + global commitinfo + if {![info exists commitinfo($p)]} { + getcommit $p + } + set l "..." + if {[llength $commitinfo($p)] > 1} { + set l [lindex $commitinfo($p) 0] + } + 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 tags} { + global ctext commitrow linknum curview + + set start [$ctext index "end - 1c"] + $ctext insert end $text $tags + set links [regexp -indices -all -inline {[0-9a-f]{40}} $text] + foreach l $links { + set s [lindex $l 0] + set e [lindex $l 1] + set linkid [string range $text $s $e] + if {![info exists commitrow($curview,$linkid)]} continue + incr e + $ctext tag add link "$start + $s c" "$start + $e c" + $ctext tag add link$linknum "$start + $s c" "$start + $e c" + $ctext tag bind link$linknum <1> \ + [list selectline $commitrow($curview,$linkid) 1] + incr linknum + } + $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 } +} + +proc viewnextline {dir} { + global canv linespc + + $canv delete hover + set ymax [lindex [$canv cget -scrollregion] 3] + set wnow [$canv yview] + set wtop [expr {[lindex $wnow 0] * $ymax}] + set newtop [expr {$wtop + $dir * $linespc}] + if {$newtop < 0} { + set newtop 0 + } elseif {$newtop > $ymax} { + set newtop $ymax + } + 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 tags var} { + global ctext commitrow linknum curview $var + + if {[catch {$ctext index $pos}]} { + return 0 + } + set tags [lsort $tags] + set sep {} + foreach tag $tags { + set id [set $var\($tag\)] + set lk link$linknum + incr linknum + $ctext insert $pos $sep + $ctext insert $pos $tag $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] +} + +proc taglist {ids} { + global idtags + + set tags {} + foreach id $ids { + foreach tag $idtags($id) { + lappend tags $tag + } + } + return $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) headids] > 1} { + $ctext insert "branch -2c" "es" + } + } + if {[info exists anc_tags($id)]} { + appendrefs follows [taglist $anc_tags($id)] tagids + } + if {[info exists desc_tags($id)]} { + appendrefs precedes [taglist $desc_tags($id)] tagids + } + $ctext conf -state disabled +} + +proc selectline {l isnew} { + global canv canv2 canv3 ctext commitinfo selectedline + global displayorder linehtag linentag linedtag + global canvy0 linespc parentlist childlist + global currentid sha1entry + global commentend idtags linknum + global mergemax numcommits pending_select + 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] + set ytop [expr {$y - $linespc - 1}] + set ybot [expr {$y + $linespc + 1}] + set wnow [$canv yview] + set wtop [expr {[lindex $wnow 0] * $ymax}] + set wbot [expr {[lindex $wnow 1] * $ymax}] + set wh [expr {$wbot - $wtop}] + set newtop $wtop + if {$ytop < $wtop} { + if {$ybot < $wtop} { + set newtop [expr {$y - $wh / 2.0}] + } else { + set newtop $ytop + if {$newtop > $wtop - $linespc} { + set newtop [expr {$wtop - $linespc}] + } + } + } elseif {$ybot > $wbot} { + if {$ytop > $wbot} { + set newtop [expr {$y - $wh / 2.0}] + } else { + set newtop [expr {$ybot - $wh}] + if {$newtop < $wtop + $linespc} { + set newtop [expr {$wtop + $linespc}] + } + } + } + if {$newtop != $wtop} { + if {$newtop < 0} { + set newtop 0 + } + allcanvs yview moveto [expr {$newtop * 1.0 / $ymax}] + drawvisible + } + + if {![info exists linehtag($l)]} return + $canv delete secsel + set t [eval $canv create rect [$canv bbox $linehtag($l)] -outline {{}} \ + -tags secsel -fill [$canv cget -selectbackground]] + $canv lower $t + $canv2 delete secsel + set t [eval $canv2 create rect [$canv2 bbox $linentag($l)] -outline {{}} \ + -tags secsel -fill [$canv2 cget -selectbackground]] + $canv2 lower $t + $canv3 delete secsel + set t [eval $canv3 create rect [$canv3 bbox $linedtag($l)] -outline {{}} \ + -tags secsel -fill [$canv3 cget -selectbackground]] + $canv3 lower $t + + if {$isnew} { + addtohistory [list selectline $l 0] + } + + set selectedline $l + + set id [lindex $displayorder $l] + set currentid $id + $sha1entry delete 0 end + $sha1entry insert 0 $id + $sha1entry selection from 0 + $sha1entry selection to end + rhighlight_sel $id + + $ctext conf -state normal + clear_ctext + set linknum 0 + set info $commitinfo($id) + set date [formatdate [lindex $info 2]] + $ctext insert end "Author: [lindex $info 1] $date\n" + set date [formatdate [lindex $info 4]] + $ctext insert end "Committer: [lindex $info 3] $date\n" + if {[info exists idtags($id)]} { + $ctext insert end "Tags:" + foreach tag $idtags($id) { + $ctext insert end " $tag" + } + $ctext insert end "\n" + } + + set headers {} + set olds [lindex $parentlist $l] + if {[llength $olds] > 1} { + set np 0 + foreach p $olds { + if {$np >= $mergemax} { + set tag mmax + } else { + set tag m$np + } + $ctext insert end "Parent: " $tag + appendwithlinks [commit_descriptor $p] {} + incr np + } + } else { + foreach p $olds { + append headers "Parent: [commit_descriptor $p]" + } + } + + foreach c [lindex $childlist $l] { + append headers "Child: [commit_descriptor $c]" + } + + # make anything that looks like a SHA1 ID be a clickable link + 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) headids] > 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 [taglist $anc_tags($id)] tagids + } + $ctext insert end "\nPrecedes: " + $ctext mark set precedes "end -1c" + $ctext mark gravity precedes left + if {[info exists desc_tags($id)]} { + appendrefs precedes [taglist $desc_tags($id)] tagids + } + $ctext insert end "\n" + } + $ctext insert end "\n" + appendwithlinks [lindex $info 5] {comment} + + $ctext tag delete Comments + $ctext tag remove found 1.0 end + $ctext conf -state disabled + set commentend [$ctext index "end - 1c"] + + init_flist "Comments" + if {$cmitmode eq "tree"} { + gettree $id + } elseif {[llength $olds] <= 1} { + startdiff $id + } else { + mergediff $id $l + } +} + +proc selfirstline {} { + unmarkmatches + selectline 0 1 +} + +proc sellastline {} { + global numcommits + unmarkmatches + set l [expr {$numcommits - 1}] + selectline $l 1 +} + +proc selnextline {dir} { + global selectedline + if {![info exists selectedline]} return + set l [expr {$selectedline + $dir}] + unmarkmatches + selectline $l 1 +} + +proc selnextpage {dir} { + global canv linespc selectedline numcommits + + set lpp [expr {([winfo height $canv] - 2) / $linespc}] + if {$lpp < 1} { + 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} { + set l 0 + } elseif {$l >= $numcommits} { + set l [expr $numcommits - 1] + } + unmarkmatches + selectline $l 1 +} + +proc unselectline {} { + global selectedline currentid + + catch {unset selectedline} + catch {unset currentid} + allcanvs delete secsel + rhighlight_none + cancel_next_highlight +} + +proc reselectline {} { + global selectedline + + if {[info exists selectedline]} { + selectline $selectedline 0 + } +} + +proc addtohistory {cmd} { + global history historyindex curview + + set elt [list $curview $cmd] + if {$historyindex > 0 + && [lindex $history [expr {$historyindex - 1}]] == $elt} { + return + } + + if {$historyindex < [llength $history]} { + set history [lreplace $history $historyindex end $elt] + } else { + lappend history $elt + } + incr historyindex + if {$historyindex > 1} { + .tf.bar.leftbut conf -state normal + } else { + .tf.bar.leftbut conf -state disabled + } + .tf.bar.rightbut conf -state disabled +} + +proc godo {elt} { + global curview + + set view [lindex $elt 0] + set cmd [lindex $elt 1] + if {$curview != $view} { + showview $view + } + eval $cmd +} + +proc goback {} { + global history historyindex + + if {$historyindex > 1} { + incr historyindex -1 + godo [lindex $history [expr {$historyindex - 1}]] + .tf.bar.rightbut conf -state normal + } + if {$historyindex <= 1} { + .tf.bar.leftbut conf -state disabled + } +} + +proc goforw {} { + global history historyindex + + if {$historyindex < [llength $history]} { + set cmd [lindex $history $historyindex] + incr historyindex + godo $cmd + .tf.bar.leftbut conf -state normal + } + if {$historyindex >= [llength $history]} { + .tf.bar.rightbut conf -state disabled + } +} + +proc gettree {id} { + global treefilelist treeidlist diffids diffmergeid treepending + + set diffids $id + catch {unset diffmergeid} + if {![info exists treefilelist($id)]} { + if {![info exists treepending]} { + if {[catch {set gtf [open [concat | git ls-tree -r $id] r]}]} { + return + } + set treepending $id + set treefilelist($id) {} + set treeidlist($id) {} + fconfigure $gtf -blocking 0 + fileevent $gtf readable [list gettreeline $gtf $id] + } + } else { + setfilelist $id + } +} + +proc gettreeline {gtf id} { + global treefilelist treeidlist treepending cmitmode diffids + + while {[gets $gtf line] >= 0} { + if {[lindex $line 1] ne "blob"} continue + set sha1 [lindex $line 2] + set fname [lindex $line 3] + lappend treefilelist($id) $fname + lappend treeidlist($id) $sha1 + } + if {![eof $gtf]} return + close $gtf + unset treepending + if {$cmitmode ne "tree"} { + if {![info exists diffmergeid]} { + gettreediffs $diffids + } + } elseif {$id ne $diffids} { + gettree $diffids + } else { + setfilelist $id + } +} + +proc showfile {f} { + global treefilelist treeidlist diffids + global ctext commentend + + set i [lsearch -exact $treefilelist($diffids) $f] + if {$i < 0} { + puts "oops, $f not in list for id $diffids" + return + } + set blob [lindex $treeidlist($diffids) $i] + 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 + clear_ctext $commentend + $ctext insert end "\n" + $ctext insert end "$f\n" filesep + $ctext config -state disabled + $ctext yview $commentend +} + +proc getblobline {bf id} { + global diffids cmitmode ctext + + if {$id ne $diffids || $cmitmode ne "tree"} { + catch {close $bf} + return + } + $ctext config -state normal + while {[gets $bf line] >= 0} { + $ctext insert end "$line\n" + } + if {[eof $bf]} { + # delete last newline + $ctext delete "end - 2c" "end - 1c" + close $bf + } + $ctext config -state disabled +} + +proc mergediff {id l} { + global diffmergeid diffopts mdifffd + global diffids + global parentlist + + set diffmergeid $id + 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] + if {[catch {set mdf [open $cmd r]} err]} { + error_popup "Error getting merge diffs: $err" + return + } + fconfigure $mdf -blocking 0 + set mdifffd($id) $mdf + set np [llength [lindex $parentlist $l]] + fileevent $mdf readable [list getmergediffline $mdf $id $np] + set nextupdate [expr {[clock clicks -milliseconds] + 100}] +} + +proc getmergediffline {mdf id np} { + global diffmergeid ctext cflist nextupdate mergemax + global difffilestart mdifffd + + set n [gets $mdf line] + if {$n < 0} { + if {[eof $mdf]} { + close $mdf + } + return + } + if {![info exists diffmergeid] || $id != $diffmergeid + || $mdf != $mdifffd($id)} { + return + } + $ctext conf -state normal + if {[regexp {^diff --cc (.*)} $line match fname]} { + # start of a new file + $ctext insert end "\n" + set here [$ctext index "end - 1c"] + lappend difffilestart $here + add_flist [list $fname] + set l [expr {(78 - [string length $fname]) / 2}] + set pad [string range "----------------------------------------" 1 $l] + $ctext insert end "$pad $fname $pad\n" filesep + } elseif {[regexp {^@@} $line]} { + $ctext insert end "$line\n" hunksep + } elseif {[regexp {^[0-9a-f]{40}$} $line] || [regexp {^index} $line]} { + # do nothing + } else { + # parse the prefix - one ' ', '-' or '+' for each parent + set spaces {} + set minuses {} + set pluses {} + set isbad 0 + for {set j 0} {$j < $np} {incr j} { + set c [string range $line $j $j] + if {$c == " "} { + lappend spaces $j + } elseif {$c == "-"} { + lappend minuses $j + } elseif {$c == "+"} { + lappend pluses $j + } else { + set isbad 1 + break + } + } + set tags {} + set num {} + if {!$isbad && $minuses ne {} && $pluses eq {}} { + # line doesn't appear in result, parents in $minuses have the line + set num [lindex $minuses 0] + } elseif {!$isbad && $pluses ne {} && $minuses eq {}} { + # line appears in result, parents in $pluses don't have the line + lappend tags mresult + set num [lindex $spaces 0] + } + if {$num ne {}} { + if {$num >= $mergemax} { + set num "max" + } + lappend tags m$num + } + $ctext insert end "$line\n" $tags + } + $ctext conf -state disabled + if {[clock clicks -milliseconds] >= $nextupdate} { + incr nextupdate 100 + fileevent $mdf readable {} + update + fileevent $mdf readable [list getmergediffline $mdf $id $np] + } +} + +proc startdiff {ids} { + global treediffs diffids treepending diffmergeid + + set diffids $ids + catch {unset diffmergeid} + if {![info exists treediffs($ids)]} { + if {![info exists treepending]} { + gettreediffs $ids + } + } else { + addtocflist $ids + } +} + +proc addtocflist {ids} { + global treediffs cflist + add_flist $treediffs($ids) + getblobdiffs $ids +} + +proc gettreediffs {ids} { + global treediff treepending + set treepending $ids + set treediff {} + if {[catch \ + {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] +} + +proc gettreediffline {gdtf ids} { + global treediff treediffs treepending diffids diffmergeid + global cmitmode + + set n [gets $gdtf line] + if {$n < 0} { + if {![eof $gdtf]} return + close $gdtf + set treediffs($ids) $treediff + unset treepending + if {$cmitmode eq "tree"} { + gettree $diffids + } elseif {$ids != $diffids} { + if {![info exists diffmergeid]} { + gettreediffs $diffids + } + } else { + addtocflist $ids + } + return + } + set file [lindex $line 5] + lappend treediff $file +} + +proc getblobdiffs {ids} { + global diffopts blobdifffd diffids env curdifftag curtagstart + global nextupdate diffinhdr treediffs + + set env(GIT_DIFF_OPTS) $diffopts + 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 + } + set diffinhdr 0 + fconfigure $bdf -blocking 0 + set blobdifffd($ids) $bdf + set curdifftag Comments + set curtagstart 0.0 + fileevent $bdf readable [list getblobdiffline $bdf $diffids] + set nextupdate [expr {[clock clicks -milliseconds] + 100}] +} + +proc setinlist {var i val} { + global $var + + while {[llength [set $var]] < $i} { + lappend $var {} + } + if {[llength [set $var]] == $i} { + lappend $var $val + } else { + lset $var $i $val + } +} + +proc getblobdiffline {bdf ids} { + global diffids blobdifffd ctext curdifftag curtagstart + global diffnexthead diffnextnote difffilestart + global nextupdate diffinhdr treediffs + + set n [gets $bdf line] + if {$n < 0} { + if {[eof $bdf]} { + close $bdf + if {$ids == $diffids && $bdf == $blobdifffd($ids)} { + $ctext tag add $curdifftag $curtagstart end + } + } + return + } + if {$ids != $diffids || $bdf != $blobdifffd($ids)} { + return + } + $ctext conf -state normal + if {[regexp {^diff --git a/(.*) b/(.*)} $line match fname newname]} { + # start of a new file + $ctext insert end "\n" + $ctext tag add $curdifftag $curtagstart end + set here [$ctext index "end - 1c"] + set curtagstart $here + set header $newname + set i [lsearch -exact $treediffs($ids) $fname] + if {$i >= 0} { + setinlist difffilestart $i $here + } + if {$newname ne $fname} { + set i [lsearch -exact $treediffs($ids) $newname] + if {$i >= 0} { + setinlist difffilestart $i $here + } + } + set curdifftag "f:$fname" + $ctext tag delete $curdifftag + set l [expr {(78 - [string length $header]) / 2}] + set pad [string range "----------------------------------------" 1 $l] + $ctext insert end "$pad $header $pad\n" filesep + set diffinhdr 1 + } elseif {$diffinhdr && [string compare -length 3 $line "---"] == 0} { + # do nothing + } elseif {$diffinhdr && [string compare -length 3 $line "+++"] == 0} { + set diffinhdr 0 + } elseif {[regexp {^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@(.*)} \ + $line match f1l f1c f2l f2c rest]} { + $ctext insert end "$line\n" hunksep + set diffinhdr 0 + } else { + set x [string range $line 0 0] + if {$x == "-" || $x == "+"} { + set tag [expr {$x == "+"}] + $ctext insert end "$line\n" d$tag + } elseif {$x == " "} { + $ctext insert end "$line\n" + } elseif {$diffinhdr || $x == "\\"} { + # e.g. "\ No newline at end of file" + $ctext insert end "$line\n" filesep + } else { + # Something else we don't recognize + if {$curdifftag != "Comments"} { + $ctext insert end "\n" + $ctext tag add $curdifftag $curtagstart end + set curtagstart [$ctext index "end - 1c"] + set curdifftag Comments + } + $ctext insert end "$line\n" filesep + } + } + $ctext conf -state disabled + if {[clock clicks -milliseconds] >= $nextupdate} { + incr nextupdate 100 + fileevent $bdf readable {} + update + fileevent $bdf readable "getblobdiffline $bdf {$ids}" + } +} + +proc prevfile {} { + global difffilestart ctext + set prev [lindex $difffilestart 0] + set here [$ctext index @0,0] + foreach loc $difffilestart { + if {[$ctext compare $loc >= $here]} { + $ctext yview $prev + return + } + set prev $loc + } + $ctext yview $prev +} + +proc nextfile {} { + global difffilestart ctext + set here [$ctext index @0,0] + foreach loc $difffilestart { + if {[$ctext compare $loc > $here]} { + $ctext yview $loc + 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 + + .bleft.sb set $f0 $f1 + if {$searchstring ne {}} { + searchmarkvisible 0 + } +} + +proc setcoords {} { + global linespc charspc canvx0 canvy0 mainfont + global xspc1 xspc2 lthickness + + set linespc [font metrics $mainfont -linespace] + set charspc [font measure $mainfont "m"] + set canvy0 [expr {int(3 + 0.5 * $linespc)}] + set canvx0 [expr {int(3 + 0.5 * $linespc)}] + set lthickness [expr {int($linespc / 9) + 1}] + set xspc1(0) $linespc + set xspc2 $linespc +} + +proc redisplay {} { + global canv + global selectedline + + set ymax [lindex [$canv cget -scrollregion] 3] + if {$ymax eq {} || $ymax == 0} return + set span [$canv yview] + clear_display + setcanvscroll + allcanvs yview moveto [lindex $span 0] + drawvisible + if {[info exists selectedline]} { + selectline $selectedline 0 + allcanvs yview moveto [lindex $span 0] + } +} + +proc incrfont {inc} { + global mainfont textfont ctext canv phase + global stopped entries + unmarkmatches + set mainfont [lreplace $mainfont 1 1 [expr {[lindex $mainfont 1] + $inc}]] + set textfont [lreplace $textfont 1 1 [expr {[lindex $textfont 1] + $inc}]] + setcoords + $ctext conf -font $textfont + $ctext tag conf filesep -font [concat $textfont bold] + foreach e $entries { + $e conf -font $mainfont + } + if {$phase eq "getcommits"} { + $canv itemconf textitems -font $mainfont + } + redisplay +} + +proc clearsha1 {} { + global sha1entry sha1string + if {[string length $sha1string] == 40} { + $sha1entry delete 0 end + } +} + +proc sha1change {n1 n2 op} { + global sha1string currentid sha1but + if {$sha1string == {} + || ([info exists currentid] && $sha1string == $currentid)} { + set state disabled + } else { + set state normal + } + if {[$sha1but cget -state] == $state} return + if {$state == "normal"} { + $sha1but conf -state normal -relief raised -text "Goto: " + } else { + $sha1but conf -state disabled -relief flat -text "SHA1 ID: " + } +} + +proc gotocommit {} { + global sha1string currentid commitrow tagids headids + global displayorder numcommits curview + + if {$sha1string == {} + || ([info exists currentid] && $sha1string == $currentid)} return + if {[info exists tagids($sha1string)]} { + set id $tagids($sha1string) + } elseif {[info exists headids($sha1string)]} { + set id $headids($sha1string) + } else { + set id [string tolower $sha1string] + if {[regexp {^[0-9a-f]{4,39}$} $id]} { + set matches {} + foreach i $displayorder { + if {[string match $id* $i]} { + lappend matches $i + } + } + if {$matches ne {}} { + if {[llength $matches] > 1} { + error_popup "Short SHA1 id $id is ambiguous" + return + } + set id [lindex $matches 0] + } + } + } + if {[info exists commitrow($curview,$id)]} { + selectline $commitrow($curview,$id) 1 + return + } + if {[regexp {^[0-9a-fA-F]{4,}$} $sha1string]} { + set type "SHA1 id" + } else { + set type "Tag/Head" + } + error_popup "$type $sha1string is not known" +} + +proc lineenter {x y id} { + global hoverx hovery hoverid hovertimer + global commitinfo canv + + if {![info exists commitinfo($id)] && ![getcommit $id]} return + set hoverx $x + set hovery $y + set hoverid $id + if {[info exists hovertimer]} { + after cancel $hovertimer + } + set hovertimer [after 500 linehover] + $canv delete hover +} + +proc linemotion {x y id} { + global hoverx hovery hoverid hovertimer + + if {[info exists hoverid] && $id == $hoverid} { + set hoverx $x + set hovery $y + if {[info exists hovertimer]} { + after cancel $hovertimer + } + set hovertimer [after 500 linehover] + } +} + +proc lineleave {id} { + global hoverid hovertimer canv + + if {[info exists hoverid] && $id == $hoverid} { + $canv delete hover + if {[info exists hovertimer]} { + after cancel $hovertimer + unset hovertimer + } + unset hoverid + } +} + +proc linehover {} { + global hoverx hovery hoverid hovertimer + global canv linespc lthickness + global commitinfo mainfont + + set text [lindex $commitinfo($hoverid) 0] + set ymax [lindex [$canv cget -scrollregion] 3] + if {$ymax == {}} return + set yfrac [lindex [$canv yview] 0] + set x [expr {$hoverx + 2 * $linespc}] + set y [expr {$hovery + $yfrac * $ymax - $linespc / 2}] + set x0 [expr {$x - 2 * $lthickness}] + set y0 [expr {$y - 2 * $lthickness}] + set x1 [expr {$x + [font measure $mainfont $text] + 2 * $lthickness}] + set y1 [expr {$y + $linespc + 2 * $lthickness}] + set t [$canv create rectangle $x0 $y0 $x1 $y1 \ + -fill \#ffff80 -outline black -width 1 -tags hover] + $canv raise $t + set t [$canv create text $x $y -anchor nw -text $text -tags hover \ + -font $mainfont] + $canv raise $t +} + +proc clickisonarrow {id y} { + global lthickness + + set ranges [rowranges $id] + set thresh [expr {2 * $lthickness + 6}] + set n [expr {[llength $ranges] - 1}] + for {set i 1} {$i < $n} {incr i} { + set row [lindex $ranges $i] + if {abs([yc $row] - $y) < $thresh} { + return $i + } + } + return {} +} + +proc arrowjump {id n y} { + global canv + + # 1 <-> 2, 3 <-> 4, etc... + set n [expr {(($n - 1) ^ 1) + 1}] + set row [lindex [rowranges $id] $n] + set yt [yc $row] + set ymax [lindex [$canv cget -scrollregion] 3] + if {$ymax eq {} || $ymax <= 0} return + set view [$canv yview] + set yspan [expr {[lindex $view 1] - [lindex $view 0]}] + set yfrac [expr {$yt / $ymax - $yspan / 2}] + if {$yfrac < 0} { + set yfrac 0 + } + allcanvs yview moveto $yfrac +} + +proc lineclick {x y id isnew} { + global ctext commitinfo children canv thickerline curview + + if {![info exists commitinfo($id)] && ![getcommit $id]} return + unmarkmatches + unselectline + normalline + $canv delete hover + # draw this line thicker than normal + set thickerline $id + drawlines $id + if {$isnew} { + set ymax [lindex [$canv cget -scrollregion] 3] + if {$ymax eq {}} return + set yfrac [lindex [$canv yview] 0] + set y [expr {$y + $yfrac * $ymax}] + } + set dirn [clickisonarrow $id $y] + if {$dirn ne {}} { + arrowjump $id $dirn $y + return + } + + if {$isnew} { + addtohistory [list lineclick $x $y $id 0] + } + # fill the details pane with info about this line + $ctext conf -state normal + 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 } + $ctext insert end "Parent:\t" + $ctext insert end $id [list link link0] + $ctext tag bind link0 <1> [list selbyid $id] + set info $commitinfo($id) + $ctext insert end "\n\t[lindex $info 0]\n" + $ctext insert end "\tAuthor:\t[lindex $info 1]\n" + set date [formatdate [lindex $info 2]] + $ctext insert end "\tDate:\t$date\n" + set kids $children($curview,$id) + if {$kids ne {}} { + $ctext insert end "\nChildren:" + set i 0 + foreach child $kids { + incr i + if {![info exists commitinfo($child)] && ![getcommit $child]} continue + set info $commitinfo($child) + $ctext insert end "\n\t" + $ctext insert end $child [list link link$i] + $ctext tag bind link$i <1> [list selbyid $child] + $ctext insert end "\n\t[lindex $info 0]" + $ctext insert end "\n\tAuthor:\t[lindex $info 1]" + set date [formatdate [lindex $info 2]] + $ctext insert end "\n\tDate:\t$date\n" + } + } + $ctext conf -state disabled + init_flist {} +} + +proc normalline {} { + global thickerline + if {[info exists thickerline]} { + set id $thickerline + unset thickerline + drawlines $id + } +} + +proc selbyid {id} { + global commitrow curview + if {[info exists commitrow($curview,$id)]} { + selectline $commitrow($curview,$id) 1 + } +} + +proc mstime {} { + global startmstime + if {![info exists startmstime]} { + set startmstime [clock clicks -milliseconds] + } + return [format "%.3f" [expr {([clock click -milliseconds] - $startmstime) / 1000.0}]] +} + +proc rowmenu {x y id} { + global rowctxmenu commitrow selectedline rowmenuid curview + + if {![info exists selectedline] + || $commitrow($curview,$id) eq $selectedline} { + set state disabled + } else { + set state normal + } + $rowctxmenu entryconfigure "Diff this*" -state $state + $rowctxmenu entryconfigure "Diff selected*" -state $state + $rowctxmenu entryconfigure "Make patch" -state $state + set rowmenuid $id + tk_popup $rowctxmenu $x $y +} + +proc diffvssel {dirn} { + global rowmenuid selectedline displayorder + + if {![info exists selectedline]} return + if {$dirn} { + set oldid [lindex $displayorder $selectedline] + set newid $rowmenuid + } else { + set oldid $rowmenuid + set newid [lindex $displayorder $selectedline] + } + addtohistory [list doseldiff $oldid $newid] + doseldiff $oldid $newid +} + +proc doseldiff {oldid newid} { + global ctext + global commitinfo + + $ctext conf -state normal + clear_ctext + init_flist "Top" + $ctext insert end "From " + $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 } + $ctext tag bind link0 <1> [list selbyid $oldid] + $ctext insert end $oldid [list link link0] + $ctext insert end "\n " + $ctext insert end [lindex $commitinfo($oldid) 0] + $ctext insert end "\n\nTo " + $ctext tag bind link1 <1> [list selbyid $newid] + $ctext insert end $newid [list link link1] + $ctext insert end "\n " + $ctext insert end [lindex $commitinfo($newid) 0] + $ctext insert end "\n" + $ctext conf -state disabled + $ctext tag delete Comments + $ctext tag remove found 1.0 end + startdiff [list $oldid $newid] +} + +proc mkpatch {} { + global rowmenuid currentid commitinfo patchtop patchnum + + if {![info exists currentid]} return + set oldid $currentid + set oldhead [lindex $commitinfo($oldid) 0] + set newid $rowmenuid + set newhead [lindex $commitinfo($newid) 0] + set top .patch + set patchtop $top + catch {destroy $top} + toplevel $top + label $top.title -text "Generate patch" + grid $top.title - -pady 10 + label $top.from -text "From:" + entry $top.fromsha1 -width 40 -relief flat + $top.fromsha1 insert 0 $oldid + $top.fromsha1 conf -state readonly + grid $top.from $top.fromsha1 -sticky w + entry $top.fromhead -width 60 -relief flat + $top.fromhead insert 0 $oldhead + $top.fromhead conf -state readonly + grid x $top.fromhead -sticky w + label $top.to -text "To:" + entry $top.tosha1 -width 40 -relief flat + $top.tosha1 insert 0 $newid + $top.tosha1 conf -state readonly + grid $top.to $top.tosha1 -sticky w + entry $top.tohead -width 60 -relief flat + $top.tohead insert 0 $newhead + $top.tohead conf -state readonly + grid x $top.tohead -sticky w + button $top.rev -text "Reverse" -command mkpatchrev -padx 5 + grid $top.rev x -pady 10 + label $top.flab -text "Output file:" + entry $top.fname -width 60 + $top.fname insert 0 [file normalize "patch$patchnum.patch"] + incr patchnum + grid $top.flab $top.fname -sticky w + frame $top.buts + button $top.buts.gen -text "Generate" -command mkpatchgo + button $top.buts.can -text "Cancel" -command mkpatchcan + grid $top.buts.gen $top.buts.can + grid columnconfigure $top.buts 0 -weight 1 -uniform a + grid columnconfigure $top.buts 1 -weight 1 -uniform a + grid $top.buts - -pady 10 -sticky ew + focus $top.fname +} + +proc mkpatchrev {} { + global patchtop + + set oldid [$patchtop.fromsha1 get] + set oldhead [$patchtop.fromhead get] + set newid [$patchtop.tosha1 get] + set newhead [$patchtop.tohead get] + foreach e [list fromsha1 fromhead tosha1 tohead] \ + v [list $newid $newhead $oldid $oldhead] { + $patchtop.$e conf -state normal + $patchtop.$e delete 0 end + $patchtop.$e insert 0 $v + $patchtop.$e conf -state readonly + } +} + +proc mkpatchgo {} { + global patchtop + + 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]} { + error_popup "Error creating patch: $err" + } + catch {destroy $patchtop} + unset patchtop +} + +proc mkpatchcan {} { + global patchtop + + catch {destroy $patchtop} + unset patchtop +} + +proc mktag {} { + global rowmenuid mktagtop commitinfo + + set top .maketag + set mktagtop $top + catch {destroy $top} + toplevel $top + label $top.title -text "Create tag" + grid $top.title - -pady 10 + label $top.id -text "ID:" + entry $top.sha1 -width 40 -relief flat + $top.sha1 insert 0 $rowmenuid + $top.sha1 conf -state readonly + grid $top.id $top.sha1 -sticky w + entry $top.head -width 60 -relief flat + $top.head insert 0 [lindex $commitinfo($rowmenuid) 0] + $top.head conf -state readonly + grid x $top.head -sticky w + label $top.tlab -text "Tag name:" + entry $top.tag -width 60 + grid $top.tlab $top.tag -sticky w + frame $top.buts + button $top.buts.gen -text "Create" -command mktaggo + button $top.buts.can -text "Cancel" -command mktagcan + grid $top.buts.gen $top.buts.can + grid columnconfigure $top.buts 0 -weight 1 -uniform a + grid columnconfigure $top.buts 1 -weight 1 -uniform a + grid $top.buts - -pady 10 -sticky ew + focus $top.tag +} + +proc domktag {} { + global mktagtop env tagids idtags + + set id [$mktagtop.sha1 get] + set tag [$mktagtop.tag get] + if {$tag == {}} { + error_popup "No tag name specified" + return + } + if {[info exists tagids($tag)]} { + error_popup "Tag \"$tag\" already exists" + return + } + if {[catch { + set dir [gitdir] + set fname [file join $dir "refs/tags" $tag] + set f [open $fname w] + puts $f $id + close $f + } err]} { + error_popup "Error creating tag: $err" + return + } + + set tagids($tag) $id + lappend idtags($id) $tag + redrawtags $id + addedtag $id +} + +proc redrawtags {id} { + global canv linehtag commitrow idpos selectedline curview + global mainfont canvxmax + + 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 + } +} + +proc mktagcan {} { + global mktagtop + + catch {destroy $mktagtop} + unset mktagtop +} + +proc mktaggo {} { + domktag + mktagcan +} + +proc writecommit {} { + global rowmenuid wrcomtop commitinfo wrcomcmd + + set top .writecommit + set wrcomtop $top + catch {destroy $top} + toplevel $top + label $top.title -text "Write commit to file" + grid $top.title - -pady 10 + label $top.id -text "ID:" + entry $top.sha1 -width 40 -relief flat + $top.sha1 insert 0 $rowmenuid + $top.sha1 conf -state readonly + grid $top.id $top.sha1 -sticky w + entry $top.head -width 60 -relief flat + $top.head insert 0 [lindex $commitinfo($rowmenuid) 0] + $top.head conf -state readonly + grid x $top.head -sticky w + label $top.clab -text "Command:" + entry $top.cmd -width 60 -textvariable wrcomcmd + grid $top.clab $top.cmd -sticky w -pady 10 + label $top.flab -text "Output file:" + entry $top.fname -width 60 + $top.fname insert 0 [file normalize "commit-[string range $rowmenuid 0 6]"] + grid $top.flab $top.fname -sticky w + frame $top.buts + button $top.buts.gen -text "Write" -command wrcomgo + button $top.buts.can -text "Cancel" -command wrcomcan + grid $top.buts.gen $top.buts.can + grid columnconfigure $top.buts 0 -weight 1 -uniform a + grid columnconfigure $top.buts 1 -weight 1 -uniform a + grid $top.buts - -pady 10 -sticky ew + focus $top.fname +} + +proc wrcomgo {} { + global wrcomtop + + set id [$wrcomtop.sha1 get] + set cmd "echo $id | [$wrcomtop.cmd get]" + set fname [$wrcomtop.fname get] + if {[catch {exec sh -c $cmd >$fname &} err]} { + error_popup "Error writing commit: $err" + } + catch {destroy $wrcomtop} + unset wrcomtop +} + +proc wrcomcan {} { + global wrcomtop + + catch {destroy $wrcomtop} + unset wrcomtop +} + +proc mkbranch {} { + global rowmenuid mkbrtop + + set top .makebranch + catch {destroy $top} + toplevel $top + label $top.title -text "Create new branch" + grid $top.title - -pady 10 + label $top.id -text "ID:" + entry $top.sha1 -width 40 -relief flat + $top.sha1 insert 0 $rowmenuid + $top.sha1 conf -state readonly + grid $top.id $top.sha1 -sticky w + label $top.nlab -text "Name:" + entry $top.name -width 40 + grid $top.nlab $top.name -sticky w + frame $top.buts + button $top.buts.go -text "Create" -command [list mkbrgo $top] + button $top.buts.can -text "Cancel" -command "catch {destroy $top}" + grid $top.buts.go $top.buts.can + grid columnconfigure $top.buts 0 -weight 1 -uniform a + grid columnconfigure $top.buts 1 -weight 1 -uniform a + grid $top.buts - -pady 10 -sticky ew + focus $top.name +} + +proc mkbrgo {top} { + global headids idheads + + set name [$top.name get] + set id [$top.sha1 get] + if {$name eq {}} { + error_popup "Please specify a name for the new branch" + return + } + catch {destroy $top} + nowbusy newbranch + update + if {[catch { + exec git branch $name $id + } err]} { + notbusy newbranch + error_popup $err + } else { + addedhead $id $name + # XXX should update list of heads displayed for selected commit + notbusy newbranch + redrawtags $id + } +} + +proc cherrypick {} { + global rowmenuid curview commitrow + global mainhead desc_heads anc_tags desc_tags allparents allchildren + + if {[info exists desc_heads($rowmenuid)] + && [lsearch -exact $desc_heads($rowmenuid) $mainhead] >= 0} { + set ok [confirm_popup "Commit [string range $rowmenuid 0 7] is already\ + included in branch $mainhead -- really re-apply it?"] + if {!$ok} return + } + nowbusy cherrypick + update + set oldhead [exec git rev-parse HEAD] + # Unfortunately git-cherry-pick writes stuff to stderr even when + # no error occurs, and exec takes that as an indication of error... + if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} { + notbusy cherrypick + error_popup $err + return + } + set newhead [exec git rev-parse HEAD] + if {$newhead eq $oldhead} { + notbusy cherrypick + error_popup "No changes committed" + return + } + set allparents($newhead) $oldhead + lappend allchildren($oldhead) $newhead + set desc_heads($newhead) $mainhead + if {[info exists anc_tags($oldhead)]} { + set anc_tags($newhead) $anc_tags($oldhead) + } + set desc_tags($newhead) {} + if {[info exists commitrow($curview,$oldhead)]} { + insertrow $commitrow($curview,$oldhead) $newhead + if {$mainhead ne {}} { + movedhead $newhead $mainhead + } + redrawtags $oldhead + redrawtags $newhead + } + notbusy cherrypick +} + +# context menu for a head +proc headmenu {x y id head} { + global headmenuid headmenuhead headctxmenu + + set headmenuid $id + set headmenuhead $head + tk_popup $headctxmenu $x $y +} + +proc cobranch {} { + global headmenuid headmenuhead mainhead headids + + # check the tree is clean first?? + set oldmainhead $mainhead + nowbusy checkout + update + if {[catch { + exec git checkout $headmenuhead + } err]} { + notbusy checkout + error_popup $err + } else { + notbusy checkout + set mainhead $headmenuhead + if {[info exists headids($oldmainhead)]} { + redrawtags $headids($oldmainhead) + } + redrawtags $headmenuid + } +} + +proc rmbranch {} { + global desc_heads headmenuid headmenuhead mainhead + global headids idheads + + set head $headmenuhead + set id $headmenuid + if {$head eq $mainhead} { + error_popup "Cannot delete the currently checked-out branch" + return + } + if {$desc_heads($id) eq $head} { + # the stuff on this branch isn't on any other branch + if {![confirm_popup "The commits on branch $head aren't on any other\ + branch.\nReally delete branch $head?"]} return + } + nowbusy rmbranch + update + if {[catch {exec git branch -D $head} err]} { + notbusy rmbranch + error_popup $err + return + } + removedhead $id $head + redrawtags $id + notbusy rmbranch +} + +# Stuff for finding nearby tags +proc getallcommits {} { + global allcstart allcommits allcfd allids + + set allids {} + 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} + } + foreach v {allcommits allchildren allparents allids desc_tags anc_tags + alldtags tagisdesc desc_heads} { + catch {unset $v} + } +} + +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 forward_pass {id children} { + global idtags desc_tags idheads desc_heads alldtags tagisdesc + + set dtags {} + set dheads {} + foreach child $children { + 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)]} { + set dheads [concat $dheads $idheads($id)] + } + set desc_heads($id) $dheads +} + +proc getallclines {fd} { + global allparents allchildren allcommits allcstart + global desc_tags anc_tags idtags tagisdesc allids + global idheads travindex + + 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 + forward_pass $id $allchildren($id) + if {[clock clicks -milliseconds] - $allcstart >= 50} { + fileevent $fd readable {} + after idle restartgetall $fd + return + } + } + if {[eof $fd]} { + set travindex [llength $allids] + set allcommits "traversing" + after idle restartatags + if {[catch {close $fd} err]} { + error_popup "Error reading full commit graph: $err.\n\ + Results may be incomplete." + } + } +} + +# walk backward through the tree and compute nearest tagged ancestors +proc restartatags {} { + global allids allparents idtags anc_tags travindex + + set t0 [clock clicks -milliseconds] + set i $travindex + 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} { + set travindex $i + after idle restartatags + return + } + } + set allcommits "done" + set travindex 0 + notbusy allcommits + dispneartags +} + +# update the desc_tags and anc_tags arrays for a new tag just added +proc addedtag {id} { + global desc_tags anc_tags allparents allchildren allcommits + global idtags tagisdesc alldtags + + if {![info exists desc_tags($id)]} return + set adt $desc_tags($id) + foreach t $desc_tags($id) { + set adt [concat $adt $alldtags($t)] + } + set adt [lsort -unique $adt] + set alldtags($id) $adt + foreach t $adt { + set tagisdesc($id,$t) -1 + set tagisdesc($t,$id) 1 + } + if {[info exists anc_tags($id)]} { + set todo $anc_tags($id) + while {$todo ne {}} { + set do [lindex $todo 0] + set todo [lrange $todo 1 end] + if {[info exists tagisdesc($id,$do)]} continue + set tagisdesc($do,$id) -1 + set tagisdesc($id,$do) 1 + if {[info exists anc_tags($do)]} { + set todo [concat $todo $anc_tags($do)] + } + } + } + + set lastold $desc_tags($id) + set lastnew [list $id] + set nup 0 + set nch 0 + set todo $allparents($id) + while {$todo ne {}} { + set do [lindex $todo 0] + set todo [lrange $todo 1 end] + if {![info exists desc_tags($do)]} continue + if {$desc_tags($do) ne $lastold} { + set lastold $desc_tags($do) + set lastnew [combine_dtags $lastold [list $id]] + incr nch + } + if {$lastold eq $lastnew} continue + set desc_tags($do) $lastnew + incr nup + if {![info exists idtags($do)]} { + set todo [concat $todo $allparents($do)] + } + } + + if {![info exists anc_tags($id)]} return + set lastold $anc_tags($id) + set lastnew [list $id] + set nup 0 + set nch 0 + set todo $allchildren($id) + while {$todo ne {}} { + set do [lindex $todo 0] + set todo [lrange $todo 1 end] + if {![info exists anc_tags($do)]} continue + if {$anc_tags($do) ne $lastold} { + set lastold $anc_tags($do) + set lastnew [combine_atags $lastold [list $id]] + incr nch + } + if {$lastold eq $lastnew} continue + set anc_tags($do) $lastnew + incr nup + if {![info exists idtags($do)]} { + set todo [concat $todo $allchildren($do)] + } + } +} + +# update the desc_heads array for a new head just added +proc addedhead {hid head} { + global desc_heads allparents headids idheads + + set headids($head) $hid + lappend idheads($hid) $head + + set todo [list $hid] + while {$todo ne {}} { + set do [lindex $todo 0] + set todo [lrange $todo 1 end] + if {![info exists desc_heads($do)] || + [lsearch -exact $desc_heads($do) $head] >= 0} continue + set oldheads $desc_heads($do) + lappend desc_heads($do) $head + set heads $desc_heads($do) + while {1} { + set p $allparents($do) + if {[llength $p] != 1 || ![info exists desc_heads($p)] || + $desc_heads($p) ne $oldheads} break + set do $p + set desc_heads($do) $heads + } + set todo [concat $todo $p] + } +} + +# update the desc_heads array for a head just removed +proc removedhead {hid head} { + global desc_heads allparents headids idheads + + unset headids($head) + if {$idheads($hid) eq $head} { + unset idheads($hid) + } else { + set i [lsearch -exact $idheads($hid) $head] + if {$i >= 0} { + set idheads($hid) [lreplace $idheads($hid) $i $i] + } + } + + set todo [list $hid] + while {$todo ne {}} { + set do [lindex $todo 0] + set todo [lrange $todo 1 end] + if {![info exists desc_heads($do)]} continue + set i [lsearch -exact $desc_heads($do) $head] + if {$i < 0} continue + set oldheads $desc_heads($do) + set heads [lreplace $desc_heads($do) $i $i] + while {1} { + set desc_heads($do) $heads + set p $allparents($do) + if {[llength $p] != 1 || ![info exists desc_heads($p)] || + $desc_heads($p) ne $oldheads} break + set do $p + } + set todo [concat $todo $p] + } +} + +# update things for a head moved to a child of its previous location +proc movedhead {id name} { + global headids idheads + + set oldid $headids($name) + set headids($name) $id + if {$idheads($oldid) eq $name} { + unset idheads($oldid) + } else { + set i [lsearch -exact $idheads($oldid) $name] + if {$i >= 0} { + set idheads($oldid) [lreplace $idheads($oldid) $i $i] + } + } + lappend idheads($id) $name +} + +proc changedrefs {} { + global desc_heads desc_tags anc_tags allcommits allids + global allchildren allparents idtags travindex + + if {![info exists allcommits]} return + catch {unset desc_heads} + catch {unset desc_tags} + catch {unset anc_tags} + catch {unset alldtags} + catch {unset tagisdesc} + foreach id $allids { + forward_pass $id $allchildren($id) + } + if {$allcommits ne "reading"} { + set travindex [llength $allids] + if {$allcommits ne "traversing"} { + set allcommits "traversing" + after idle restartatags + } + } +} + +proc rereadrefs {} { + global idtags idheads idotherrefs mainhead + + set refids [concat [array names idtags] \ + [array names idheads] [array names idotherrefs]] + foreach id $refids { + if {![info exists ref($id)]} { + set ref($id) [listrefs $id] + } + } + set oldmainhead $mainhead + readrefs + changedrefs + set refids [lsort -unique [concat $refids [array names idtags] \ + [array names idheads] [array names idotherrefs]]] + foreach id $refids { + set v [listrefs $id] + if {![info exists ref($id)] || $ref($id) != $v || + ($id eq $oldmainhead && $id ne $mainhead) || + ($id eq $mainhead && $id ne $oldmainhead)} { + redrawtags $id + } + } +} + +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 + + if {$isnew} { + addtohistory [list showtag $tag 0] + } + $ctext conf -state normal + clear_ctext + set linknum 0 + if {[info exists tagcontents($tag)]} { + set text $tagcontents($tag) + } else { + set text "Tag: $tag\nId: $tagids($tag)" + } + appendwithlinks $text {} + $ctext conf -state disabled + init_flist {} +} + +proc doquit {} { + global stopped + set stopped 100 + savestuff . + destroy . +} + +proc doprefs {} { + global maxwidth maxgraphpct diffopts + global oldprefs prefstop showneartags + global bgcolor fgcolor ctext diffcolors + + set top .gitkprefs + set prefstop $top + if {[winfo exists $top]} { + raise $top + return + } + foreach v {maxwidth maxgraphpct diffopts showneartags} { + set oldprefs($v) [set $v] + } + toplevel $top + wm title $top "Gitk preferences" + label $top.ldisp -text "Commit list display options" + grid $top.ldisp - -sticky w -pady 10 + label $top.spacer -text " " + label $top.maxwidthl -text "Maximum graph width (lines)" \ + -font optionfont + spinbox $top.maxwidth -from 0 -to 100 -width 4 -textvariable maxwidth + grid $top.spacer $top.maxwidthl $top.maxwidth -sticky w + label $top.maxpctl -text "Maximum graph width (% of pane)" \ + -font optionfont + spinbox $top.maxpct -from 1 -to 100 -width 4 -textvariable maxgraphpct + grid x $top.maxpctl $top.maxpct -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 + + label $top.cdisp -text "Colors: press to choose" + grid $top.cdisp - -sticky w -pady 10 + label $top.bg -padx 40 -relief sunk -background $bgcolor + button $top.bgbut -text "Background" -font optionfont \ + -command [list choosecolor bgcolor 0 $top.bg background setbg] + grid x $top.bgbut $top.bg -sticky w + label $top.fg -padx 40 -relief sunk -background $fgcolor + button $top.fgbut -text "Foreground" -font optionfont \ + -command [list choosecolor fgcolor 0 $top.fg foreground setfg] + grid x $top.fgbut $top.fg -sticky w + label $top.diffold -padx 40 -relief sunk -background [lindex $diffcolors 0] + button $top.diffoldbut -text "Diff: old lines" -font optionfont \ + -command [list choosecolor diffcolors 0 $top.diffold "diff old lines" \ + [list $ctext tag conf d0 -foreground]] + grid x $top.diffoldbut $top.diffold -sticky w + label $top.diffnew -padx 40 -relief sunk -background [lindex $diffcolors 1] + button $top.diffnewbut -text "Diff: new lines" -font optionfont \ + -command [list choosecolor diffcolors 1 $top.diffnew "diff new lines" \ + [list $ctext tag conf d1 -foreground]] + grid x $top.diffnewbut $top.diffnew -sticky w + label $top.hunksep -padx 40 -relief sunk -background [lindex $diffcolors 2] + button $top.hunksepbut -text "Diff: hunk header" -font optionfont \ + -command [list choosecolor diffcolors 2 $top.hunksep \ + "diff hunk header" \ + [list $ctext tag conf hunksep -foreground]] + grid x $top.hunksepbut $top.hunksep -sticky w + + frame $top.buts + button $top.buts.ok -text "OK" -command prefsok + button $top.buts.can -text "Cancel" -command prefscan + grid $top.buts.ok $top.buts.can + grid columnconfigure $top.buts 0 -weight 1 -uniform a + grid columnconfigure $top.buts 1 -weight 1 -uniform a + grid $top.buts - - -pady 10 -sticky ew +} + +proc choosecolor {v vi w x cmd} { + global $v + + set c [tk_chooseColor -initialcolor [lindex [set $v] $vi] \ + -title "Gitk: choose color for $x"] + if {$c eq {}} return + $w conf -background $c + lset $v $vi $c + eval $cmd $c +} + +proc setbg {c} { + global bglist + + foreach w $bglist { + $w conf -background $c + } +} + +proc setfg {c} { + global fglist canv + + foreach w $fglist { + $w conf -foreground $c + } + allcanvs itemconf text -fill $c + $canv itemconf circle -outline $c +} + +proc prefscan {} { + global maxwidth maxgraphpct diffopts + global oldprefs prefstop showneartags + + foreach v {maxwidth maxgraphpct diffopts showneartags} { + set $v $oldprefs($v) + } + catch {destroy $prefstop} + unset prefstop +} + +proc prefsok {} { + global maxwidth maxgraphpct + global oldprefs prefstop showneartags + + catch {destroy $prefstop} + unset prefstop + if {$maxwidth != $oldprefs(maxwidth) + || $maxgraphpct != $oldprefs(maxgraphpct)} { + redisplay + } elseif {$showneartags != $oldprefs(showneartags)} { + reselectline + } +} + +proc formatdate {d} { + return [clock format $d -format "%Y-%m-%d %H:%M:%S"] +} + +# This list of encoding names and aliases is distilled from +# http://www.iana.org/assignments/character-sets. +# Not all of them are supported by Tcl. +set encoding_aliases { + { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII + ISO646-US US-ASCII us IBM367 cp367 csASCII } + { ISO-10646-UTF-1 csISO10646UTF1 } + { ISO_646.basic:1983 ref csISO646basic1983 } + { INVARIANT csINVARIANT } + { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion } + { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom } + { NATS-SEFI iso-ir-8-1 csNATSSEFI } + { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD } + { NATS-DANO iso-ir-9-1 csNATSDANO } + { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD } + { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish } + { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames } + { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 } + { ISO-2022-KR csISO2022KR } + { EUC-KR csEUCKR } + { ISO-2022-JP csISO2022JP } + { ISO-2022-JP-2 csISO2022JP2 } + { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7 + csISO13JISC6220jp } + { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro } + { IT iso-ir-15 ISO646-IT csISO15Italian } + { PT iso-ir-16 ISO646-PT csISO16Portuguese } + { ES iso-ir-17 ISO646-ES csISO17Spanish } + { greek7-old iso-ir-18 csISO18Greek7Old } + { latin-greek iso-ir-19 csISO19LatinGreek } + { DIN_66003 iso-ir-21 de ISO646-DE csISO21German } + { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French } + { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 } + { ISO_5427 iso-ir-37 csISO5427Cyrillic } + { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 } + { BS_viewdata iso-ir-47 csISO47BSViewdata } + { INIS iso-ir-49 csISO49INIS } + { INIS-8 iso-ir-50 csISO50INIS8 } + { INIS-cyrillic iso-ir-51 csISO51INISCyrillic } + { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 } + { ISO_5428:1980 iso-ir-55 csISO5428Greek } + { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 } + { GB_2312-80 iso-ir-58 chinese csISO58GB231280 } + { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian + csISO60Norwegian1 } + { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 } + { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French } + { videotex-suppl iso-ir-70 csISO70VideotexSupp1 } + { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 } + { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 } + { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian } + { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 } + { greek7 iso-ir-88 csISO88Greek7 } + { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 } + { iso-ir-90 csISO90 } + { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a } + { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b + csISO92JISC62991984b } + { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd } + { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand } + { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add + csISO95JIS62291984handadd } + { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana } + { ISO_2033-1983 iso-ir-98 e13b csISO2033 } + { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS } + { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819 + CP819 csISOLatin1 } + { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 } + { T.61-7bit iso-ir-102 csISO102T617bit } + { T.61-8bit T.61 iso-ir-103 csISO103T618bit } + { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 } + { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 } + { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic } + { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 } + { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 } + { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr } + { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708 + arabic csISOLatinArabic } + { ISO_8859-6-E csISO88596E ISO-8859-6-E } + { ISO_8859-6-I csISO88596I ISO-8859-6-I } + { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118 + greek greek8 csISOLatinGreek } + { T.101-G2 iso-ir-128 csISO128T101G2 } + { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew + csISOLatinHebrew } + { ISO_8859-8-E csISO88598E ISO-8859-8-E } + { ISO_8859-8-I csISO88598I ISO-8859-8-I } + { CSN_369103 iso-ir-139 csISO139CSN369103 } + { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 } + { ISO_6937-2-add iso-ir-142 csISOTextComm } + { IEC_P27-1 iso-ir-143 csISO143IECP271 } + { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic + csISOLatinCyrillic } + { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian } + { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian } + { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 } + { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT } + { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba } + { ISO_6937-2-25 iso-ir-152 csISO6937Add } + { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 } + { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp } + { ISO_10367-box iso-ir-155 csISO10367Box } + { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 } + { latin-lap lap iso-ir-158 csISO158Lap } + { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 } + { DS_2089 DS2089 ISO646-DK dk csISO646Danish } + { us-dk csUSDK } + { dk-us csDKUS } + { JIS_X0201 X0201 csHalfWidthKatakana } + { KSC5636 ISO646-KR csKSC5636 } + { ISO-10646-UCS-2 csUnicode } + { ISO-10646-UCS-4 csUCS4 } + { DEC-MCS dec csDECMCS } + { hp-roman8 roman8 r8 csHPRoman8 } + { macintosh mac csMacintosh } + { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl + csIBM037 } + { IBM038 EBCDIC-INT cp038 csIBM038 } + { IBM273 CP273 csIBM273 } + { IBM274 EBCDIC-BE CP274 csIBM274 } + { IBM275 EBCDIC-BR cp275 csIBM275 } + { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 } + { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 } + { IBM280 CP280 ebcdic-cp-it csIBM280 } + { IBM281 EBCDIC-JP-E cp281 csIBM281 } + { IBM284 CP284 ebcdic-cp-es csIBM284 } + { IBM285 CP285 ebcdic-cp-gb csIBM285 } + { IBM290 cp290 EBCDIC-JP-kana csIBM290 } + { IBM297 cp297 ebcdic-cp-fr csIBM297 } + { IBM420 cp420 ebcdic-cp-ar1 csIBM420 } + { IBM423 cp423 ebcdic-cp-gr csIBM423 } + { IBM424 cp424 ebcdic-cp-he csIBM424 } + { IBM437 cp437 437 csPC8CodePage437 } + { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 } + { IBM775 cp775 csPC775Baltic } + { IBM850 cp850 850 csPC850Multilingual } + { IBM851 cp851 851 csIBM851 } + { IBM852 cp852 852 csPCp852 } + { IBM855 cp855 855 csIBM855 } + { IBM857 cp857 857 csIBM857 } + { IBM860 cp860 860 csIBM860 } + { IBM861 cp861 861 cp-is csIBM861 } + { IBM862 cp862 862 csPC862LatinHebrew } + { IBM863 cp863 863 csIBM863 } + { IBM864 cp864 csIBM864 } + { IBM865 cp865 865 csIBM865 } + { IBM866 cp866 866 csIBM866 } + { IBM868 CP868 cp-ar csIBM868 } + { IBM869 cp869 869 cp-gr csIBM869 } + { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 } + { IBM871 CP871 ebcdic-cp-is csIBM871 } + { IBM880 cp880 EBCDIC-Cyrillic csIBM880 } + { IBM891 cp891 csIBM891 } + { IBM903 cp903 csIBM903 } + { IBM904 cp904 904 csIBBM904 } + { IBM905 CP905 ebcdic-cp-tr csIBM905 } + { IBM918 CP918 ebcdic-cp-ar2 csIBM918 } + { IBM1026 CP1026 csIBM1026 } + { EBCDIC-AT-DE csIBMEBCDICATDE } + { EBCDIC-AT-DE-A csEBCDICATDEA } + { EBCDIC-CA-FR csEBCDICCAFR } + { EBCDIC-DK-NO csEBCDICDKNO } + { EBCDIC-DK-NO-A csEBCDICDKNOA } + { EBCDIC-FI-SE csEBCDICFISE } + { EBCDIC-FI-SE-A csEBCDICFISEA } + { EBCDIC-FR csEBCDICFR } + { EBCDIC-IT csEBCDICIT } + { EBCDIC-PT csEBCDICPT } + { EBCDIC-ES csEBCDICES } + { EBCDIC-ES-A csEBCDICESA } + { EBCDIC-ES-S csEBCDICESS } + { EBCDIC-UK csEBCDICUK } + { EBCDIC-US csEBCDICUS } + { UNKNOWN-8BIT csUnknown8BiT } + { MNEMONIC csMnemonic } + { MNEM csMnem } + { VISCII csVISCII } + { VIQR csVIQR } + { KOI8-R csKOI8R } + { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro } + { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro } + { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro } + { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro } + { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro } + { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro } + { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro } + { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro } + { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro } + { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro } + { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro } + { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro } + { IBM1047 IBM-1047 } + { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian } + { Amiga-1251 Ami1251 Amiga1251 Ami-1251 } + { UNICODE-1-1 csUnicode11 } + { CESU-8 csCESU-8 } + { BOCU-1 csBOCU-1 } + { UNICODE-1-1-UTF-7 csUnicode11UTF7 } + { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic + l8 } + { ISO-8859-15 ISO_8859-15 Latin-9 } + { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 } + { GBK CP936 MS936 windows-936 } + { JIS_Encoding csJISEncoding } + { Shift_JIS MS_Kanji csShiftJIS } + { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese + EUC-JP } + { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese } + { ISO-10646-UCS-Basic csUnicodeASCII } + { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 } + { ISO-Unicode-IBM-1261 csUnicodeIBM1261 } + { ISO-Unicode-IBM-1268 csUnicodeIBM1268 } + { ISO-Unicode-IBM-1276 csUnicodeIBM1276 } + { ISO-Unicode-IBM-1264 csUnicodeIBM1264 } + { ISO-Unicode-IBM-1265 csUnicodeIBM1265 } + { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 } + { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 } + { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 } + { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 } + { Adobe-Standard-Encoding csAdobeStandardEncoding } + { Ventura-US csVenturaUS } + { Ventura-International csVenturaInternational } + { PC8-Danish-Norwegian csPC8DanishNorwegian } + { PC8-Turkish csPC8Turkish } + { IBM-Symbols csIBMSymbols } + { IBM-Thai csIBMThai } + { HP-Legal csHPLegal } + { HP-Pi-font csHPPiFont } + { HP-Math8 csHPMath8 } + { Adobe-Symbol-Encoding csHPPSMath } + { HP-DeskTop csHPDesktop } + { Ventura-Math csVenturaMath } + { Microsoft-Publishing csMicrosoftPublishing } + { Windows-31J csWindows31J } + { GB2312 csGB2312 } + { Big5 csBig5 } +} + +proc tcl_encoding {enc} { + global encoding_aliases + set names [encoding names] + set lcnames [string tolower $names] + set enc [string tolower $enc] + set i [lsearch -exact $lcnames $enc] + if {$i < 0} { + # look for "isonnn" instead of "iso-nnn" or "iso_nnn" + if {[regsub {^iso[-_]} $enc iso encx]} { + set i [lsearch -exact $lcnames $encx] + } + } + if {$i < 0} { + foreach l $encoding_aliases { + set ll [string tolower $l] + if {[lsearch -exact $ll $enc] < 0} continue + # look through the aliases for one that tcl knows about + foreach e $ll { + set i [lsearch -exact $lcnames $e] + if {$i < 0} { + if {[regsub {^iso[-_]} $e iso ex]} { + set i [lsearch -exact $lcnames $ex] + } + } + if {$i >= 0} break + } + break + } + } + if {$i >= 0} { + return [lindex $names $i] + } + return {} +} + +# defaults... +set datemode 0 +set diffopts "-U 5 -p" +set wrcomcmd "git diff-tree --stdin -p --pretty" + +set gitencoding {} +catch { + set gitencoding [exec git config --get i18n.commitencoding] +} +if {$gitencoding == ""} { + set gitencoding "utf-8" +} +set tclencoding [tcl_encoding $gitencoding] +if {$tclencoding == {}} { + puts stderr "Warning: encoding $gitencoding is not supported by Tcl/Tk" +} + +set mainfont {Helvetica 9} +set textfont {Courier 9} +set uifont {Helvetica 9 bold} +set findmergefiles 0 +set maxgraphpct 50 +set maxwidth 16 +set revlistorder 0 +set fastdate 0 +set uparrowlen 7 +set downarrowlen 7 +set mingaplen 30 +set cmitmode "patch" +set wrapcomment "none" +set showneartags 1 + +set colors {green red blue magenta darkgrey brown orange} +set bgcolor white +set fgcolor black +set diffcolors {red "#00a000" blue} + +catch {source ~/.gitk} + +font create optionfont -family sans-serif -size -12 + +set revtreeargs {} +foreach arg $argv { + switch -regexp -- $arg { + "^$" { } + "^-d" { set datemode 1 } + default { + lappend revtreeargs $arg + } + } +} + +# check that we can find a .git directory somewhere... +set gitdir [gitdir] +if {![file isdirectory $gitdir]} { + show_error {} . "Cannot find the git directory \"$gitdir\"." + exit 1 +} + +set cmdline_files {} +set i [lsearch -exact $revtreeargs "--"] +if {$i >= 0} { + set cmdline_files [lrange $revtreeargs [expr {$i + 1}] end] + 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 cmdline_files [split $f "\n"] + set n [llength $cmdline_files] + set revtreeargs [lrange $revtreeargs 0 end-$n] + } err]} { + # unfortunately we get both stdout and stderr in $err, + # so look for "fatal:". + set i [string first "fatal:" $err] + if {$i > 0} { + set err [string range $err [expr {$i + 6}] end] + } + 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 None +set viewfiles(0) {} +set viewperm(0) 0 +set viewargs(0) {} + +set cmdlineok 0 +set stopped 0 +set stuffsaved 0 +set patchnum 0 +setcoords +makewindow +wm title . "[file tail $argv0]: [file tail [pwd]]" +readrefs + +if {$cmdline_files ne {} || $revtreeargs ne {}} { + # create a view for the files/dirs specified on the command line + set curview 1 + set selectedview 1 + set nextviewnum 2 + set viewname(1) "Command line" + set viewfiles(1) $cmdline_files + set viewargs(1) $revtreeargs + set viewperm(1) 0 + addviewmenu 1 + .bar.view entryconf Edit* -state normal + .bar.view entryconf Delete* -state normal +} + +if {[info exists permviews]} { + foreach v $permviews { + set n $nextviewnum + incr nextviewnum + set viewname($n) [lindex $v 0] + set viewfiles($n) [lindex $v 1] + set viewargs($n) [lindex $v 2] + set viewperm($n) 1 + addviewmenu $n + } +} +getcommits diff --git a/gitweb/README b/gitweb/README new file mode 100644 index 0000000000..e02e90f042 --- /dev/null +++ b/gitweb/README @@ -0,0 +1,82 @@ +GIT web Interface +================= + +The one working on: + http://www.kernel.org/git/ + +From the git version 1.4.0 gitweb is bundled with git. + + +How to configure gitweb for your local system +--------------------------------------------- + +You can specify the following configuration variables when building GIT: + * GITWEB_SITENAME + Shown in the title of all generated pages, defaults to the servers name. + * GITWEB_PROJECTROOT + The root directory for all projects shown by gitweb. + * GITWEB_LIST + points to a directory to scan for projects (defaults to project root) + or to a file for explicit listing of projects. + * GITWEB_HOMETEXT + points to an .html file which is included on the gitweb project + overview page. + * GITWEB_CSS + Points to the location where you put gitweb.css on your web server. + * GITWEB_LOGO + Points to the location where you put git-logo.png on your web server. + * GITWEB_CONFIG + This file will be loaded using 'require' and can be used to override any + of the options above as well as some other options - see the top of + 'gitweb.cgi' for their full list and description. If the environment + $GITWEB_CONFIG is set when gitweb.cgi is executed the file in the + environment variable will be loaded instead of the file + specified when gitweb.cgi was created. + + +Runtime gitweb configuration +---------------------------- + +You can adjust gitweb behaviour using the file specified in `GITWEB_CONFIG` +(defaults to 'gitweb_config.perl' in the same directory as the CGI). +See the top of 'gitweb.cgi' for the list of variables and some description. +The most notable thing that is not configurable at compile time are the +optional features, stored in the '%features' variable. You can find further +description on how to reconfigure the default features setting in your +`GITWEB_CONFIG` or per-project in `project.git/config` inside 'gitweb.cgi'. + + +Webserver configuration +----------------------- + +If you want to have one URL for both gitweb and your http:// +repositories, you can configure apache like this: + +<VirtualHost www:80> + ServerName git.domain.org + DocumentRoot /pub/git + RewriteEngine on + RewriteRule ^/(.*\.git/(?!/?(info|objects|refs)).*)?$ /cgi-bin/gitweb.cgi%{REQUEST_URI} [L,PT] + SetEnv GITWEB_CONFIG /etc/gitweb.conf +</VirtualHost> + +The above configuration expects your public repositories to live under +/pub/git and will serve them as http://git.domain.org/dir-under-pub-git, +both as cloneable GIT URL and as browseable gitweb interface. +If you then start your git-daemon with --base-path=/pub/git --export-all +then you can even use the git:// URL with exactly the same path. + +Setting the environment variable GITWEB_CONFIG will tell gitweb to use +the named file (i.e. in this example /etc/gitweb.conf) as a +configuration for gitweb. Perl variables defined in here will +override the defaults given at the head of the gitweb.perl (or +gitweb.cgi). Look at the comments in that file for information on +which variables and what they mean. + + +Originally written by: + Kay Sievers <kay.sievers@vrfy.org> + +Any comment/question/concern to: + Git mailing list <git@vger.kernel.org> + diff --git a/gitweb/git-favicon.png b/gitweb/git-favicon.png Binary files differnew file mode 100644 index 0000000000..de637c0608 --- /dev/null +++ b/gitweb/git-favicon.png diff --git a/gitweb/git-logo.png b/gitweb/git-logo.png Binary files differnew file mode 100644 index 0000000000..16ae8d5382 --- /dev/null +++ b/gitweb/git-logo.png diff --git a/gitweb/gitweb.css b/gitweb/gitweb.css new file mode 100644 index 0000000000..7177c6e86b --- /dev/null +++ b/gitweb/gitweb.css @@ -0,0 +1,465 @@ +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; +} + +span.cntrl { + border: dashed #aaaaaa; + border-width: 1px; + padding: 0px 2px 0px 2px; + margin: 0px 2px 0px 2px; +} + +img.logo { + float: right; + border-width: 0px; +} + +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; + font-weight: bold; + 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; + font-family: monospace; +} + +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; + font-family: monospace; +} + +div.log_body { + padding: 8px 8px 8px 150px; +} + +span.age { + position: relative; + float: left; + width: 142px; + font-style: italic; +} + +div.page_body span.signoff { + color: #888888; +} + +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; +} + +div.author_date { + padding: 8px; + border: solid #d9d8d1; + border-width: 0px 0px 1px 0px; + font-style: italic; +} + +a.list { + text-decoration: none; + color: #000000; +} + +a.subject, a.name { + font-weight: bold; +} + +table.tags a.subject { + font-weight: normal; +} + +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; +} + +table.project_list { + border-spacing: 0; +} + +table.diff_tree { + border-spacing: 0; + font-family: monospace; +} + +table.blame { + border-collapse: collapse; +} + +table.blame td { + padding: 0px 5px; + font-size: 12px; + vertical-align: top; +} + +th { + padding: 2px 5px; + font-size: 12px; + text-align: left; +} + +tr.light:hover { + background-color: #edece6; +} + +tr.dark { + background-color: #f6f6f0; +} + +tr.dark2 { + background-color: #f6f6f0; +} + +tr.dark:hover { + background-color: #edece6; +} + +td { + padding: 2px 5px; + font-size: 12px; + vertical-align: top; +} + +td.link, td.selflink { + padding: 2px 5px; + font-family: sans-serif; + font-size: 10px; +} + +td.selflink { + padding-right: 0px; +} + +td.sha1 { + font-family: monospace; +} + +td.error { + color: red; + background-color: yellow; +} + +td.current_head { + text-decoration: underline; +} + +table.diff_tree span.file_status.new { + color: #008000; +} + +table.diff_tree span.file_status.deleted { + color: #c00000; +} + +table.diff_tree span.file_status.moved, +table.diff_tree span.file_status.mode_chnge { + color: #777777; +} + +table.diff_tree span.file_status.copied { + color: #70a070; +} + +/* age2: 60*60*24*2 <= age */ +table.project_list td.age2, table.blame td.age2 { + font-style: italic; +} + +/* age1: 60*60*2 <= age < 60*60*24*2 */ +table.project_list td.age1 { + color: #009900; + font-style: italic; +} + +table.blame td.age1 { + color: #009900; + background: transparent; +} + +/* age0: age < 60*60*2 */ +table.project_list td.age0 { + color: #009900; + font-style: italic; + font-weight: bold; +} + +table.blame td.age0 { + color: #009900; + background: transparent; + font-weight: bold; +} + +td.pre, div.pre, div.diff { + font-family: monospace; + font-size: 12px; + white-space: pre; +} + +td.mode { + font-family: monospace; +} + +/* styling of diffs (patchsets): commitdiff and blobdiff views */ +div.diff.header, +div.diff.extended_header { + white-space: normal; +} + +div.diff.header { + font-weight: bold; + + background-color: #edece6; + + margin-top: 4px; + padding: 4px 0px 2px 0px; + border: solid #d9d8d1; + border-width: 1px 0px 1px 0px; +} + +div.diff.header a.path { + text-decoration: underline; +} + +div.diff.extended_header, +div.diff.extended_header a.path, +div.diff.extended_header a.hash { + color: #777777; +} + +div.diff.extended_header .info { + color: #b0b0b0; +} + +div.diff.extended_header { + background-color: #f6f5ee; + padding: 2px 0px 2px 0px; +} + +div.diff a.list, +div.diff a.path, +div.diff a.hash { + text-decoration: none; +} + +div.diff a.list:hover, +div.diff a.path:hover, +div.diff a.hash:hover { + text-decoration: underline; +} + +div.diff.to_file a.path, +div.diff.to_file { + color: #007000; +} + +div.diff.add { + color: #008800; +} + +div.diff.from_file a.path, +div.diff.from_file { + color: #aa0000; +} + +div.diff.rem { + color: #cc0000; +} + +div.diff.chunk_header a, +div.diff.chunk_header { + color: #990099; +} + +div.diff.chunk_header { + border: dotted #ffe0ff; + border-width: 1px 0px 0px 0px; + margin-top: 2px; +} + +div.diff.chunk_header span.chunk_info { + background-color: #ffeeff; +} + +div.diff.chunk_header span.section { + color: #aa22aa; +} + +div.diff.incomplete { + color: #cccccc; +} + + +div.index_include { + border: solid #d9d8d1; + border-width: 0px 0px 1px; + padding: 12px 8px; +} + +div.search { + font-size: 12px; + font-weight: normal; + margin: 4px 8px; + position: absolute; + top: 56px; + right: 12px +} + +td.linenr { + text-align: right; +} + +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.refs span { + padding: 0px 4px; + font-size: 10px; + font-weight: normal; + border: 1px solid; + background-color: #ffaaff; + border-color: #ffccff #ff00ee #ff00ee #ffccff; +} + +span.refs span.ref { + background-color: #aaaaff; + border-color: #ccccff #0033cc #0033cc #ccccff; +} + +span.refs span.tag { + background-color: #ffffaa; + border-color: #ffffcc #ffee00 #ffee00 #ffffcc; +} + +span.refs span.head { + background-color: #aaffaa; + border-color: #ccffcc #00cc33 #00cc33 #ccffcc; +} + +span.atnight { + color: #cc0000; +} + +span.match { + color: #e00000; +} diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl new file mode 100755 index 0000000000..653ca3cc60 --- /dev/null +++ b/gitweb/gitweb.perl @@ -0,0 +1,4727 @@ +#!/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'; +use File::Find qw(); +use File::Basename qw(basename); +binmode STDOUT, ':utf8'; + +BEGIN { + CGI->compile() if $ENV{MOD_PERL}; +} + +our $cgi = new CGI; +our $version = "++GIT_VERSION++"; +our $my_url = $cgi->url(); +our $my_uri = $cgi->url(-absolute => 1); + +# core git executable to use +# this can just be "git" if your webserver has a sensible PATH +our $GIT = "++GIT_BINDIR++/git"; + +# absolute fs-path which will be prepended to the project path +#our $projectroot = "/pub/scm"; +our $projectroot = "++GITWEB_PROJECTROOT++"; + +# target of the home link on top of all pages +our $home_link = $my_uri || "/"; + +# string of the home link on top of all pages +our $home_link_str = "++GITWEB_HOME_LINK_STR++"; + +# name of your site or organization to appear in page titles +# replace this with something more descriptive for clearer bookmarks +our $site_name = "++GITWEB_SITENAME++" + || ($ENV{'SERVER_NAME'} || "Untitled") . " Git"; + +# filename of html text to include at top of each page +our $site_header = "++GITWEB_SITE_HEADER++"; +# html text to include at home page +our $home_text = "++GITWEB_HOMETEXT++"; +# filename of html text to include at bottom of each page +our $site_footer = "++GITWEB_SITE_FOOTER++"; + +# URI of stylesheets +our @stylesheets = ("++GITWEB_CSS++"); +# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG. +our $stylesheet = undef; +# URI of GIT logo (72x27 size) +our $logo = "++GITWEB_LOGO++"; +# URI of GIT favicon, assumed to be image/png type +our $favicon = "++GITWEB_FAVICON++"; + +# URI and label (title) of GIT logo link +#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/"; +#our $logo_label = "git documentation"; +our $logo_url = "http://git.or.cz/"; +our $logo_label = "git homepage"; + +# source of projects list +our $projects_list = "++GITWEB_LIST++"; + +# show repository only if this file exists +# (only effective if this variable evaluates to true) +our $export_ok = "++GITWEB_EXPORT_OK++"; + +# only allow viewing of repositories also shown on the overview page +our $strict_export = "++GITWEB_STRICT_EXPORT++"; + +# list of git base URLs used for URL to where fetch project from, +# i.e. full URL is "$git_base_url/$project" +our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++"); + +# default blob_plain mimetype and default charset for text/plain blob +our $default_blob_plain_mimetype = 'text/plain'; +our $default_text_plain_charset = undef; + +# file to use for guessing MIME types before trying /etc/mime.types +# (relative to the current git repository) +our $mimetypes_file = undef; + +# You define site-wide feature defaults here; override them with +# $GITWEB_CONFIG as necessary. +our %feature = ( + # feature => { + # 'sub' => feature-sub (subroutine), + # 'override' => allow-override (boolean), + # 'default' => [ default options...] (array reference)} + # + # if feature is overridable (it means that allow-override has true value, + # then feature-sub will be called with default options as parameters; + # return value of feature-sub indicates if to enable specified feature + # + # use gitweb_check_feature(<feature>) to check if <feature> is enabled + + # Enable the 'blame' blob view, showing the last commit that modified + # each line in the file. This can be very CPU-intensive. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'blame'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'blame'}{'override'} = 1; + # and in project config gitweb.blame = 0|1; + 'blame' => { + 'sub' => \&feature_blame, + 'override' => 0, + 'default' => [0]}, + + # Enable the 'snapshot' link, providing a compressed tarball of any + # tree. This can potentially generate high traffic if you have large + # project. + + # To disable system wide have in $GITWEB_CONFIG + # $feature{'snapshot'}{'default'} = [undef]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'snapshot'}{'override'} = 1; + # and in project config gitweb.snapshot = none|gzip|bzip2; + 'snapshot' => { + 'sub' => \&feature_snapshot, + 'override' => 0, + # => [content-encoding, suffix, program] + 'default' => ['x-gzip', 'gz', 'gzip']}, + + # Enable text search, which will list the commits which match author, + # committer or commit text to a given string. Enabled by default. + 'search' => { + 'override' => 0, + 'default' => [1]}, + + # Enable the pickaxe search, which will list the commits that modified + # a given string in a file. This can be practical and quite faster + # alternative to 'blame', but still potentially CPU-intensive. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'pickaxe'}{'default'} = [1]; + # To have project specific config enable override in $GITWEB_CONFIG + # $feature{'pickaxe'}{'override'} = 1; + # and in project config gitweb.pickaxe = 0|1; + 'pickaxe' => { + 'sub' => \&feature_pickaxe, + 'override' => 0, + 'default' => [1]}, + + # Make gitweb use an alternative format of the URLs which can be + # more readable and natural-looking: project name is embedded + # directly in the path and the query string contains other + # auxiliary information. All gitweb installations recognize + # URL in either format; this configures in which formats gitweb + # generates links. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'pathinfo'}{'default'} = [1]; + # Project specific override is not supported. + + # Note that you will need to change the default location of CSS, + # favicon, logo and possibly other files to an absolute URL. Also, + # if gitweb.cgi serves as your indexfile, you will need to force + # $my_uri to contain the script name in your $GITWEB_CONFIG. + 'pathinfo' => { + 'override' => 0, + 'default' => [0]}, + + # Make gitweb consider projects in project root subdirectories + # to be forks of existing projects. Given project $projname.git, + # projects matching $projname/*.git will not be shown in the main + # projects list, instead a '+' mark will be added to $projname + # there and a 'forks' view will be enabled for the project, listing + # all the forks. This feature is supported only if project list + # is taken from a directory, not file. + + # To enable system wide have in $GITWEB_CONFIG + # $feature{'forks'}{'default'} = [1]; + # Project specific override is not supported. + 'forks' => { + 'override' => 0, + 'default' => [0]}, +); + +sub gitweb_check_feature { + my ($name) = @_; + return unless exists $feature{$name}; + my ($sub, $override, @defaults) = ( + $feature{$name}{'sub'}, + $feature{$name}{'override'}, + @{$feature{$name}{'default'}}); + if (!$override) { return @defaults; } + if (!defined $sub) { + warn "feature $name is not overrideable"; + return @defaults; + } + return $sub->(@defaults); +} + +sub feature_blame { + my ($val) = git_get_project_config('blame', '--bool'); + + if ($val eq 'true') { + return 1; + } elsif ($val eq 'false') { + return 0; + } + + return $_[0]; +} + +sub feature_snapshot { + my ($ctype, $suffix, $command) = @_; + + my ($val) = git_get_project_config('snapshot'); + + if ($val eq 'gzip') { + return ('x-gzip', 'gz', 'gzip'); + } elsif ($val eq 'bzip2') { + return ('x-bzip2', 'bz2', 'bzip2'); + } elsif ($val eq 'none') { + return (); + } + + return ($ctype, $suffix, $command); +} + +sub gitweb_have_snapshot { + my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot'); + my $have_snapshot = (defined $ctype && defined $suffix); + + return $have_snapshot; +} + +sub feature_pickaxe { + my ($val) = git_get_project_config('pickaxe', '--bool'); + + if ($val eq 'true') { + return (1); + } elsif ($val eq 'false') { + return (0); + } + + return ($_[0]); +} + +# checking HEAD file with -e is fragile if the repository was +# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed +# and then pruned. +sub check_head_link { + my ($dir) = @_; + my $headfile = "$dir/HEAD"; + return ((-e $headfile) || + (-l $headfile && readlink($headfile) =~ /^refs\/heads\//)); +} + +sub check_export_ok { + my ($dir) = @_; + return (check_head_link($dir) && + (!$export_ok || -e "$dir/$export_ok")); +} + +# rename detection options for git-diff and git-diff-tree +# - default is '-M', with the cost proportional to +# (number of removed files) * (number of new files). +# - more costly is '-C' (or '-C', '-M'), with the cost proportional to +# (number of changed files + number of removed files) * (number of new files) +# - even more costly is '-C', '--find-copies-harder' with cost +# (number of files in the original tree) * (number of new files) +# - one might want to include '-B' option, e.g. '-B', '-M' +our @diff_opts = ('-M'); # taken from git_commit + +our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++"; +do $GITWEB_CONFIG if -e $GITWEB_CONFIG; + +# version of the core git binary +our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown"; + +$projects_list ||= $projectroot; + +# ====================================================================== +# input validation and dispatch +our $action = $cgi->param('a'); +if (defined $action) { + if ($action =~ m/[^0-9a-zA-Z\.\-_]/) { + die_error(undef, "Invalid action parameter"); + } +} + +# parameters which are pathnames +our $project = $cgi->param('p'); +if (defined $project) { + if (!validate_pathname($project) || + !(-d "$projectroot/$project") || + !check_head_link("$projectroot/$project") || + ($export_ok && !(-e "$projectroot/$project/$export_ok")) || + ($strict_export && !project_in_list($project))) { + undef $project; + die_error(undef, "No such project"); + } +} + +our $file_name = $cgi->param('f'); +if (defined $file_name) { + if (!validate_pathname($file_name)) { + die_error(undef, "Invalid file parameter"); + } +} + +our $file_parent = $cgi->param('fp'); +if (defined $file_parent) { + if (!validate_pathname($file_parent)) { + die_error(undef, "Invalid file parent parameter"); + } +} + +# parameters which are refnames +our $hash = $cgi->param('h'); +if (defined $hash) { + if (!validate_refname($hash)) { + die_error(undef, "Invalid hash parameter"); + } +} + +our $hash_parent = $cgi->param('hp'); +if (defined $hash_parent) { + if (!validate_refname($hash_parent)) { + die_error(undef, "Invalid hash parent parameter"); + } +} + +our $hash_base = $cgi->param('hb'); +if (defined $hash_base) { + if (!validate_refname($hash_base)) { + die_error(undef, "Invalid hash base parameter"); + } +} + +our $hash_parent_base = $cgi->param('hpb'); +if (defined $hash_parent_base) { + if (!validate_refname($hash_parent_base)) { + die_error(undef, "Invalid hash parent base parameter"); + } +} + +# other parameters +our $page = $cgi->param('pg'); +if (defined $page) { + if ($page =~ m/[^0-9]/) { + die_error(undef, "Invalid page parameter"); + } +} + +our $searchtext = $cgi->param('s'); +if (defined $searchtext) { + if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) { + die_error(undef, "Invalid search parameter"); + } + if (length($searchtext) < 2) { + die_error(undef, "At least two characters are required for search parameter"); + } + $searchtext = quotemeta $searchtext; +} + +our $searchtype = $cgi->param('st'); +if (defined $searchtype) { + if ($searchtype =~ m/[^a-z]/) { + die_error(undef, "Invalid searchtype parameter"); + } +} + +# now read PATH_INFO and use it as alternative to parameters +sub evaluate_path_info { + return if defined $project; + my $path_info = $ENV{"PATH_INFO"}; + return if !$path_info; + $path_info =~ s,^/+,,; + return if !$path_info; + # find which part of PATH_INFO is project + $project = $path_info; + $project =~ s,/+$,,; + while ($project && !check_head_link("$projectroot/$project")) { + $project =~ s,/*[^/]*$,,; + } + # validate project + $project = validate_pathname($project); + if (!$project || + ($export_ok && !-e "$projectroot/$project/$export_ok") || + ($strict_export && !project_in_list($project))) { + undef $project; + return; + } + # do not change any parameters if an action is given using the query string + return if $action; + $path_info =~ s,^$project/*,,; + my ($refname, $pathname) = split(/:/, $path_info, 2); + if (defined $pathname) { + # we got "project.git/branch:filename" or "project.git/branch:dir/" + # we could use git_get_type(branch:pathname), but it needs $git_dir + $pathname =~ s,^/+,,; + if (!$pathname || substr($pathname, -1) eq "/") { + $action ||= "tree"; + $pathname =~ s,/$,,; + } else { + $action ||= "blob_plain"; + } + $hash_base ||= validate_refname($refname); + $file_name ||= validate_pathname($pathname); + } elsif (defined $refname) { + # we got "project.git/branch" + $action ||= "shortlog"; + $hash ||= validate_refname($refname); + } +} +evaluate_path_info(); + +# path to the current git repository +our $git_dir; +$git_dir = "$projectroot/$project" if $project; + +# dispatch +my %actions = ( + "blame" => \&git_blame2, + "blobdiff" => \&git_blobdiff, + "blobdiff_plain" => \&git_blobdiff_plain, + "blob" => \&git_blob, + "blob_plain" => \&git_blob_plain, + "commitdiff" => \&git_commitdiff, + "commitdiff_plain" => \&git_commitdiff_plain, + "commit" => \&git_commit, + "forks" => \&git_forks, + "heads" => \&git_heads, + "history" => \&git_history, + "log" => \&git_log, + "rss" => \&git_rss, + "atom" => \&git_atom, + "search" => \&git_search, + "search_help" => \&git_search_help, + "shortlog" => \&git_shortlog, + "summary" => \&git_summary, + "tag" => \&git_tag, + "tags" => \&git_tags, + "tree" => \&git_tree, + "snapshot" => \&git_snapshot, + "object" => \&git_object, + # those below don't need $project + "opml" => \&git_opml, + "project_list" => \&git_project_list, + "project_index" => \&git_project_index, +); + +if (defined $project) { + $action ||= 'summary'; +} else { + $action ||= 'project_list'; +} +if (!defined($actions{$action})) { + die_error(undef, "Unknown action"); +} +if ($action !~ m/^(opml|project_list|project_index)$/ && + !$project) { + die_error(undef, "Project needed"); +} +$actions{$action}->(); +exit; + +## ====================================================================== +## action links + +sub href(%) { + my %params = @_; + # default is to use -absolute url() i.e. $my_uri + my $href = $params{-full} ? $my_url : $my_uri; + + # XXX: Warning: If you touch this, check the search form for updating, + # too. + + my @mapping = ( + project => "p", + action => "a", + file_name => "f", + file_parent => "fp", + hash => "h", + hash_parent => "hp", + hash_base => "hb", + hash_parent_base => "hpb", + page => "pg", + order => "o", + searchtext => "s", + searchtype => "st", + ); + my %mapping = @mapping; + + $params{'project'} = $project unless exists $params{'project'}; + + my ($use_pathinfo) = gitweb_check_feature('pathinfo'); + if ($use_pathinfo) { + # use PATH_INFO for project name + $href .= "/$params{'project'}" if defined $params{'project'}; + delete $params{'project'}; + + # Summary just uses the project path URL + if (defined $params{'action'} && $params{'action'} eq 'summary') { + delete $params{'action'}; + } + } + + # now encode the parameters explicitly + my @result = (); + for (my $i = 0; $i < @mapping; $i += 2) { + my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]); + if (defined $params{$name}) { + push @result, $symbol . "=" . esc_param($params{$name}); + } + } + $href .= "?" . join(';', @result) if scalar @result; + + return $href; +} + + +## ====================================================================== +## validation, quoting/unquoting and escaping + +sub validate_pathname { + my $input = shift || return undef; + + # no '.' or '..' as elements of path, i.e. no '.' nor '..' + # at the beginning, at the end, and between slashes. + # also this catches doubled slashes + if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) { + return undef; + } + # no null characters + if ($input =~ m!\0!) { + return undef; + } + return $input; +} + +sub validate_refname { + my $input = shift || return undef; + + # textual hashes are O.K. + if ($input =~ m/^[0-9a-fA-F]{40}$/) { + return $input; + } + # it must be correct pathname + $input = validate_pathname($input) + or return undef; + # restrictions on ref name according to git-check-ref-format + if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) { + return undef; + } + return $input; +} + +# very thin wrapper for decode("utf8", $str, Encode::FB_DEFAULT); +sub to_utf8 { + my $str = shift; + return decode("utf8", $str, Encode::FB_DEFAULT); +} + +# 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; +} + +# quote unsafe chars in whole URL, so some charactrs cannot be quoted +sub esc_url { + 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; + my %opts = @_; + + $str = to_utf8($str); + $str = escapeHTML($str); + if ($opts{'-nbsp'}) { + $str =~ s/ / /g; + } + $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg; + return $str; +} + +# quote control characters and escape filename to HTML +sub esc_path { + my $str = shift; + my %opts = @_; + + $str = to_utf8($str); + $str = escapeHTML($str); + if ($opts{'-nbsp'}) { + $str =~ s/ / /g; + } + $str =~ s|([[:cntrl:]])|quot_cec($1)|eg; + return $str; +} + +# Make control characters "printable", using character escape codes (CEC) +sub quot_cec { + my $cntrl = shift; + my %es = ( # character escape codes, aka escape sequences + "\t" => '\t', # tab (HT) + "\n" => '\n', # line feed (LF) + "\r" => '\r', # carrige return (CR) + "\f" => '\f', # form feed (FF) + "\b" => '\b', # backspace (BS) + "\a" => '\a', # alarm (bell) (BEL) + "\e" => '\e', # escape (ESC) + "\013" => '\v', # vertical tab (VT) + "\000" => '\0', # nul character (NUL) + ); + my $chr = ( (exists $es{$cntrl}) + ? $es{$cntrl} + : sprintf('\%03o', ord($cntrl)) ); + return "<span class=\"cntrl\">$chr</span>"; +} + +# Alternatively use unicode control pictures codepoints, +# Unicode "printable representation" (PR) +sub quot_upr { + my $cntrl = shift; + my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl)); + return "<span class=\"cntrl\">$chr</span>"; +} + +# git may return quoted and escaped filenames +sub unquote { + my $str = shift; + + sub unq { + my $seq = shift; + my %es = ( # character escape codes, aka escape sequences + 't' => "\t", # tab (HT, TAB) + 'n' => "\n", # newline (NL) + 'r' => "\r", # return (CR) + 'f' => "\f", # form feed (FF) + 'b' => "\b", # backspace (BS) + 'a' => "\a", # alarm (bell) (BEL) + 'e' => "\e", # escape (ESC) + 'v' => "\013", # vertical tab (VT) + ); + + if ($seq =~ m/^[0-7]{1,3}$/) { + # octal char sequence + return chr(oct($seq)); + } elsif (exists $es{$seq}) { + # C escape sequence, aka character escape code + return $es{$seq} + } + # quoted ordinary character + return $seq; + } + + if ($str =~ m/^"(.*)"$/) { + # needs unquoting + $str = $1; + $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg; + } + return $str; +} + +# escape tabs (convert tabs to spaces) +sub untabify { + my $line = shift; + + while ((my $pos = index($line, "\t")) != -1) { + if (my $count = (8 - ($pos % 8))) { + my $spaces = ' ' x $count; + $line =~ s/\t/$spaces/; + } + } + + return $line; +} + +sub project_in_list { + my $project = shift; + my @list = git_get_projects_list(); + return @list && scalar(grep { $_->{'path'} eq $project } @list); +} + +## ---------------------------------------------------------------------- +## HTML aware string manipulation + +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 = " ..."; + $body =~ s/&[^;]*$//; # remove chopped character entities + } + return "$body$tail"; +} + +## ---------------------------------------------------------------------- +## functions returning short strings + +# CSS class for given age value (in seconds) +sub age_class { + my $age = shift; + + if ($age < 60*60*2) { + return "age0"; + } elsif ($age < 60*60*24*2) { + return "age1"; + } else { + return "age2"; + } +} + +# convert age in seconds to "nn units ago" string +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; +} + +# convert file mode in octal to symbolic file mode string +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 '----------'; + } +} + +# convert file mode in octal to file type string +sub file_type { + my $mode = shift; + + if ($mode !~ m/^[0-7]+$/) { + return $mode; + } else { + $mode = oct $mode; + } + + if (S_ISDIR($mode & S_IFMT)) { + return "directory"; + } elsif (S_ISLNK($mode)) { + return "symlink"; + } elsif (S_ISREG($mode)) { + return "file"; + } else { + return "unknown"; + } +} + +# convert file mode in octal to file type description string +sub file_type_long { + my $mode = shift; + + if ($mode !~ m/^[0-7]+$/) { + return $mode; + } else { + $mode = oct $mode; + } + + if (S_ISDIR($mode & S_IFMT)) { + return "directory"; + } elsif (S_ISLNK($mode)) { + return "symlink"; + } elsif (S_ISREG($mode)) { + if ($mode & S_IXUSR) { + return "executable"; + } else { + return "file"; + }; + } else { + return "unknown"; + } +} + + +## ---------------------------------------------------------------------- +## functions returning short HTML fragments, or transforming HTML fragments +## which don't belong to other sections + +# format line of commit message. +sub format_log_line_html { + my $line = shift; + + $line = esc_html($line, -nbsp=>1); + if ($line =~ m/([0-9a-fA-F]{8,40})/) { + my $hash_text = $1; + my $link = + $cgi->a({-href => href(action=>"object", hash=>$hash_text), + -class => "text"}, $hash_text); + $line =~ s/$hash_text/$link/; + } + return $line; +} + +# format marker of refs pointing to given object +sub format_ref_marker { + my ($refs, $id) = @_; + my $markers = ''; + + if (defined $refs->{$id}) { + foreach my $ref (@{$refs->{$id}}) { + my ($type, $name) = qw(); + # e.g. tags/v2.6.11 or heads/next + if ($ref =~ m!^(.*?)s?/(.*)$!) { + $type = $1; + $name = $2; + } else { + $type = "ref"; + $name = $ref; + } + + $markers .= " <span class=\"$type\" title=\"$ref\">" . + esc_html($name) . "</span>"; + } + } + + if ($markers) { + return ' <span class="refs">'. $markers . '</span>'; + } else { + return ""; + } +} + +# format, perhaps shortened and with markers, title line +sub format_subject_html { + my ($long, $short, $href, $extra) = @_; + $extra = '' unless defined($extra); + + if (length($short) < length($long)) { + return $cgi->a({-href => $href, -class => "list subject", + -title => to_utf8($long)}, + esc_html($short) . $extra); + } else { + return $cgi->a({-href => $href, -class => "list subject"}, + esc_html($long) . $extra); + } +} + +# format patch (diff) line (rather not to be used for diff headers) +sub format_diff_line { + my $line = shift; + my ($from, $to) = @_; + my $char = substr($line, 0, 1); + my $diff_class = ""; + + chomp $line; + + if ($char eq '+') { + $diff_class = " add"; + } elsif ($char eq "-") { + $diff_class = " rem"; + } elsif ($char eq "@") { + $diff_class = " chunk_header"; + } elsif ($char eq "\\") { + $diff_class = " incomplete"; + } + $line = untabify($line); + if ($from && $to && $line =~ m/^\@{2} /) { + my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) = + $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/; + + $from_lines = 0 unless defined $from_lines; + $to_lines = 0 unless defined $to_lines; + + if ($from->{'href'}) { + $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start", + -class=>"list"}, $from_text); + } + if ($to->{'href'}) { + $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start", + -class=>"list"}, $to_text); + } + $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" . + "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>"; + return "<div class=\"diff$diff_class\">$line</div>\n"; + } + return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n"; +} + +## ---------------------------------------------------------------------- +## git utility subroutines, invoking git commands + +# returns path to the core git executable and the --git-dir parameter as list +sub git_cmd { + return $GIT, '--git-dir='.$git_dir; +} + +# returns path to the core git executable and the --git-dir parameter as string +sub git_cmd_str { + return join(' ', git_cmd()); +} + +# get HEAD ref of given project as hash +sub git_get_head_hash { + my $project = shift; + my $o_git_dir = $git_dir; + my $retval = undef; + $git_dir = "$projectroot/$project"; + if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") { + my $head = <$fd>; + close $fd; + if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) { + $retval = $1; + } + } + if (defined $o_git_dir) { + $git_dir = $o_git_dir; + } + return $retval; +} + +# get type of given object +sub git_get_type { + my $hash = shift; + + open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return; + my $type = <$fd>; + close $fd or return; + chomp $type; + return $type; +} + +sub git_get_project_config { + my ($key, $type) = @_; + + return unless ($key); + $key =~ s/^gitweb\.//; + return if ($key =~ m/\W/); + + my @x = (git_cmd(), 'config'); + if (defined $type) { push @x, $type; } + push @x, "--get"; + push @x, "gitweb.$key"; + my $val = qx(@x); + chomp $val; + return ($val); +} + +# get hash of given path at given ref +sub git_get_hash_by_path { + my $base = shift; + my $path = shift || return undef; + my $type = shift; + + $path =~ s,/+$,,; + + open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path + or die_error(undef, "Open git-ls-tree failed"); + my $line = <$fd>; + close $fd or return undef; + + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/; + if (defined $type && $type ne $2) { + # type doesn't match + return undef; + } + return $3; +} + +## ...................................................................... +## git utility functions, directly accessing git repository + +sub git_get_project_description { + my $path = shift; + + open my $fd, "$projectroot/$path/description" or return undef; + my $descr = <$fd>; + close $fd; + chomp $descr; + return $descr; +} + +sub git_get_project_url_list { + my $path = shift; + + open my $fd, "$projectroot/$path/cloneurl" or return; + my @git_project_url_list = map { chomp; $_ } <$fd>; + close $fd; + + return wantarray ? @git_project_url_list : \@git_project_url_list; +} + +sub git_get_projects_list { + my ($filter) = @_; + my @list; + + $filter ||= ''; + $filter =~ s/\.git$//; + + if (-d $projects_list) { + # search in directory + my $dir = $projects_list . ($filter ? "/$filter" : ''); + # remove the trailing "/" + $dir =~ s!/+$!!; + my $pfxlen = length("$dir"); + + my ($check_forks) = gitweb_check_feature('forks'); + + File::Find::find({ + follow_fast => 1, # follow symbolic links + dangling_symlinks => 0, # ignore dangling symlinks, silently + wanted => sub { + # skip project-list toplevel, if we get it. + return if (m!^[/.]$!); + # only directories can be git repositories + return unless (-d $_); + + my $subdir = substr($File::Find::name, $pfxlen + 1); + # we check related file in $projectroot + if ($check_forks and $subdir =~ m#/.#) { + $File::Find::prune = 1; + } elsif (check_export_ok("$projectroot/$filter/$subdir")) { + push @list, { path => ($filter ? "$filter/" : '') . $subdir }; + $File::Find::prune = 1; + } + }, + }, "$dir"); + + } 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; + while (my $line = <$fd>) { + chomp $line; + my ($path, $owner) = split ' ', $line; + $path = unescape($path); + $owner = unescape($owner); + if (!defined $path) { + next; + } + if ($filter ne '') { + # looking for forks; + my $pfx = substr($path, 0, length($filter)); + if ($pfx ne $filter) { + next; + } + my $sfx = substr($path, length($filter)); + if ($sfx !~ /^\/.*\.git$/) { + next; + } + } + if (check_export_ok("$projectroot/$path")) { + my $pr = { + path => $path, + owner => to_utf8($owner), + }; + push @list, $pr + } + } + close $fd; + } + @list = sort {$a->{'path'} cmp $b->{'path'}} @list; + return @list; +} + +sub git_get_project_owner { + my $project = shift; + my $owner; + + return undef unless $project; + + # read from file (url-encoded): + # 'git%2Fgit.git Linus+Torvalds' + # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin' + # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman' + 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 = to_utf8($ow); + last; + } + } + close $fd; + } + if (!defined $owner) { + $owner = get_file_owner("$projectroot/$project"); + } + + return $owner; +} + +sub git_get_last_activity { + my ($path) = @_; + my $fd; + + $git_dir = "$projectroot/$path"; + open($fd, "-|", git_cmd(), 'for-each-ref', + '--format=%(committer)', + '--sort=-committerdate', + '--count=1', + 'refs/heads') or return; + my $most_recent = <$fd>; + close $fd or return; + if ($most_recent =~ / (\d+) [-+][01]\d\d\d$/) { + my $timestamp = $1; + my $age = time - $timestamp; + return ($age, age_string($age)); + } +} + +sub git_get_references { + my $type = shift || ""; + my %refs; + # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11 + # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{} + open my $fd, "-|", git_cmd(), "show-ref", "--dereference", + ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type + or return; + + while (my $line = <$fd>) { + chomp $line; + if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) { + if (defined $refs{$1}) { + push @{$refs{$1}}, $2; + } else { + $refs{$1} = [ $2 ]; + } + } + } + close $fd or return; + return \%refs; +} + +sub git_get_rev_name_tags { + my $hash = shift || return undef; + + open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash + or return; + my $name_rev = <$fd>; + close $fd; + + if ($name_rev =~ m|^$hash tags/(.*)$|) { + return $1; + } else { + # catches also '$hash undefined' output + return undef; + } +} + +## ---------------------------------------------------------------------- +## parse to hash functions + +sub parse_date { + 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; + $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ", + 1900+$year, $mon, $mday, $hour ,$min, $sec; + + $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; + $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s", + 1900+$year, $mon+1, $mday, + $hour, $min, $sec, $tz); + return %date; +} + +sub parse_tag { + my $tag_id = shift; + my %tag; + my @comment; + + open my $fd, "-|", git_cmd(), "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 parse_commit_text { + my ($commit_text, $withparents) = @_; + my @commit_lines = split '\n', $commit_text; + my %co; + + pop @commit_lines; # Remove '\0' + + my $header = shift @commit_lines; + if (!($header =~ m/^[0-9a-fA-F]{40}/)) { + return; + } + ($co{'id'}, my @parents) = split ' ', $header; + while (my $line = shift @commit_lines) { + last if $line eq "\n"; + if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) { + $co{'tree'} = $1; + } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) { + push @parents, $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; + $co{'author_email'} = $2; + } 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'}; + if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) { + $co{'committer_name'} = $1; + $co{'committer_email'} = $2; + } else { + $co{'committer_name'} = $co{'committer'}; + } + } + } + if (!defined $co{'tree'}) { + return; + }; + $co{'parents'} = \@parents; + $co{'parent'} = $parents[0]; + + 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; + } + } + if ($co{'title'} eq "") { + $co{'title'} = $co{'title_short'} = '(no commit message)'; + } + # 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 parse_commit { + my ($commit_id) = @_; + my %co; + + local $/ = "\0"; + + open my $fd, "-|", git_cmd(), "rev-list", + "--parents", + "--header", + "--max-count=1", + $commit_id, + "--", + or die_error(undef, "Open git-rev-list failed"); + %co = parse_commit_text(<$fd>, 1); + close $fd; + + return %co; +} + +sub parse_commits { + my ($commit_id, $maxcount, $skip, $arg, $filename) = @_; + my @cos; + + $maxcount ||= 1; + $skip ||= 0; + + local $/ = "\0"; + + open my $fd, "-|", git_cmd(), "rev-list", + "--header", + ($arg ? ($arg) : ()), + ("--max-count=" . $maxcount), + ("--skip=" . $skip), + $commit_id, + "--", + ($filename ? ($filename) : ()) + or die_error(undef, "Open git-rev-list failed"); + while (my $line = <$fd>) { + my %co = parse_commit_text($line); + push @cos, \%co; + } + close $fd; + + return wantarray ? @cos : \@cos; +} + +# parse ref from ref_file, given by ref_id, with given type +sub parse_ref { + my $ref_file = shift; + my $ref_id = shift; + my $type = shift || git_get_type($ref_id); + my %ref_item; + + $ref_item{'type'} = $type; + $ref_item{'id'} = $ref_id; + $ref_item{'epoch'} = 0; + $ref_item{'age'} = "unknown"; + if ($type eq "tag") { + my %tag = parse_tag($ref_id); + $ref_item{'comment'} = $tag{'comment'}; + if ($tag{'type'} eq "commit") { + my %co = parse_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"){ + my %co = parse_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'}; + } else { + $ref_item{'reftype'} = $type; + $ref_item{'name'} = $ref_file; + $ref_item{'refid'} = $ref_id; + } + + return %ref_item; +} + +# parse line of git-diff-tree "raw" output +sub parse_difftree_raw_line { + my $line = shift; + my %res; + + # ':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(.*)$/) { + $res{'from_mode'} = $1; + $res{'to_mode'} = $2; + $res{'from_id'} = $3; + $res{'to_id'} = $4; + $res{'status'} = $5; + $res{'similarity'} = $6; + if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied + ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7); + } else { + $res{'file'} = unquote($7); + } + } + # 'c512b523472485aef4fff9e57b229d9d243c967f' + elsif ($line =~ m/^([0-9a-fA-F]{40})$/) { + $res{'commit'} = $1; + } + + return wantarray ? %res : \%res; +} + +# parse line of git-ls-tree output +sub parse_ls_tree_line ($;%) { + my $line = shift; + my %opts = @_; + my %res; + + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s; + + $res{'mode'} = $1; + $res{'type'} = $2; + $res{'hash'} = $3; + if ($opts{'-z'}) { + $res{'name'} = $4; + } else { + $res{'name'} = unquote($4); + } + + return wantarray ? %res : \%res; +} + +## ...................................................................... +## parse to array of hashes functions + +sub git_get_heads_list { + my $limit = shift; + my @headslist; + + open my $fd, '-|', git_cmd(), 'for-each-ref', + ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate', + '--format=%(objectname) %(refname) %(subject)%00%(committer)', + 'refs/heads' + or return; + while (my $line = <$fd>) { + my %ref_item; + + chomp $line; + my ($refinfo, $committerinfo) = split(/\0/, $line); + my ($hash, $name, $title) = split(' ', $refinfo, 3); + my ($committer, $epoch, $tz) = + ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/); + $name =~ s!^refs/heads/!!; + + $ref_item{'name'} = $name; + $ref_item{'id'} = $hash; + $ref_item{'title'} = $title || '(no commit message)'; + $ref_item{'epoch'} = $epoch; + if ($epoch) { + $ref_item{'age'} = age_string(time - $ref_item{'epoch'}); + } else { + $ref_item{'age'} = "unknown"; + } + + push @headslist, \%ref_item; + } + close $fd; + + return wantarray ? @headslist : \@headslist; +} + +sub git_get_tags_list { + my $limit = shift; + my @tagslist; + + open my $fd, '-|', git_cmd(), 'for-each-ref', + ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate', + '--format=%(objectname) %(objecttype) %(refname) '. + '%(*objectname) %(*objecttype) %(subject)%00%(creator)', + 'refs/tags' + or return; + while (my $line = <$fd>) { + my %ref_item; + + chomp $line; + my ($refinfo, $creatorinfo) = split(/\0/, $line); + my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6); + my ($creator, $epoch, $tz) = + ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/); + $name =~ s!^refs/tags/!!; + + $ref_item{'type'} = $type; + $ref_item{'id'} = $id; + $ref_item{'name'} = $name; + if ($type eq "tag") { + $ref_item{'subject'} = $title; + $ref_item{'reftype'} = $reftype; + $ref_item{'refid'} = $refid; + } else { + $ref_item{'reftype'} = $type; + $ref_item{'refid'} = $id; + } + + if ($type eq "tag" || $type eq "commit") { + $ref_item{'epoch'} = $epoch; + if ($epoch) { + $ref_item{'age'} = age_string(time - $ref_item{'epoch'}); + } else { + $ref_item{'age'} = "unknown"; + } + } + + push @tagslist, \%ref_item; + } + close $fd; + + return wantarray ? @tagslist : \@tagslist; +} + +## ---------------------------------------------------------------------- +## filesystem-related functions + +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 to_utf8($owner); +} + +## ...................................................................... +## mimetype related functions + +sub mimetype_guess_file { + my $filename = shift; + my $mimemap = shift; + -r $mimemap or return undef; + + my %mimemap; + open(MIME, $mimemap) or return undef; + while (<MIME>) { + next if m/^#/; # skip comments + my ($mime, $exts) = split(/\t+/); + if (defined $exts) { + my @exts = split(/\s+/, $exts); + foreach my $ext (@exts) { + $mimemap{$ext} = $mime; + } + } + } + close(MIME); + + $filename =~ /\.([^.]*)$/; + return $mimemap{$1}; +} + +sub mimetype_guess { + my $filename = shift; + my $mime; + $filename =~ /\./ or return undef; + + if ($mimetypes_file) { + my $file = $mimetypes_file; + if ($file !~ m!^/!) { # if it is relative path + # it is relative to project + $file = "$projectroot/$project/$file"; + } + $mime = mimetype_guess_file($filename, $file); + } + $mime ||= mimetype_guess_file($filename, '/etc/mime.types'); + return $mime; +} + +sub blob_mimetype { + my $fd = shift; + my $filename = shift; + + if ($filename) { + my $mime = mimetype_guess($filename); + $mime and return $mime; + } + + # just in case + return $default_blob_plain_mimetype unless $fd; + + if (-T $fd) { + return 'text/plain' . + ($default_text_plain_charset ? '; charset='.$default_text_plain_charset : ''); + } elsif (! $filename) { + return 'application/octet-stream'; + } elsif ($filename =~ m/\.png$/i) { + return 'image/png'; + } elsif ($filename =~ m/\.gif$/i) { + return 'image/gif'; + } elsif ($filename =~ m/\.jpe?g$/i) { + return 'image/jpeg'; + } else { + return 'application/octet-stream'; + } +} + +## ====================================================================== +## functions printing HTML: header, footer, error page + +sub git_header_html { + my $status = shift || "200 OK"; + my $expires = shift; + + my $title = "$site_name"; + if (defined $project) { + $title .= " - " . to_utf8($project); + if (defined $action) { + $title .= "/$action"; + if (defined $file_name) { + $title .= " - " . esc_path($file_name); + if ($action eq "tree" && $file_name !~ m|/$|) { + $title .= "/"; + } + } + } + } + my $content_type; + # require explicit support from the UA if we are to send the page as + # 'application/xhtml+xml', otherwise send it as plain old 'text/html'. + # we have to do this because MSIE sometimes globs '*/*', pretending to + # support xhtml+xml but choking when it gets what it asked for. + if (defined $cgi->http('HTTP_ACCEPT') && + $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ && + $cgi->Accept('application/xhtml+xml') != 0) { + $content_type = 'application/xhtml+xml'; + } else { + $content_type = 'text/html'; + } + print $cgi->header(-type=>$content_type, -charset => 'utf-8', + -status=> $status, -expires => $expires); + my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : ''; + 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 version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke --> +<!-- git core binaries version $git_version --> +<head> +<meta http-equiv="content-type" content="$content_type; charset=utf-8"/> +<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/> +<meta name="robots" content="index, nofollow"/> +<title>$title</title> +EOF +# print out each stylesheet that exist + if (defined $stylesheet) { +#provides backwards capability for those people who define style sheet in a config file + print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n"; + } else { + foreach my $stylesheet (@stylesheets) { + next unless $stylesheet; + print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n"; + } + } + if (defined $project) { + printf('<link rel="alternate" title="%s log RSS feed" '. + 'href="%s" type="application/rss+xml" />'."\n", + esc_param($project), href(action=>"rss")); + printf('<link rel="alternate" title="%s log Atom feed" '. + 'href="%s" type="application/atom+xml" />'."\n", + esc_param($project), href(action=>"atom")); + } else { + printf('<link rel="alternate" title="%s projects list" '. + 'href="%s" type="text/plain; charset=utf-8"/>'."\n", + $site_name, href(project=>undef, action=>"project_index")); + printf('<link rel="alternate" title="%s projects feeds" '. + 'href="%s" type="text/x-opml"/>'."\n", + $site_name, href(project=>undef, action=>"opml")); + } + if (defined $favicon) { + print qq(<link rel="shortcut icon" href="$favicon" type="image/png"/>\n); + } + + print "</head>\n" . + "<body>\n"; + + if (-f $site_header) { + open (my $fd, $site_header); + print <$fd>; + close $fd; + } + + print "<div class=\"page_header\">\n" . + $cgi->a({-href => esc_url($logo_url), + -title => $logo_label}, + qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>)); + print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / "; + if (defined $project) { + print $cgi->a({-href => href(action=>"summary")}, esc_html($project)); + if (defined $action) { + print " / $action"; + } + print "\n"; + } + my ($have_search) = gitweb_check_feature('search'); + if ((defined $project) && ($have_search)) { + if (!defined $searchtext) { + $searchtext = ""; + } + my $search_hash; + if (defined $hash_base) { + $search_hash = $hash_base; + } elsif (defined $hash) { + $search_hash = $hash; + } else { + $search_hash = "HEAD"; + } + $cgi->param("a", "search"); + $cgi->param("h", $search_hash); + $cgi->param("p", $project); + 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->popup_menu(-name => 'st', -default => 'commit', + -values => ['commit', 'author', 'committer', 'pickaxe']) . + $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) . + " search:\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_get_project_description($project); + if (defined $descr) { + print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n"; + } + print $cgi->a({-href => href(action=>"rss"), + -class => "rss_logo"}, "RSS") . " "; + print $cgi->a({-href => href(action=>"atom"), + -class => "rss_logo"}, "Atom") . "\n"; + } else { + print $cgi->a({-href => href(project=>undef, action=>"opml"), + -class => "rss_logo"}, "OPML") . " "; + print $cgi->a({-href => href(project=>undef, action=>"project_index"), + -class => "rss_logo"}, "TXT") . "\n"; + } + print "</div>\n" ; + + if (-f $site_footer) { + open (my $fd, $site_footer); + print <$fd>; + close $fd; + } + + print "</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 <<EOF; +<div class="page_body"> +<br /><br /> +$status - $error +<br /> +</div> +EOF + git_footer_html(); + exit; +} + +## ---------------------------------------------------------------------- +## functions printing or outputting HTML: navigation + +sub git_print_page_nav { + my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_; + $extra = '' if !defined $extra; # pager or formats + + my @navs = qw(summary shortlog log commit commitdiff tree); + if ($suppress) { + @navs = grep { $_ ne $suppress } @navs; + } + + my %arg = map { $_ => {action=>$_} } @navs; + if (defined $head) { + for (qw(commit commitdiff)) { + $arg{$_}{hash} = $head; + } + if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) { + for (qw(shortlog log)) { + $arg{$_}{hash} = $head; + } + } + } + $arg{tree}{hash} = $treehead if defined $treehead; + $arg{tree}{hash_base} = $treebase if defined $treebase; + + print "<div class=\"page_nav\">\n" . + (join " | ", + map { $_ eq $current ? + $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_") + } @navs); + print "<br/>\n$extra<br/>\n" . + "</div>\n"; +} + +sub format_paging_nav { + my ($action, $hash, $head, $page, $nrevs) = @_; + my $paging_nav; + + + if ($hash ne $head || $page) { + $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD"); + } else { + $paging_nav .= "HEAD"; + } + + if ($page > 0) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1), + -accesskey => "p", -title => "Alt-p"}, "prev"); + } else { + $paging_nav .= " ⋅ prev"; + } + + if ($nrevs >= (100 * ($page+1)-1)) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } else { + $paging_nav .= " ⋅ next"; + } + + return $paging_nav; +} + +## ...................................................................... +## functions printing or outputting HTML: div + +sub git_print_header_div { + my ($action, $title, $hash, $hash_base) = @_; + my %args = (); + + $args{action} = $action; + $args{hash} = $hash if $hash; + $args{hash_base} = $hash_base if $hash_base; + + print "<div class=\"header\">\n" . + $cgi->a({-href => href(%args), -class => "title"}, + $title ? $title : $action) . + "\n</div>\n"; +} + +#sub git_print_authorship (\%) { +sub git_print_authorship { + my $co = shift; + + my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'}); + print "<div class=\"author_date\">" . + esc_html($co->{'author_name'}) . + " [$ad{'rfc2822'}"; + if ($ad{'hour_local'} < 6) { + printf(" (<span class=\"atnight\">%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 "]</div>\n"; +} + +sub git_print_page_path { + my $name = shift; + my $type = shift; + my $hb = shift; + + + print "<div class=\"page_path\">"; + print $cgi->a({-href => href(action=>"tree", hash_base=>$hb), + -title => 'tree root'}, to_utf8("[$project]")); + print " / "; + if (defined $name) { + my @dirname = split '/', $name; + my $basename = pop @dirname; + my $fullname = ''; + + foreach my $dir (@dirname) { + $fullname .= ($fullname ? '/' : '') . $dir; + print $cgi->a({-href => href(action=>"tree", file_name=>$fullname, + hash_base=>$hb), + -title => esc_html($fullname)}, esc_path($dir)); + print " / "; + } + if (defined $type && $type eq 'blob') { + print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name, + hash_base=>$hb), + -title => esc_html($name)}, esc_path($basename)); + } elsif (defined $type && $type eq 'tree') { + print $cgi->a({-href => href(action=>"tree", file_name=>$file_name, + hash_base=>$hb), + -title => esc_html($name)}, esc_path($basename)); + print " / "; + } else { + print esc_path($basename); + } + } + print "<br/></div>\n"; +} + +# sub git_print_log (\@;%) { +sub git_print_log ($;%) { + my $log = shift; + my %opts = @_; + + if ($opts{'-remove_title'}) { + # remove title, i.e. first line of log + shift @$log; + } + # remove leading empty lines + while (defined $log->[0] && $log->[0] eq "") { + shift @$log; + } + + # print log + my $signoff = 0; + my $empty = 0; + foreach my $line (@$log) { + if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) { + $signoff = 1; + $empty = 0; + if (! $opts{'-remove_signoff'}) { + print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n"; + next; + } else { + # remove signoff lines + next; + } + } else { + $signoff = 0; + } + + # print only one empty line + # do not print empty line after signoff + if ($line eq "") { + next if ($empty || $signoff); + $empty = 1; + } else { + $empty = 0; + } + + print format_log_line_html($line) . "<br/>\n"; + } + + if ($opts{'-final_empty_line'}) { + # end with single empty line + print "<br/>\n" unless $empty; + } +} + +# return link target (what link points to) +sub git_get_link_target { + my $hash = shift; + my $link_target; + + # read link + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash + or return; + { + local $/; + $link_target = <$fd>; + } + close $fd + or return; + + return $link_target; +} + +# given link target, and the directory (basedir) the link is in, +# return target of link relative to top directory (top tree); +# return undef if it is not possible (including absolute links). +sub normalize_link_target { + my ($link_target, $basedir, $hash_base) = @_; + + # we can normalize symlink target only if $hash_base is provided + return unless $hash_base; + + # absolute symlinks (beginning with '/') cannot be normalized + return if (substr($link_target, 0, 1) eq '/'); + + # normalize link target to path from top (root) tree (dir) + my $path; + if ($basedir) { + $path = $basedir . '/' . $link_target; + } else { + # we are in top (root) tree (dir) + $path = $link_target; + } + + # remove //, /./, and /../ + my @path_parts; + foreach my $part (split('/', $path)) { + # discard '.' and '' + next if (!$part || $part eq '.'); + # handle '..' + if ($part eq '..') { + if (@path_parts) { + pop @path_parts; + } else { + # link leads outside repository (outside top dir) + return; + } + } else { + push @path_parts, $part; + } + } + $path = join('/', @path_parts); + + return $path; +} + +# print tree entry (row of git_tree), but without encompassing <tr> element +sub git_print_tree_entry { + my ($t, $basedir, $hash_base, $have_blame) = @_; + + my %base_key = (); + $base_key{'hash_base'} = $hash_base if defined $hash_base; + + # The format of a table row is: mode list link. Where mode is + # the mode of the entry, list is the name of the entry, an href, + # and link is the action links of the entry. + + print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n"; + if ($t->{'type'} eq "blob") { + print "<td class=\"list\">" . + $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key), + -class => "list"}, esc_path($t->{'name'})); + if (S_ISLNK(oct $t->{'mode'})) { + my $link_target = git_get_link_target($t->{'hash'}); + if ($link_target) { + my $norm_target = normalize_link_target($link_target, $basedir, $hash_base); + if (defined $norm_target) { + print " -> " . + $cgi->a({-href => href(action=>"object", hash_base=>$hash_base, + file_name=>$norm_target), + -title => $norm_target}, esc_path($link_target)); + } else { + print " -> " . esc_path($link_target); + } + } + } + print "</td>\n"; + print "<td class=\"link\">"; + print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + "blob"); + if ($have_blame) { + print " | " . + $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + "blame"); + } + if (defined $hash_base) { + print " | " . + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")}, + "history"); + } + print " | " . + $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base, + file_name=>"$basedir$t->{'name'}")}, + "raw"); + print "</td>\n"; + + } elsif ($t->{'type'} eq "tree") { + print "<td class=\"list\">"; + print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + esc_path($t->{'name'})); + print "</td>\n"; + print "<td class=\"link\">"; + print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'}, + file_name=>"$basedir$t->{'name'}", %base_key)}, + "tree"); + if (defined $hash_base) { + print " | " . + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + file_name=>"$basedir$t->{'name'}")}, + "history"); + } + print "</td>\n"; + } +} + +## ...................................................................... +## functions printing large fragments of HTML + +sub git_difftree_body { + my ($difftree, $hash, $parent) = @_; + my ($have_blame) = gitweb_check_feature('blame'); + print "<div class=\"list_head\">\n"; + if ($#{$difftree} > 10) { + print(($#{$difftree} + 1) . " files changed:\n"); + } + print "</div>\n"; + + print "<table class=\"diff_tree\">\n"; + my $alternate = 1; + my $patchno = 0; + foreach my $line (@{$difftree}) { + my %diff = parse_difftree_raw_line($line); + + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + + my ($to_mode_oct, $to_mode_str, $to_file_type); + my ($from_mode_oct, $from_mode_str, $from_file_type); + if ($diff{'to_mode'} ne ('0' x 6)) { + $to_mode_oct = oct $diff{'to_mode'}; + if (S_ISREG($to_mode_oct)) { # only for regular file + $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits + } + $to_file_type = file_type($diff{'to_mode'}); + } + if ($diff{'from_mode'} ne ('0' x 6)) { + $from_mode_oct = oct $diff{'from_mode'}; + if (S_ISREG($to_mode_oct)) { # only for regular file + $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits + } + $from_file_type = file_type($diff{'from_mode'}); + } + + if ($diff{'status'} eq "A") { # created + my $mode_chng = "<span class=\"file_status new\">[new $to_file_type"; + $mode_chng .= " with mode: $to_mode_str" if $to_mode_str; + $mode_chng .= "]</span>"; + print "<td>"; + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + hash_base=>$hash, file_name=>$diff{'file'}), + -class => "list"}, esc_path($diff{'file'})); + print "</td>\n"; + print "<td>$mode_chng</td>\n"; + print "<td class=\"link\">"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch"); + print " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + hash_base=>$hash, file_name=>$diff{'file'})}, + "blob"); + print "</td>\n"; + + } elsif ($diff{'status'} eq "D") { # deleted + my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>"; + print "<td>"; + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'}, + hash_base=>$parent, file_name=>$diff{'file'}), + -class => "list"}, esc_path($diff{'file'})); + print "</td>\n"; + print "<td>$mode_chng</td>\n"; + print "<td class=\"link\">"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch"); + print " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'}, + hash_base=>$parent, file_name=>$diff{'file'})}, + "blob") . " | "; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash_base=>$parent, + file_name=>$diff{'file'})}, + "blame") . " | "; + } + print $cgi->a({-href => href(action=>"history", hash_base=>$parent, + file_name=>$diff{'file'})}, + "history"); + print "</td>\n"; + + } elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed + my $mode_chnge = ""; + if ($diff{'from_mode'} != $diff{'to_mode'}) { + $mode_chnge = "<span class=\"file_status mode_chnge\">[changed"; + if ($from_file_type ne $to_file_type) { + $mode_chnge .= " from $from_file_type to $to_file_type"; + } + if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) { + if ($from_mode_str && $to_mode_str) { + $mode_chnge .= " mode: $from_mode_str->$to_mode_str"; + } elsif ($to_mode_str) { + $mode_chnge .= " mode: $to_mode_str"; + } + } + $mode_chnge .= "]</span>\n"; + } + print "<td>"; + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + hash_base=>$hash, file_name=>$diff{'file'}), + -class => "list"}, esc_path($diff{'file'})); + print "</td>\n"; + print "<td>$mode_chnge</td>\n"; + print "<td class=\"link\">"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch") . + " | "; + } elsif ($diff{'to_id'} ne $diff{'from_id'}) { + # "commit" view and modified file (not onlu mode changed) + print $cgi->a({-href => href(action=>"blobdiff", + hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'}, + hash_base=>$hash, hash_parent_base=>$parent, + file_name=>$diff{'file'})}, + "diff") . + " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + hash_base=>$hash, file_name=>$diff{'file'})}, + "blob") . " | "; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash_base=>$hash, + file_name=>$diff{'file'})}, + "blame") . " | "; + } + print $cgi->a({-href => href(action=>"history", hash_base=>$hash, + file_name=>$diff{'file'})}, + "history"); + print "</td>\n"; + + } elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied + my %status_name = ('R' => 'moved', 'C' => 'copied'); + my $nstatus = $status_name{$diff{'status'}}; + my $mode_chng = ""; + if ($diff{'from_mode'} != $diff{'to_mode'}) { + # mode also for directories, so we cannot use $to_mode_str + $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777); + } + print "<td>" . + $cgi->a({-href => href(action=>"blob", hash_base=>$hash, + hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}), + -class => "list"}, esc_path($diff{'to_file'})) . "</td>\n" . + "<td><span class=\"file_status $nstatus\">[$nstatus from " . + $cgi->a({-href => href(action=>"blob", hash_base=>$parent, + hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}), + -class => "list"}, esc_path($diff{'from_file'})) . + " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]</span></td>\n" . + "<td class=\"link\">"; + if ($action eq 'commitdiff') { + # link to patch + $patchno++; + print $cgi->a({-href => "#patch$patchno"}, "patch") . + " | "; + } elsif ($diff{'to_id'} ne $diff{'from_id'}) { + # "commit" view and modified file (not only pure rename or copy) + print $cgi->a({-href => href(action=>"blobdiff", + hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'}, + hash_base=>$hash, hash_parent_base=>$parent, + file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})}, + "diff") . + " | "; + } + print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'}, + hash_base=>$parent, file_name=>$diff{'to_file'})}, + "blob") . " | "; + if ($have_blame) { + print $cgi->a({-href => href(action=>"blame", hash_base=>$hash, + file_name=>$diff{'to_file'})}, + "blame") . " | "; + } + print $cgi->a({-href => href(action=>"history", hash_base=>$hash, + file_name=>$diff{'to_file'})}, + "history"); + print "</td>\n"; + + } # we should not encounter Unmerged (U) or Unknown (X) status + print "</tr>\n"; + } + print "</table>\n"; +} + +sub git_patchset_body { + my ($fd, $difftree, $hash, $hash_parent) = @_; + + my $patch_idx = 0; + my $patch_line; + my $diffinfo; + my (%from, %to); + + print "<div class=\"patchset\">\n"; + + # skip to first patch + while ($patch_line = <$fd>) { + chomp $patch_line; + + last if ($patch_line =~ m/^diff /); + } + + PATCH: + while ($patch_line) { + my @diff_header; + my ($from_id, $to_id); + + # git diff header + #assert($patch_line =~ m/^diff /) if DEBUG; + #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed + push @diff_header, $patch_line; + + # extended diff header + EXTENDED_HEADER: + while ($patch_line = <$fd>) { + chomp $patch_line; + + last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /); + + if ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) { + $from_id = $1; + $to_id = $2; + } + + push @diff_header, $patch_line; + } + my $last_patch_line = $patch_line; + + # check if current patch belong to current raw line + # and parse raw git-diff line if needed + if (defined $diffinfo && + $diffinfo->{'from_id'} eq $from_id && + $diffinfo->{'to_id'} eq $to_id) { + # this is split patch + print "<div class=\"patch cont\">\n"; + } else { + # advance raw git-diff output if needed + $patch_idx++ if defined $diffinfo; + + # read and prepare patch information + if (ref($difftree->[$patch_idx]) eq "HASH") { + # pre-parsed (or generated by hand) + $diffinfo = $difftree->[$patch_idx]; + } else { + $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]); + } + $from{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'}; + $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'}; + if ($diffinfo->{'status'} ne "A") { # not new (added) file + $from{'href'} = href(action=>"blob", hash_base=>$hash_parent, + hash=>$diffinfo->{'from_id'}, + file_name=>$from{'file'}); + } else { + delete $from{'href'}; + } + if ($diffinfo->{'status'} ne "D") { # not deleted file + $to{'href'} = href(action=>"blob", hash_base=>$hash, + hash=>$diffinfo->{'to_id'}, + file_name=>$to{'file'}); + } else { + delete $to{'href'}; + } + # this is first patch for raw difftree line with $patch_idx index + # we index @$difftree array from 0, but number patches from 1 + print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n"; + } + + # print "git diff" header + $patch_line = shift @diff_header; + $patch_line =~ s!^(diff (.*?) )"?a/.*$!$1!; + if ($from{'href'}) { + $patch_line .= $cgi->a({-href => $from{'href'}, -class => "path"}, + 'a/' . esc_path($from{'file'})); + } else { # file was added + $patch_line .= 'a/' . esc_path($from{'file'}); + } + $patch_line .= ' '; + if ($to{'href'}) { + $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"}, + 'b/' . esc_path($to{'file'})); + } else { # file was deleted + $patch_line .= 'b/' . esc_path($to{'file'}); + } + print "<div class=\"diff header\">$patch_line</div>\n"; + + # print extended diff header + print "<div class=\"diff extended_header\">\n" if (@diff_header > 0); + EXTENDED_HEADER: + foreach $patch_line (@diff_header) { + # match <path> + if ($patch_line =~ s!^((copy|rename) from ).*$!$1! && $from{'href'}) { + $patch_line .= $cgi->a({-href=>$from{'href'}, -class=>"path"}, + esc_path($from{'file'})); + } + if ($patch_line =~ s!^((copy|rename) to ).*$!$1! && $to{'href'}) { + $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"path"}, + esc_path($to{'file'})); + } + # match <mode> + if ($patch_line =~ m/\s(\d{6})$/) { + $patch_line .= '<span class="info"> (' . + file_type_long($1) . + ')</span>'; + } + # match <hash> + if ($patch_line =~ m/^index/) { + my ($from_link, $to_link); + if ($from{'href'}) { + $from_link = $cgi->a({-href=>$from{'href'}, -class=>"hash"}, + substr($diffinfo->{'from_id'},0,7)); + } else { + $from_link = '0' x 7; + } + if ($to{'href'}) { + $to_link = $cgi->a({-href=>$to{'href'}, -class=>"hash"}, + substr($diffinfo->{'to_id'},0,7)); + } else { + $to_link = '0' x 7; + } + #affirm { + # my ($from_hash, $to_hash) = + # ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/); + # my ($from_id, $to_id) = + # ($diffinfo->{'from_id'}, $diffinfo->{'to_id'}); + # ($from_hash eq $from_id) && ($to_hash eq $to_id); + #} if DEBUG; + my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'}); + $patch_line =~ s!$from_id\.\.$to_id!$from_link..$to_link!; + } + print $patch_line . "<br/>\n"; + } + print "</div>\n" if (@diff_header > 0); # class="diff extended_header" + + # from-file/to-file diff header + $patch_line = $last_patch_line; + if (! $patch_line) { + print "</div>\n"; # class="patch" + last PATCH; + } + next PATCH if ($patch_line =~ m/^diff /); + #assert($patch_line =~ m/^---/) if DEBUG; + if ($from{'href'} && $patch_line =~ m!^--- "?a/!) { + $patch_line = '--- a/' . + $cgi->a({-href=>$from{'href'}, -class=>"path"}, + esc_path($from{'file'})); + } + print "<div class=\"diff from_file\">$patch_line</div>\n"; + + $patch_line = <$fd>; + chomp $patch_line; + + #assert($patch_line =~ m/^+++/) if DEBUG; + if ($to{'href'} && $patch_line =~ m!^\+\+\+ "?b/!) { + $patch_line = '+++ b/' . + $cgi->a({-href=>$to{'href'}, -class=>"path"}, + esc_path($to{'file'})); + } + print "<div class=\"diff to_file\">$patch_line</div>\n"; + + # the patch itself + LINE: + while ($patch_line = <$fd>) { + chomp $patch_line; + + next PATCH if ($patch_line =~ m/^diff /); + + print format_diff_line($patch_line, \%from, \%to); + } + + } continue { + print "</div>\n"; # class="patch" + } + + print "</div>\n"; # class="patchset" +} + +# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + +sub git_project_list_body { + my ($projlist, $order, $from, $to, $extra, $no_header) = @_; + + my ($check_forks) = gitweb_check_feature('forks'); + + my @projects; + foreach my $pr (@$projlist) { + my (@aa) = git_get_last_activity($pr->{'path'}); + unless (@aa) { + next; + } + ($pr->{'age'}, $pr->{'age_string'}) = @aa; + if (!defined $pr->{'descr'}) { + my $descr = git_get_project_description($pr->{'path'}) || ""; + $pr->{'descr_long'} = to_utf8($descr); + $pr->{'descr'} = chop_str($descr, 25, 5); + } + if (!defined $pr->{'owner'}) { + $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || ""; + } + if ($check_forks) { + my $pname = $pr->{'path'}; + if (($pname =~ s/\.git$//) && + ($pname !~ /\/$/) && + (-d "$projectroot/$pname")) { + $pr->{'forks'} = "-d $projectroot/$pname"; + } + else { + $pr->{'forks'} = 0; + } + } + push @projects, $pr; + } + + $order ||= "project"; + $from = 0 unless defined $from; + $to = $#projects if (!defined $to || $#projects < $to); + + print "<table class=\"project_list\">\n"; + unless ($no_header) { + print "<tr>\n"; + if ($check_forks) { + print "<th></th>\n"; + } + if ($order eq "project") { + @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects; + print "<th>Project</th>\n"; + } else { + print "<th>" . + $cgi->a({-href => href(project=>undef, order=>'project'), + -class => "header"}, "Project") . + "</th>\n"; + } + if ($order eq "descr") { + @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects; + print "<th>Description</th>\n"; + } else { + print "<th>" . + $cgi->a({-href => href(project=>undef, order=>'descr'), + -class => "header"}, "Description") . + "</th>\n"; + } + if ($order eq "owner") { + @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects; + print "<th>Owner</th>\n"; + } else { + print "<th>" . + $cgi->a({-href => href(project=>undef, order=>'owner'), + -class => "header"}, "Owner") . + "</th>\n"; + } + if ($order eq "age") { + @projects = sort {$a->{'age'} <=> $b->{'age'}} @projects; + print "<th>Last Change</th>\n"; + } else { + print "<th>" . + $cgi->a({-href => href(project=>undef, order=>'age'), + -class => "header"}, "Last Change") . + "</th>\n"; + } + print "<th></th>\n" . + "</tr>\n"; + } + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my $pr = $projects[$i]; + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + if ($check_forks) { + print "<td>"; + if ($pr->{'forks'}) { + print "<!-- $pr->{'forks'} -->\n"; + print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+"); + } + print "</td>\n"; + } + print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), + -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" . + "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"), + -class => "list", -title => $pr->{'descr_long'}}, + esc_html($pr->{'descr'})) . "</td>\n" . + "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n"; + print "<td class=\"". age_class($pr->{'age'}) . "\">" . + $pr->{'age_string'} . "</td>\n" . + "<td class=\"link\">" . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " . + $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") . + ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') . + "</td>\n" . + "</tr>\n"; + } + if (defined $extra) { + print "<tr>\n"; + if ($check_forks) { + print "<td></td>\n"; + } + print "<td colspan=\"5\">$extra</td>\n" . + "</tr>\n"; + } + print "</table>\n"; +} + +sub git_shortlog_body { + # uses global variable $project + my ($commitlist, $from, $to, $refs, $extra) = @_; + + my $have_snapshot = gitweb_have_snapshot(); + + $from = 0 unless defined $from; + $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); + + print "<table class=\"shortlog\" cellspacing=\"0\">\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; + my $commit = $co{'id'}; + my $ref = format_ref_marker($refs, $commit); + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" . + 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>"; + print format_subject_html($co{'title'}, $co{'title_short'}, + href(action=>"commit", hash=>$commit), $ref); + print "</td>\n" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " . + $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree"); + if ($have_snapshot) { + print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot"); + } + print "</td>\n" . + "</tr>\n"; + } + if (defined $extra) { + print "<tr>\n" . + "<td colspan=\"4\">$extra</td>\n" . + "</tr>\n"; + } + print "</table>\n"; +} + +sub git_history_body { + # Warning: assumes constant type (blob or tree) during history + my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_; + + $from = 0 unless defined $from; + $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist}); + + print "<table class=\"history\" cellspacing=\"0\">\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; + if (!%co) { + next; + } + my $commit = $co{'id'}; + + my $ref = format_ref_marker($refs, $commit); + + 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" . + # shortlog uses chop_str($co{'author_name'}, 10) + "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" . + "<td>"; + # originally git_history used chop_str($co{'title'}, 50) + print format_subject_html($co{'title'}, $co{'title_short'}, + href(action=>"commit", hash=>$commit), $ref); + print "</td>\n" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff"); + + if ($ftype eq 'blob') { + my $blob_current = git_get_hash_by_path($hash_base, $file_name); + my $blob_parent = git_get_hash_by_path($commit, $file_name); + if (defined $blob_current && defined $blob_parent && + $blob_current ne $blob_parent) { + print " | " . + $cgi->a({-href => href(action=>"blobdiff", + hash=>$blob_current, hash_parent=>$blob_parent, + hash_base=>$hash_base, hash_parent_base=>$commit, + file_name=>$file_name)}, + "diff to current"); + } + } + print "</td>\n" . + "</tr>\n"; + } + if (defined $extra) { + print "<tr>\n" . + "<td colspan=\"4\">$extra</td>\n" . + "</tr>\n"; + } + print "</table>\n"; +} + +sub git_tags_body { + # uses global variable $project + my ($taglist, $from, $to, $extra) = @_; + $from = 0 unless defined $from; + $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to); + + print "<table class=\"tags\" cellspacing=\"0\">\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my $entry = $taglist->[$i]; + my %tag = %$entry; + my $comment = $tag{'subject'}; + my $comment_short; + if (defined $comment) { + $comment_short = chop_str($comment, 30, 5); + } + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + if (defined $tag{'age'}) { + print "<td><i>$tag{'age'}</i></td>\n"; + } else { + print "<td></td>\n"; + } + print "<td>" . + $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}), + -class => "list name"}, esc_html($tag{'name'})) . + "</td>\n" . + "<td>"; + if (defined $comment) { + print format_subject_html($comment, $comment_short, + href(action=>"tag", hash=>$tag{'id'})); + } + print "</td>\n" . + "<td class=\"selflink\">"; + if ($tag{'type'} eq "tag") { + print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag"); + } else { + print " "; + } + print "</td>\n" . + "<td class=\"link\">" . " | " . + $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'}); + if ($tag{'reftype'} eq "commit") { + print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . + " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log"); + } elsif ($tag{'reftype'} eq "blob") { + print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw"); + } + print "</td>\n" . + "</tr>"; + } + if (defined $extra) { + print "<tr>\n" . + "<td colspan=\"5\">$extra</td>\n" . + "</tr>\n"; + } + print "</table>\n"; +} + +sub git_heads_body { + # uses global variable $project + my ($headlist, $head, $from, $to, $extra) = @_; + $from = 0 unless defined $from; + $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to); + + print "<table class=\"heads\" cellspacing=\"0\">\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my $entry = $headlist->[$i]; + my %ref = %$entry; + my $curr = $ref{'id'} eq $head; + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + print "<td><i>$ref{'age'}</i></td>\n" . + ($curr ? "<td class=\"current_head\">" : "<td>") . + $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'}), + -class => "list name"},esc_html($ref{'name'})) . + "</td>\n" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'name'})}, "shortlog") . " | " . + $cgi->a({-href => href(action=>"log", hash=>$ref{'name'})}, "log") . " | " . + $cgi->a({-href => href(action=>"tree", hash=>$ref{'name'}, hash_base=>$ref{'name'})}, "tree") . + "</td>\n" . + "</tr>"; + } + if (defined $extra) { + print "<tr>\n" . + "<td colspan=\"3\">$extra</td>\n" . + "</tr>\n"; + } + print "</table>\n"; +} + +sub git_search_grep_body { + my ($commitlist, $from, $to, $extra) = @_; + $from = 0 unless defined $from; + $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to); + + print "<table class=\"grep\" cellspacing=\"0\">\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my %co = %{$commitlist->[$i]}; + if (!%co) { + next; + } + my $commit = $co{'id'}; + 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 => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"}, + esc_html(chop_str($co{'title'}, 50)) . "<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 class=\"match\">$match</span>$trail"; + print chop_str($text, 80, 5) . "<br/>\n"; + } + } + print "</td>\n" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); + print "</td>\n" . + "</tr>\n"; + } + if (defined $extra) { + print "<tr>\n" . + "<td colspan=\"3\">$extra</td>\n" . + "</tr>\n"; + } + print "</table>\n"; +} + +## ====================================================================== +## ====================================================================== +## actions + +sub git_project_list { + my $order = $cgi->param('o'); + if (defined $order && $order !~ m/project|descr|owner|age/) { + die_error(undef, "Unknown order parameter"); + } + + my @list = git_get_projects_list(); + if (!@list) { + die_error(undef, "No projects found"); + } + + 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"; + } + git_project_list_body(\@list, $order); + git_footer_html(); +} + +sub git_forks { + my $order = $cgi->param('o'); + if (defined $order && $order !~ m/project|descr|owner|age/) { + die_error(undef, "Unknown order parameter"); + } + + my @list = git_get_projects_list($project); + if (!@list) { + die_error(undef, "No forks found"); + } + + git_header_html(); + git_print_page_nav('',''); + git_print_header_div('summary', "$project forks"); + git_project_list_body(\@list, $order); + git_footer_html(); +} + +sub git_project_index { + my @projects = git_get_projects_list($project); + + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -content_disposition => 'inline; filename="index.aux"'); + + foreach my $pr (@projects) { + if (!exists $pr->{'owner'}) { + $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}"); + } + + my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'}); + # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' ' + $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg; + $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg; + $path =~ s/ /\+/g; + $owner =~ s/ /\+/g; + + print "$path $owner\n"; + } +} + +sub git_summary { + my $descr = git_get_project_description($project) || "none"; + my %co = parse_commit("HEAD"); + my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); + my $head = $co{'id'}; + + my $owner = git_get_project_owner($project); + + my $refs = git_get_references(); + # These get_*_list functions return one more to allow us to see if + # there are more ... + my @taglist = git_get_tags_list(16); + my @headlist = git_get_heads_list(16); + my @forklist; + my ($check_forks) = gitweb_check_feature('forks'); + + if ($check_forks) { + @forklist = git_get_projects_list($project); + } + + git_header_html(); + git_print_page_nav('summary','', $head); + + 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"; + # use per project git URL list in $projectroot/$project/cloneurl + # or make project git URL from git base URL and project name + my $url_tag = "URL"; + my @url_list = git_get_project_url_list($project); + @url_list = map { "$_/$project" } @git_base_url_list unless @url_list; + foreach my $git_url (@url_list) { + next unless $git_url; + print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n"; + $url_tag = ""; + } + print "</table>\n"; + + if (-s "$projectroot/$project/README.html") { + if (open my $fd, "$projectroot/$project/README.html") { + print "<div class=\"title\">readme</div>\n"; + print $_ while (<$fd>); + close $fd; + } + } + + # we need to request one more than 16 (0..15) to check if + # those 16 are all + my @commitlist = parse_commits($head, 17); + git_print_header_div('shortlog'); + git_shortlog_body(\@commitlist, 0, 15, $refs, + $#commitlist <= 15 ? undef : + $cgi->a({-href => href(action=>"shortlog")}, "...")); + + if (@taglist) { + git_print_header_div('tags'); + git_tags_body(\@taglist, 0, 15, + $#taglist <= 15 ? undef : + $cgi->a({-href => href(action=>"tags")}, "...")); + } + + if (@headlist) { + git_print_header_div('heads'); + git_heads_body(\@headlist, $head, 0, 15, + $#headlist <= 15 ? undef : + $cgi->a({-href => href(action=>"heads")}, "...")); + } + + if (@forklist) { + git_print_header_div('forks'); + git_project_list_body(\@forklist, undef, 0, 15, + $#forklist <= 15 ? undef : + $cgi->a({-href => href(action=>"forks")}, "..."), + 'noheader'); + } + + git_footer_html(); +} + +sub git_tag { + my $head = git_get_head_hash($project); + git_header_html(); + git_print_page_nav('','', $head,undef,$head); + my %tag = parse_tag($hash); + git_print_header_div('commit', esc_html($tag{'name'}), $hash); + print "<div class=\"title_text\">\n" . + "<table cellspacing=\"0\">\n" . + "<tr>\n" . + "<td>object</td>\n" . + "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, + $tag{'object'}) . "</td>\n" . + "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, + $tag{'type'}) . "</td>\n" . + "</tr>\n"; + if (defined($tag{'author'})) { + my %ad = parse_date($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) { + chomp $line; + print esc_html($line, -nbsp=>1) . "<br/>\n"; + } + print "</div>\n"; + git_footer_html(); +} + +sub git_blame2 { + my $fd; + my $ftype; + + my ($have_blame) = gitweb_check_feature('blame'); + if (!$have_blame) { + die_error('403 Permission denied', "Permission denied"); + } + die_error('404 Not Found', "File name not defined") if (!$file_name); + $hash_base ||= git_get_head_hash($project); + die_error(undef, "Couldn't find base commit") unless ($hash_base); + my %co = parse_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 looking up file"); + } + $ftype = git_get_type($hash); + if ($ftype !~ "blob") { + die_error("400 Bad Request", "Object is not a blob"); + } + open ($fd, "-|", git_cmd(), "blame", '-p', '--', + $file_name, $hash_base) + or die_error(undef, "Open git-blame failed"); + git_header_html(); + my $formats_nav = + $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "blob") . + " | " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "history") . + " | " . + $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, + "HEAD"); + git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + git_print_page_path($file_name, $ftype, $hash_base); + my @rev_color = (qw(light2 dark2)); + my $num_colors = scalar(@rev_color); + my $current_color = 0; + my $last_rev; + print <<HTML; +<div class="page_body"> +<table class="blame"> +<tr><th>Commit</th><th>Line</th><th>Data</th></tr> +HTML + my %metainfo = (); + while (1) { + $_ = <$fd>; + last unless defined $_; + my ($full_rev, $orig_lineno, $lineno, $group_size) = + /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/; + if (!exists $metainfo{$full_rev}) { + $metainfo{$full_rev} = {}; + } + my $meta = $metainfo{$full_rev}; + while (<$fd>) { + last if (s/^\t//); + if (/^(\S+) (.*)$/) { + $meta->{$1} = $2; + } + } + my $data = $_; + chomp $data; + my $rev = substr($full_rev, 0, 8); + my $author = $meta->{'author'}; + my %date = parse_date($meta->{'author-time'}, + $meta->{'author-tz'}); + my $date = $date{'iso-tz'}; + if ($group_size) { + $current_color = ++$current_color % $num_colors; + } + print "<tr class=\"$rev_color[$current_color]\">\n"; + if ($group_size) { + print "<td class=\"sha1\""; + print " title=\"". esc_html($author) . ", $date\""; + print " rowspan=\"$group_size\"" if ($group_size > 1); + print ">"; + print $cgi->a({-href => href(action=>"commit", + hash=>$full_rev, + file_name=>$file_name)}, + esc_html($rev)); + print "</td>\n"; + } + open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^") + or die_error("could not open git-rev-parse"); + my $parent_commit = <$dd>; + close $dd; + chomp($parent_commit); + my $blamed = href(action => 'blame', + file_name => $meta->{'filename'}, + hash_base => $parent_commit); + print "<td class=\"linenr\">"; + print $cgi->a({ -href => "$blamed#l$orig_lineno", + -id => "l$lineno", + -class => "linenr" }, + esc_html($lineno)); + print "</td>"; + print "<td class=\"pre\">" . esc_html($data) . "</td>\n"; + print "</tr>\n"; + } + print "</table>\n"; + print "</div>"; + close $fd + or print "Reading blob failed\n"; + git_footer_html(); +} + +sub git_blame { + my $fd; + + my ($have_blame) = gitweb_check_feature('blame'); + if (!$have_blame) { + die_error('403 Permission denied', "Permission denied"); + } + die_error('404 Not Found', "File name not defined") if (!$file_name); + $hash_base ||= git_get_head_hash($project); + die_error(undef, "Couldn't find base commit") unless ($hash_base); + my %co = parse_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, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base) + or die_error(undef, "Open git-annotate failed"); + git_header_html(); + my $formats_nav = + $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "blob") . + " | " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, + "history") . + " | " . + $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, + "HEAD"); + git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + git_print_page_path($file_name, 'blob', $hash_base); + print "<div class=\"page_body\">\n"; + print <<HTML; +<table class="blame"> + <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_class; + + 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" class="error">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_class = age_class($age); + $author = esc_html ($author); + $author =~ s/ / /g; + + $data = untabify($data); + $data = esc_html ($data); + + print <<HTML; + <tr class="$line_class[$line_class_num]"> + <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td> + <td class="$age_class">$age_str</td> + <td>$author</td> + <td class="linenr"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td> + <td class="pre">$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_get_head_hash($project); + git_header_html(); + git_print_page_nav('','', $head,undef,$head); + git_print_header_div('summary', $project); + + my @tagslist = git_get_tags_list(); + if (@tagslist) { + git_tags_body(\@tagslist); + } + git_footer_html(); +} + +sub git_heads { + my $head = git_get_head_hash($project); + git_header_html(); + git_print_page_nav('','', $head,undef,$head); + git_print_header_div('summary', $project); + + my @headslist = git_get_heads_list(); + if (@headslist) { + git_heads_body(\@headslist, $head); + } + git_footer_html(); +} + +sub git_blob_plain { + my $expires; + + if (!defined $hash) { + if (defined $file_name) { + my $base = $hash_base || git_get_head_hash($project); + $hash = git_get_hash_by_path($base, $file_name, "blob") + or die_error(undef, "Error lookup file"); + } else { + die_error(undef, "No file name defined"); + } + } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) { + # blobs defined by non-textual hash id's can be cached + $expires = "+1d"; + } + + my $type = shift; + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash + or die_error(undef, "Couldn't cat $file_name, $hash"); + + $type ||= blob_mimetype($fd, $file_name); + + # save as filename, even when no $file_name is given + my $save_as = "$hash"; + if (defined $file_name) { + $save_as = $file_name; + } elsif ($type =~ m/^text\//) { + $save_as .= '.txt'; + } + + print $cgi->header( + -type => "$type", + -expires=>$expires, + -content_disposition => 'inline; filename="' . "$save_as" . '"'); + undef $/; + binmode STDOUT, ':raw'; + print <$fd>; + binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi + $/ = "\n"; + close $fd; +} + +sub git_blob { + my $expires; + + if (!defined $hash) { + if (defined $file_name) { + my $base = $hash_base || git_get_head_hash($project); + $hash = git_get_hash_by_path($base, $file_name, "blob") + or die_error(undef, "Error lookup file"); + } else { + die_error(undef, "No file name defined"); + } + } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) { + # blobs defined by non-textual hash id's can be cached + $expires = "+1d"; + } + + my ($have_blame) = gitweb_check_feature('blame'); + open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash + or die_error(undef, "Couldn't cat $file_name, $hash"); + my $mimetype = blob_mimetype($fd, $file_name); + if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)!) { + close $fd; + return git_blob_plain($mimetype); + } + # we can have blame only for text/* mimetype + $have_blame &&= ($mimetype =~ m!^text/!); + + git_header_html(undef, $expires); + my $formats_nav = ''; + if (defined $hash_base && (my %co = parse_commit($hash_base))) { + if (defined $file_name) { + if ($have_blame) { + $formats_nav .= + $cgi->a({-href => href(action=>"blame", hash_base=>$hash_base, + hash=>$hash, file_name=>$file_name)}, + "blame") . + " | "; + } + $formats_nav .= + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + hash=>$hash, file_name=>$file_name)}, + "history") . + " | " . + $cgi->a({-href => href(action=>"blob_plain", + hash=>$hash, file_name=>$file_name)}, + "raw") . + " | " . + $cgi->a({-href => href(action=>"blob", + hash_base=>"HEAD", file_name=>$file_name)}, + "HEAD"); + } else { + $formats_nav .= + $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "raw"); + } + git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + } else { + print "<div class=\"page_nav\">\n" . + "<br/><br/></div>\n" . + "<div class=\"title\">$hash</div>\n"; + } + git_print_page_path($file_name, "blob", $hash_base); + print "<div class=\"page_body\">\n"; + if ($mimetype =~ m!^text/!) { + my $nr; + while (my $line = <$fd>) { + chomp $line; + $nr++; + $line = untabify($line); + printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n", + $nr, $nr, $nr, esc_html($line, -nbsp=>1); + } + } elsif ($mimetype =~ m!^image/!) { + print qq!<img type="$mimetype"!; + if ($file_name) { + print qq! alt="$file_name" title="$file_name"!; + } + print qq! src="! . + href(action=>"blob_plain", hash=>$hash, + hash_base=>$hash_base, file_name=>$file_name) . + qq!" />\n!; + } + close $fd + or print "Reading blob failed.\n"; + print "</div>"; + git_footer_html(); +} + +sub git_tree { + my $have_snapshot = gitweb_have_snapshot(); + + if (!defined $hash_base) { + $hash_base = "HEAD"; + } + if (!defined $hash) { + if (defined $file_name) { + $hash = git_get_hash_by_path($hash_base, $file_name, "tree"); + } else { + $hash = $hash_base; + } + } + $/ = "\0"; + open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash + or die_error(undef, "Open git-ls-tree failed"); + my @entries = map { chomp; $_ } <$fd>; + close $fd or die_error(undef, "Reading tree failed"); + $/ = "\n"; + + my $refs = git_get_references(); + my $ref = format_ref_marker($refs, $hash_base); + git_header_html(); + my $basedir = ''; + my ($have_blame) = gitweb_check_feature('blame'); + if (defined $hash_base && (my %co = parse_commit($hash_base))) { + my @views_nav = (); + if (defined $file_name) { + push @views_nav, + $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, + hash=>$hash, file_name=>$file_name)}, + "history"), + $cgi->a({-href => href(action=>"tree", + hash_base=>"HEAD", file_name=>$file_name)}, + "HEAD"), + } + if ($have_snapshot) { + # FIXME: Should be available when we have no hash base as well. + push @views_nav, + $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, + "snapshot"); + } + git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav)); + git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base); + } else { + undef $hash_base; + print "<div class=\"page_nav\">\n"; + print "<br/><br/></div>\n"; + print "<div class=\"title\">$hash</div>\n"; + } + if (defined $file_name) { + $basedir = $file_name; + if ($basedir ne '' && substr($basedir, -1) ne '/') { + $basedir .= '/'; + } + } + git_print_page_path($file_name, 'tree', $hash_base); + print "<div class=\"page_body\">\n"; + print "<table cellspacing=\"0\">\n"; + my $alternate = 1; + # '..' (top directory) link if possible + if (defined $hash_base && + defined $file_name && $file_name =~ m![^/]+$!) { + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + + my $up = $file_name; + $up =~ s!/?[^/]+$!!; + undef $up unless $up; + # based on git_print_tree_entry + print '<td class="mode">' . mode_str('040000') . "</td>\n"; + print '<td class="list">'; + print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base, + file_name=>$up)}, + ".."); + print "</td>\n"; + print "<td class=\"link\"></td>\n"; + + print "</tr>\n"; + } + foreach my $line (@entries) { + my %t = parse_ls_tree_line($line, -z => 1); + + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + + git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame); + + print "</tr>\n"; + } + print "</table>\n" . + "</div>"; + git_footer_html(); +} + +sub git_snapshot { + my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot'); + my $have_snapshot = (defined $ctype && defined $suffix); + if (!$have_snapshot) { + die_error('403 Permission denied', "Permission denied"); + } + + if (!defined $hash) { + $hash = git_get_head_hash($project); + } + + my $filename = to_utf8(basename($project)) . "-$hash.tar.$suffix"; + + print $cgi->header( + -type => "application/$ctype", + -content_disposition => 'inline; filename="' . "$filename" . '"', + -status => '200 OK'); + + my $git = git_cmd_str(); + my $name = $project; + $name =~ s/\047/\047\\\047\047/g; + open my $fd, "-|", + "$git archive --format=tar --prefix=\'$name\'/ $hash | $command" + or die_error(undef, "Execute git-tar-tree failed."); + binmode STDOUT, ':raw'; + print <$fd>; + binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi + close $fd; + +} + +sub git_log { + my $head = git_get_head_hash($project); + if (!defined $hash) { + $hash = $head; + } + if (!defined $page) { + $page = 0; + } + my $refs = git_get_references(); + + my @commitlist = parse_commits($hash, 101, (100 * $page)); + + my $paging_nav = format_paging_nav('log', $hash, $head, $page, (100 * ($page+1))); + + git_header_html(); + git_print_page_nav('log','', $hash,undef,undef, $paging_nav); + + if (!@commitlist) { + my %co = parse_commit($hash); + + git_print_header_div('summary', $project); + print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n"; + } + my $to = ($#commitlist >= 99) ? (99) : ($#commitlist); + for (my $i = 0; $i <= $to; $i++) { + my %co = %{$commitlist[$i]}; + next if !%co; + my $commit = $co{'id'}; + my $ref = format_ref_marker($refs, $commit); + my %ad = parse_date($co{'author_epoch'}); + git_print_header_div('commit', + "<span class=\"age\">$co{'age_string'}</span>" . + esc_html($co{'title'}) . $ref, + $commit); + print "<div class=\"title_text\">\n" . + "<div class=\"log_link\">\n" . + $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . + " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") . + "<br/>\n" . + "</div>\n" . + "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" . + "</div>\n"; + + print "<div class=\"log_body\">\n"; + git_print_log($co{'comment'}, -final_empty_line=> 1); + print "</div>\n"; + } + if ($#commitlist >= 100) { + print "<div class=\"page_nav\">\n"; + print $cgi->a({-href => href(action=>"log", hash=>$hash, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + print "</div>\n"; + } + git_footer_html(); +} + +sub git_commit { + $hash ||= $hash_base || "HEAD"; + my %co = parse_commit($hash); + if (!%co) { + die_error(undef, "Unknown commit object"); + } + my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'}); + my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); + + my $parent = $co{'parent'}; + my $parents = $co{'parents'}; # listref + + # we need to prepare $formats_nav before any parameter munging + my $formats_nav; + if (!defined $parent) { + # --root commitdiff + $formats_nav .= '(initial)'; + } elsif (@$parents == 1) { + # single parent commit + $formats_nav .= + '(parent: ' . + $cgi->a({-href => href(action=>"commit", + hash=>$parent)}, + esc_html(substr($parent, 0, 7))) . + ')'; + } else { + # merge commit + $formats_nav .= + '(merge: ' . + join(' ', map { + $cgi->a({-href => href(action=>"commitdiff", + hash=>$_)}, + esc_html(substr($_, 0, 7))); + } @$parents ) . + ')'; + } + + if (!defined $parent) { + $parent = "--root"; + } + my @difftree; + if (@$parents <= 1) { + # difftree output is not printed for merges + open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id", + @diff_opts, $parent, $hash, "--" + or die_error(undef, "Open git-diff-tree failed"); + @difftree = map { chomp; $_ } <$fd>; + close $fd or die_error(undef, "Reading git-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 = git_get_references(); + my $ref = format_ref_marker($refs, $co{'id'}); + + my $have_snapshot = gitweb_have_snapshot(); + + git_header_html(undef, $expires); + git_print_page_nav('commit', '', + $hash, $co{'tree'}, $hash, + $formats_nav); + + if (defined $co{'parent'}) { + git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash); + } else { + git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash); + } + 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 class=\"atnight\">%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 class=\"sha1\">$co{'id'}</td></tr>\n"; + print "<tr>" . + "<td>tree</td>" . + "<td class=\"sha1\">" . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash), + class => "list"}, $co{'tree'}) . + "</td>" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)}, + "tree"); + if ($have_snapshot) { + print " | " . + $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot"); + } + print "</td>" . + "</tr>\n"; + + foreach my $par (@$parents) { + print "<tr>" . + "<td>parent</td>" . + "<td class=\"sha1\">" . + $cgi->a({-href => href(action=>"commit", hash=>$par), + class => "list"}, $par) . + "</td>" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") . + " | " . + $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") . + "</td>" . + "</tr>\n"; + } + print "</table>". + "</div>\n"; + + print "<div class=\"page_body\">\n"; + git_print_log($co{'comment'}); + print "</div>\n"; + + if (@$parents <= 1) { + # do not output difftree/whatchanged for merges + git_difftree_body(\@difftree, $hash, $parent); + } + + git_footer_html(); +} + +sub git_object { + # object is defined by: + # - hash or hash_base alone + # - hash_base and file_name + my $type; + + # - hash or hash_base alone + if ($hash || ($hash_base && !defined $file_name)) { + my $object_id = $hash || $hash_base; + + my $git_command = git_cmd_str(); + open my $fd, "-|", "$git_command cat-file -t $object_id 2>/dev/null" + or die_error('404 Not Found', "Object does not exist"); + $type = <$fd>; + chomp $type; + close $fd + or die_error('404 Not Found', "Object does not exist"); + + # - hash_base and file_name + } elsif ($hash_base && defined $file_name) { + $file_name =~ s,/+$,,; + + system(git_cmd(), "cat-file", '-e', $hash_base) == 0 + or die_error('404 Not Found', "Base object does not exist"); + + # here errors should not hapen + open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name + or die_error(undef, "Open git-ls-tree failed"); + my $line = <$fd>; + close $fd; + + #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c' + unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) { + die_error('404 Not Found', "File or directory for given base does not exist"); + } + $type = $2; + $hash = $3; + } else { + die_error('404 Not Found', "Not enough information to find object"); + } + + print $cgi->redirect(-uri => href(action=>$type, -full=>1, + hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name), + -status => '302 Found'); +} + +sub git_blobdiff { + my $format = shift || 'html'; + + my $fd; + my @difftree; + my %diffinfo; + my $expires; + + # preparing $fd and %diffinfo for git_patchset_body + # new style URI + if (defined $hash_base && defined $hash_parent_base) { + if (defined $file_name) { + # read raw output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $hash_parent_base, $hash_base, + "--", $file_name + or die_error(undef, "Open git-diff-tree failed"); + @difftree = map { chomp; $_ } <$fd>; + close $fd + or die_error(undef, "Reading git-diff-tree failed"); + @difftree + or die_error('404 Not Found', "Blob diff not found"); + + } elsif (defined $hash && + $hash =~ /[0-9a-fA-F]{40}/) { + # try to find filename from $hash + + # read filtered raw output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $hash_parent_base, $hash_base, "--" + or die_error(undef, "Open git-diff-tree failed"); + @difftree = + # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c' + # $hash == to_id + grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ } + map { chomp; $_ } <$fd>; + close $fd + or die_error(undef, "Reading git-diff-tree failed"); + @difftree + or die_error('404 Not Found', "Blob diff not found"); + + } else { + die_error('404 Not Found', "Missing one of the blob diff parameters"); + } + + if (@difftree > 1) { + die_error('404 Not Found', "Ambiguous blob diff specification"); + } + + %diffinfo = parse_difftree_raw_line($difftree[0]); + $file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'}; + $file_name ||= $diffinfo{'to_file'} || $diffinfo{'file'}; + + $hash_parent ||= $diffinfo{'from_id'}; + $hash ||= $diffinfo{'to_id'}; + + # non-textual hash id's can be cached + if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ && + $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) { + $expires = '+1d'; + } + + # open patch output + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + '-p', $hash_parent_base, $hash_base, + "--", $file_name + or die_error(undef, "Open git-diff-tree failed"); + } + + # old/legacy style URI + if (!%diffinfo && # if new style URI failed + defined $hash && defined $hash_parent) { + # fake git-diff-tree raw output + $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob"; + $diffinfo{'from_id'} = $hash_parent; + $diffinfo{'to_id'} = $hash; + if (defined $file_name) { + if (defined $file_parent) { + $diffinfo{'status'} = '2'; + $diffinfo{'from_file'} = $file_parent; + $diffinfo{'to_file'} = $file_name; + } else { # assume not renamed + $diffinfo{'status'} = '1'; + $diffinfo{'from_file'} = $file_name; + $diffinfo{'to_file'} = $file_name; + } + } else { # no filename given + $diffinfo{'status'} = '2'; + $diffinfo{'from_file'} = $hash_parent; + $diffinfo{'to_file'} = $hash; + } + + # non-textual hash id's can be cached + if ($hash =~ m/^[0-9a-fA-F]{40}$/ && + $hash_parent =~ m/^[0-9a-fA-F]{40}$/) { + $expires = '+1d'; + } + + # open patch output + open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts, + $hash_parent, $hash, "--" + or die_error(undef, "Open git-diff failed"); + } else { + die_error('404 Not Found', "Missing one of the blob diff parameters") + unless %diffinfo; + } + + # header + if ($format eq 'html') { + my $formats_nav = + $cgi->a({-href => href(action=>"blobdiff_plain", + hash=>$hash, hash_parent=>$hash_parent, + hash_base=>$hash_base, hash_parent_base=>$hash_parent_base, + file_name=>$file_name, file_parent=>$file_parent)}, + "raw"); + git_header_html(undef, $expires); + if (defined $hash_base && (my %co = parse_commit($hash_base))) { + git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + } else { + print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n"; + print "<div class=\"title\">$hash vs $hash_parent</div>\n"; + } + if (defined $file_name) { + git_print_page_path($file_name, "blob", $hash_base); + } else { + print "<div class=\"page_path\"></div>\n"; + } + + } elsif ($format eq 'plain') { + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -expires => $expires, + -content_disposition => 'inline; filename="' . "$file_name" . '.patch"'); + + print "X-Git-Url: " . $cgi->self_url() . "\n\n"; + + } else { + die_error(undef, "Unknown blobdiff format"); + } + + # patch + if ($format eq 'html') { + print "<div class=\"page_body\">\n"; + + git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base); + close $fd; + + print "</div>\n"; # class="page_body" + git_footer_html(); + + } else { + while (my $line = <$fd>) { + $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg; + $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg; + + print $line; + + last if $line =~ m!^\+\+\+!; + } + local $/ = undef; + print <$fd>; + close $fd; + } +} + +sub git_blobdiff_plain { + git_blobdiff('plain'); +} + +sub git_commitdiff { + my $format = shift || 'html'; + $hash ||= $hash_base || "HEAD"; + my %co = parse_commit($hash); + if (!%co) { + die_error(undef, "Unknown commit object"); + } + + # we need to prepare $formats_nav before any parameter munging + my $formats_nav; + if ($format eq 'html') { + $formats_nav = + $cgi->a({-href => href(action=>"commitdiff_plain", + hash=>$hash, hash_parent=>$hash_parent)}, + "raw"); + + if (defined $hash_parent) { + # commitdiff with two commits given + my $hash_parent_short = $hash_parent; + if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) { + $hash_parent_short = substr($hash_parent, 0, 7); + } + $formats_nav .= + ' (from: ' . + $cgi->a({-href => href(action=>"commitdiff", + hash=>$hash_parent)}, + esc_html($hash_parent_short)) . + ')'; + } elsif (!$co{'parent'}) { + # --root commitdiff + $formats_nav .= ' (initial)'; + } elsif (scalar @{$co{'parents'}} == 1) { + # single parent commit + $formats_nav .= + ' (parent: ' . + $cgi->a({-href => href(action=>"commitdiff", + hash=>$co{'parent'})}, + esc_html(substr($co{'parent'}, 0, 7))) . + ')'; + } else { + # merge commit + $formats_nav .= + ' (merge: ' . + join(' ', map { + $cgi->a({-href => href(action=>"commitdiff", + hash=>$_)}, + esc_html(substr($_, 0, 7))); + } @{$co{'parents'}} ) . + ')'; + } + } + + if (!defined $hash_parent) { + $hash_parent = $co{'parent'} || '--root'; + } + + # read commitdiff + my $fd; + my @difftree; + if ($format eq 'html') { + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + "--no-commit-id", "--patch-with-raw", "--full-index", + $hash_parent, $hash, "--" + or die_error(undef, "Open git-diff-tree failed"); + + while (my $line = <$fd>) { + chomp $line; + # empty line ends raw part of diff-tree output + last unless $line; + push @difftree, $line; + } + + } elsif ($format eq 'plain') { + open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + '-p', $hash_parent, $hash, "--" + or die_error(undef, "Open git-diff-tree failed"); + + } else { + die_error(undef, "Unknown commitdiff format"); + } + + # non-textual hash id's can be cached + my $expires; + if ($hash =~ m/^[0-9a-fA-F]{40}$/) { + $expires = "+1d"; + } + + # write commit message + if ($format eq 'html') { + my $refs = git_get_references(); + my $ref = format_ref_marker($refs, $co{'id'}); + + git_header_html(undef, $expires); + git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav); + git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash); + git_print_authorship(\%co); + print "<div class=\"page_body\">\n"; + if (@{$co{'comment'}} > 1) { + print "<div class=\"log\">\n"; + git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1); + print "</div>\n"; # class="log" + } + + } elsif ($format eq 'plain') { + my $refs = git_get_references("tags"); + my $tagname = git_get_rev_name_tags($hash); + my $filename = basename($project) . "-$hash.patch"; + + print $cgi->header( + -type => 'text/plain', + -charset => 'utf-8', + -expires => $expires, + -content_disposition => 'inline; filename="' . "$filename" . '"'); + my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'}); + print <<TEXT; +From: $co{'author'} +Date: $ad{'rfc2822'} ($ad{'tz_local'}) +Subject: $co{'title'} +TEXT + print "X-Git-Tag: $tagname\n" if $tagname; + print "X-Git-Url: " . $cgi->self_url() . "\n\n"; + + foreach my $line (@{$co{'comment'}}) { + print "$line\n"; + } + print "---\n\n"; + } + + # write patch + if ($format eq 'html') { + git_difftree_body(\@difftree, $hash, $hash_parent); + print "<br/>\n"; + + git_patchset_body($fd, \@difftree, $hash, $hash_parent); + close $fd; + print "</div>\n"; # class="page_body" + git_footer_html(); + + } elsif ($format eq 'plain') { + local $/ = undef; + print <$fd>; + close $fd + or print "Reading git-diff-tree failed\n"; + } +} + +sub git_commitdiff_plain { + git_commitdiff('plain'); +} + +sub git_history { + if (!defined $hash_base) { + $hash_base = git_get_head_hash($project); + } + if (!defined $page) { + $page = 0; + } + my $ftype; + my %co = parse_commit($hash_base); + if (!%co) { + die_error(undef, "Unknown commit object"); + } + + my $refs = git_get_references(); + my $limit = sprintf("--max-count=%i", (100 * ($page+1))); + + if (!defined $hash && defined $file_name) { + $hash = git_get_hash_by_path($hash_base, $file_name); + } + if (defined $hash) { + $ftype = git_get_type($hash); + } + + my @commitlist = parse_commits($hash_base, 101, (100 * $page), "--full-history", $file_name); + + my $paging_nav = ''; + if ($page > 0) { + $paging_nav .= + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name)}, + "first"); + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name, page=>$page-1), + -accesskey => "p", -title => "Alt-p"}, "prev"); + } else { + $paging_nav .= "first"; + $paging_nav .= " ⋅ prev"; + } + if ($#commitlist >= 100) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } else { + $paging_nav .= " ⋅ next"; + } + my $next_link = ''; + if ($#commitlist >= 100) { + $next_link = + $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, + file_name=>$file_name, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } + + git_header_html(); + git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash_base); + git_print_page_path($file_name, $ftype, $hash_base); + + git_history_body(\@commitlist, 0, 99, + $refs, $hash_base, $ftype, $next_link); + + git_footer_html(); +} + +sub git_search { + my ($have_search) = gitweb_check_feature('search'); + if (!$have_search) { + die_error('403 Permission denied', "Permission denied"); + } + if (!defined $searchtext) { + die_error(undef, "Text field empty"); + } + if (!defined $hash) { + $hash = git_get_head_hash($project); + } + my %co = parse_commit($hash); + if (!%co) { + die_error(undef, "Unknown commit object"); + } + if (!defined $page) { + $page = 0; + } + + $searchtype ||= 'commit'; + if ($searchtype eq 'pickaxe') { + # 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 ($have_pickaxe) = gitweb_check_feature('pickaxe'); + if (!$have_pickaxe) { + die_error('403 Permission denied', "Permission denied"); + } + } + + git_header_html(); + + if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') { + my $greptype; + if ($searchtype eq 'commit') { + $greptype = "--grep="; + } elsif ($searchtype eq 'author') { + $greptype = "--author="; + } elsif ($searchtype eq 'committer') { + $greptype = "--committer="; + } + $greptype .= $searchtext; + my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype); + + my $paging_nav = ''; + if ($page > 0) { + $paging_nav .= + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype)}, + "first"); + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page-1), + -accesskey => "p", -title => "Alt-p"}, "prev"); + } else { + $paging_nav .= "first"; + $paging_nav .= " ⋅ prev"; + } + if ($#commitlist >= 100) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } else { + $paging_nav .= " ⋅ next"; + } + my $next_link = ''; + if ($#commitlist >= 100) { + $next_link = + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } + + git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash); + git_search_grep_body(\@commitlist, 0, 99, $next_link); + } + + if ($searchtype eq 'pickaxe') { + git_print_page_nav('','', $hash,$co{'tree'},$hash); + git_print_header_div('commit', esc_html($co{'title'}), $hash); + + print "<table cellspacing=\"0\">\n"; + my $alternate = 1; + $/ = "\n"; + my $git_command = git_cmd_str(); + open my $fd, "-|", "$git_command rev-list $hash | " . + "$git_command 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 => href(action=>"commit", hash=>$co{'id'}), + -class => "list subject"}, + esc_html(chop_str($co{'title'}, 50)) . "<br/>"); + while (my $setref = shift @files) { + my %set = %$setref; + print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'}, + hash=>$set{'id'}, file_name=>$set{'file'}), + -class => "list"}, + "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") . + "<br/>\n"; + } + print "</td>\n" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); + print "</td>\n" . + "</tr>\n"; + } + %co = parse_commit($1); + } + } + close $fd; + + print "</table>\n"; + } + git_footer_html(); +} + +sub git_search_help { + git_header_html(); + git_print_page_nav('','', $hash,$hash,$hash); + print <<EOT; +<dl> +<dt><b>commit</b></dt> +<dd>The commit messages and authorship information will be scanned for the given string.</dd> +<dt><b>author</b></dt> +<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given string.</dd> +<dt><b>committer</b></dt> +<dd>Name and e-mail of the committer and date of commit will be scanned for the given string.</dd> +EOT + my ($have_pickaxe) = gitweb_check_feature('pickaxe'); + if ($have_pickaxe) { + print <<EOT; +<dt><b>pickaxe</b></dt> +<dd>All commits that caused the string to appear or disappear from any file (changes that +added, removed or "modified" the string) will be listed. This search can take a while and +takes a lot of strain on the server, so please use it wisely.</dd> +EOT + } + print "</dl>\n"; + git_footer_html(); +} + +sub git_shortlog { + my $head = git_get_head_hash($project); + if (!defined $hash) { + $hash = $head; + } + if (!defined $page) { + $page = 0; + } + my $refs = git_get_references(); + + my @commitlist = parse_commits($hash, 101, (100 * $page)); + + my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, (100 * ($page+1))); + my $next_link = ''; + if ($#commitlist >= 100) { + $next_link = + $cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } + + git_header_html(); + git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav); + git_print_header_div('summary', $project); + + git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link); + + git_footer_html(); +} + +## ...................................................................... +## feeds (RSS, Atom; OPML) + +sub git_feed { + my $format = shift || 'atom'; + my ($have_blame) = gitweb_check_feature('blame'); + + # Atom: http://www.atomenabled.org/developers/syndication/ + # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ + if ($format ne 'rss' && $format ne 'atom') { + die_error(undef, "Unknown web feed format"); + } + + # log/feed of current (HEAD) branch, log of given branch, history of file/directory + my $head = $hash || 'HEAD'; + my @commitlist = parse_commits($head, 150); + + my %latest_commit; + my %latest_date; + my $content_type = "application/$format+xml"; + if (defined $cgi->http('HTTP_ACCEPT') && + $cgi->Accept('text/xml') > $cgi->Accept($content_type)) { + # browser (feed reader) prefers text/xml + $content_type = 'text/xml'; + } + if (defined($commitlist[0])) { + %latest_commit = %{$commitlist[0]}; + %latest_date = parse_date($latest_commit{'author_epoch'}); + print $cgi->header( + -type => $content_type, + -charset => 'utf-8', + -last_modified => $latest_date{'rfc2822'}); + } else { + print $cgi->header( + -type => $content_type, + -charset => 'utf-8'); + } + + # Optimization: skip generating the body if client asks only + # for Last-Modified date. + return if ($cgi->request_method() eq 'HEAD'); + + # header variables + my $title = "$site_name - $project/$action"; + my $feed_type = 'log'; + if (defined $hash) { + $title .= " - '$hash'"; + $feed_type = 'branch log'; + if (defined $file_name) { + $title .= " :: $file_name"; + $feed_type = 'history'; + } + } elsif (defined $file_name) { + $title .= " - $file_name"; + $feed_type = 'history'; + } + $title .= " $feed_type"; + my $descr = git_get_project_description($project); + if (defined $descr) { + $descr = esc_html($descr); + } else { + $descr = "$project " . + ($format eq 'rss' ? 'RSS' : 'Atom') . + " feed"; + } + my $owner = git_get_project_owner($project); + $owner = esc_html($owner); + + #header + my $alt_url; + if (defined $file_name) { + $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name); + } elsif (defined $hash) { + $alt_url = href(-full=>1, action=>"log", hash=>$hash); + } else { + $alt_url = href(-full=>1, action=>"summary"); + } + print qq!<?xml version="1.0" encoding="utf-8"?>\n!; + if ($format eq 'rss') { + print <<XML; +<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"> +<channel> +XML + print "<title>$title</title>\n" . + "<link>$alt_url</link>\n" . + "<description>$descr</description>\n" . + "<language>en</language>\n"; + } elsif ($format eq 'atom') { + print <<XML; +<feed xmlns="http://www.w3.org/2005/Atom"> +XML + print "<title>$title</title>\n" . + "<subtitle>$descr</subtitle>\n" . + '<link rel="alternate" type="text/html" href="' . + $alt_url . '" />' . "\n" . + '<link rel="self" type="' . $content_type . '" href="' . + $cgi->self_url() . '" />' . "\n" . + "<id>" . href(-full=>1) . "</id>\n" . + # use project owner for feed author + "<author><name>$owner</name></author>\n"; + if (defined $favicon) { + print "<icon>" . esc_url($favicon) . "</icon>\n"; + } + if (defined $logo_url) { + # not twice as wide as tall: 72 x 27 pixels + print "<logo>" . esc_url($logo) . "</logo>\n"; + } + if (! %latest_date) { + # dummy date to keep the feed valid until commits trickle in: + print "<updated>1970-01-01T00:00:00Z</updated>\n"; + } else { + print "<updated>$latest_date{'iso-8601'}</updated>\n"; + } + } + + # contents + for (my $i = 0; $i <= $#commitlist; $i++) { + my %co = %{$commitlist[$i]}; + my $commit = $co{'id'}; + # we read 150, we always show 30 and the ones more recent than 48 hours + if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) { + last; + } + my %cd = parse_date($co{'author_epoch'}); + + # get list of changed files + open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, + $co{'parent'}, $co{'id'}, "--", (defined $file_name ? $file_name : ()) + or next; + my @difftree = map { chomp; $_ } <$fd>; + close $fd + or next; + + # print element (entry, item) + my $co_url = href(-full=>1, action=>"commit", hash=>$commit); + if ($format eq 'rss') { + print "<item>\n" . + "<title>" . esc_html($co{'title'}) . "</title>\n" . + "<author>" . esc_html($co{'author'}) . "</author>\n" . + "<pubDate>$cd{'rfc2822'}</pubDate>\n" . + "<guid isPermaLink=\"true\">$co_url</guid>\n" . + "<link>$co_url</link>\n" . + "<description>" . esc_html($co{'title'}) . "</description>\n" . + "<content:encoded>" . + "<![CDATA[\n"; + } elsif ($format eq 'atom') { + print "<entry>\n" . + "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" . + "<updated>$cd{'iso-8601'}</updated>\n" . + "<author>\n" . + " <name>" . esc_html($co{'author_name'}) . "</name>\n"; + if ($co{'author_email'}) { + print " <email>" . esc_html($co{'author_email'}) . "</email>\n"; + } + print "</author>\n" . + # use committer for contributor + "<contributor>\n" . + " <name>" . esc_html($co{'committer_name'}) . "</name>\n"; + if ($co{'committer_email'}) { + print " <email>" . esc_html($co{'committer_email'}) . "</email>\n"; + } + print "</contributor>\n" . + "<published>$cd{'iso-8601'}</published>\n" . + "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" . + "<id>$co_url</id>\n" . + "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" . + "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n"; + } + my $comment = $co{'comment'}; + print "<pre>\n"; + foreach my $line (@$comment) { + $line = esc_html($line); + print "$line\n"; + } + print "</pre><ul>\n"; + foreach my $difftree_line (@difftree) { + my %difftree = parse_difftree_raw_line($difftree_line); + next if !$difftree{'from_id'}; + + my $file = $difftree{'file'} || $difftree{'to_file'}; + + print "<li>" . + "[" . + $cgi->a({-href => href(-full=>1, action=>"blobdiff", + hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'}, + hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'}, + file_name=>$file, file_parent=>$difftree{'from_file'}), + -title => "diff"}, 'D'); + if ($have_blame) { + print $cgi->a({-href => href(-full=>1, action=>"blame", + file_name=>$file, hash_base=>$commit), + -title => "blame"}, 'B'); + } + # if this is not a feed of a file history + if (!defined $file_name || $file_name ne $file) { + print $cgi->a({-href => href(-full=>1, action=>"history", + file_name=>$file, hash=>$commit), + -title => "history"}, 'H'); + } + $file = esc_path($file); + print "] ". + "$file</li>\n"; + } + if ($format eq 'rss') { + print "</ul>]]>\n" . + "</content:encoded>\n" . + "</item>\n"; + } elsif ($format eq 'atom') { + print "</ul>\n</div>\n" . + "</content>\n" . + "</entry>\n"; + } + } + + # end of feed + if ($format eq 'rss') { + print "</channel>\n</rss>\n"; + } elsif ($format eq 'atom') { + print "</feed>\n"; + } +} + +sub git_rss { + git_feed('rss'); +} + +sub git_atom { + git_feed('atom'); +} + +sub git_opml { + my @list = git_get_projects_list(); + + print $cgi->header(-type => 'text/xml', -charset => 'utf-8'); + print <<XML; +<?xml version="1.0" encoding="utf-8"?> +<opml version="1.0"> +<head> + <title>$site_name OPML Export</title> +</head> +<body> +<outline text="git RSS feeds"> +XML + + foreach my $pr (@list) { + my %proj = %$pr; + my $head = git_get_head_hash($proj{'path'}); + if (!defined $head) { + next; + } + $git_dir = "$projectroot/$proj{'path'}"; + my %co = parse_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 <<XML; +</outline> +</body> +</opml> +XML +} diff --git a/gitweb/test/Märchen b/gitweb/test/Märchen new file mode 100644 index 0000000000..8f7a1d3e9c --- /dev/null +++ b/gitweb/test/Märchen @@ -0,0 +1,2 @@ +Märchen +Märchen diff --git a/gitweb/test/file with spaces b/gitweb/test/file with spaces new file mode 100644 index 0000000000..f108543c4e --- /dev/null +++ b/gitweb/test/file with spaces @@ -0,0 +1,4 @@ +This +filename +contains +spaces. diff --git a/gitweb/test/file+plus+sign b/gitweb/test/file+plus+sign new file mode 100644 index 0000000000..fd05278808 --- /dev/null +++ b/gitweb/test/file+plus+sign @@ -0,0 +1,6 @@ +This +filename +contains ++ +plus +chars. diff --git a/grep.c b/grep.c new file mode 100644 index 0000000000..fcc6762302 --- /dev/null +++ b/grep.c @@ -0,0 +1,575 @@ +#include "cache.h" +#include "grep.h" + +void append_grep_pattern(struct grep_opt *opt, const char *pat, + const char *origin, int no, enum grep_pat_token t) +{ + struct grep_pat *p = xcalloc(1, sizeof(*p)); + p->pattern = pat; + p->origin = origin; + p->no = no; + p->token = t; + *opt->pattern_tail = p; + opt->pattern_tail = &p->next; + p->next = NULL; +} + +static void compile_regexp(struct grep_pat *p, struct grep_opt *opt) +{ + int err = regcomp(&p->regexp, p->pattern, opt->regflags); + if (err) { + char errbuf[1024]; + char where[1024]; + if (p->no) + sprintf(where, "In '%s' at %d, ", + p->origin, p->no); + else if (p->origin) + sprintf(where, "%s, ", p->origin); + else + where[0] = 0; + regerror(err, &p->regexp, errbuf, 1024); + regfree(&p->regexp); + die("%s'%s': %s", where, p->pattern, errbuf); + } +} + +static struct grep_expr *compile_pattern_or(struct grep_pat **); +static struct grep_expr *compile_pattern_atom(struct grep_pat **list) +{ + struct grep_pat *p; + struct grep_expr *x; + + p = *list; + switch (p->token) { + case GREP_PATTERN: /* atom */ + case GREP_PATTERN_HEAD: + case GREP_PATTERN_BODY: + x = xcalloc(1, sizeof (struct grep_expr)); + x->node = GREP_NODE_ATOM; + x->u.atom = p; + *list = p->next; + return x; + case GREP_OPEN_PAREN: + *list = p->next; + x = compile_pattern_or(list); + if (!x) + return NULL; + if (!*list || (*list)->token != GREP_CLOSE_PAREN) + die("unmatched parenthesis"); + *list = (*list)->next; + return x; + default: + return NULL; + } +} + +static struct grep_expr *compile_pattern_not(struct grep_pat **list) +{ + struct grep_pat *p; + struct grep_expr *x; + + p = *list; + switch (p->token) { + case GREP_NOT: + if (!p->next) + die("--not not followed by pattern expression"); + *list = p->next; + x = xcalloc(1, sizeof (struct grep_expr)); + x->node = GREP_NODE_NOT; + x->u.unary = compile_pattern_not(list); + if (!x->u.unary) + die("--not followed by non pattern expression"); + return x; + default: + return compile_pattern_atom(list); + } +} + +static struct grep_expr *compile_pattern_and(struct grep_pat **list) +{ + struct grep_pat *p; + struct grep_expr *x, *y, *z; + + x = compile_pattern_not(list); + p = *list; + if (p && p->token == GREP_AND) { + if (!p->next) + die("--and not followed by pattern expression"); + *list = p->next; + y = compile_pattern_and(list); + if (!y) + die("--and not followed by pattern expression"); + z = xcalloc(1, sizeof (struct grep_expr)); + z->node = GREP_NODE_AND; + z->u.binary.left = x; + z->u.binary.right = y; + return z; + } + return x; +} + +static struct grep_expr *compile_pattern_or(struct grep_pat **list) +{ + struct grep_pat *p; + struct grep_expr *x, *y, *z; + + x = compile_pattern_and(list); + p = *list; + if (x && p && p->token != GREP_CLOSE_PAREN) { + y = compile_pattern_or(list); + if (!y) + die("not a pattern expression %s", p->pattern); + z = xcalloc(1, sizeof (struct grep_expr)); + z->node = GREP_NODE_OR; + z->u.binary.left = x; + z->u.binary.right = y; + return z; + } + return x; +} + +static struct grep_expr *compile_pattern_expr(struct grep_pat **list) +{ + return compile_pattern_or(list); +} + +void compile_grep_patterns(struct grep_opt *opt) +{ + struct grep_pat *p; + + if (opt->all_match) + opt->extended = 1; + + for (p = opt->pattern_list; p; p = p->next) { + switch (p->token) { + case GREP_PATTERN: /* atom */ + case GREP_PATTERN_HEAD: + case GREP_PATTERN_BODY: + if (!opt->fixed) + compile_regexp(p, opt); + break; + default: + opt->extended = 1; + break; + } + } + + if (!opt->extended) + return; + + /* Then bundle them up in an expression. + * A classic recursive descent parser would do. + */ + p = opt->pattern_list; + opt->pattern_expression = compile_pattern_expr(&p); + if (p) + die("incomplete pattern expression: %s", p->pattern); +} + +static void free_pattern_expr(struct grep_expr *x) +{ + switch (x->node) { + case GREP_NODE_ATOM: + break; + case GREP_NODE_NOT: + free_pattern_expr(x->u.unary); + break; + case GREP_NODE_AND: + case GREP_NODE_OR: + free_pattern_expr(x->u.binary.left); + free_pattern_expr(x->u.binary.right); + break; + } + free(x); +} + +void free_grep_patterns(struct grep_opt *opt) +{ + struct grep_pat *p, *n; + + for (p = opt->pattern_list; p; p = n) { + n = p->next; + switch (p->token) { + case GREP_PATTERN: /* atom */ + case GREP_PATTERN_HEAD: + case GREP_PATTERN_BODY: + regfree(&p->regexp); + break; + default: + break; + } + free(p); + } + + if (!opt->extended) + return; + free_pattern_expr(opt->pattern_expression); +} + +static char *end_of_line(char *cp, unsigned long *left) +{ + unsigned long l = *left; + while (l && *cp != '\n') { + l--; + cp++; + } + *left = l; + return cp; +} + +static int word_char(char ch) +{ + return isalnum(ch) || ch == '_'; +} + +static void show_line(struct grep_opt *opt, const char *bol, const char *eol, + const char *name, unsigned lno, char sign) +{ + if (opt->pathname) + printf("%s%c", name, sign); + if (opt->linenum) + printf("%d%c", lno, sign); + printf("%.*s\n", (int)(eol-bol), bol); +} + +/* + * NEEDSWORK: share code with diff.c + */ +#define FIRST_FEW_BYTES 8000 +static int buffer_is_binary(const char *ptr, unsigned long size) +{ + if (FIRST_FEW_BYTES < size) + size = FIRST_FEW_BYTES; + return !!memchr(ptr, 0, size); +} + +static int fixmatch(const char *pattern, char *line, regmatch_t *match) +{ + char *hit = strstr(line, pattern); + if (!hit) { + match->rm_so = match->rm_eo = -1; + return REG_NOMATCH; + } + else { + match->rm_so = hit - line; + match->rm_eo = match->rm_so + strlen(pattern); + return 0; + } +} + +static int match_one_pattern(struct grep_opt *opt, struct grep_pat *p, char *bol, char *eol, enum grep_context ctx) +{ + int hit = 0; + int at_true_bol = 1; + regmatch_t pmatch[10]; + + if ((p->token != GREP_PATTERN) && + ((p->token == GREP_PATTERN_HEAD) != (ctx == GREP_CONTEXT_HEAD))) + return 0; + + again: + if (!opt->fixed) { + regex_t *exp = &p->regexp; + hit = !regexec(exp, bol, ARRAY_SIZE(pmatch), + pmatch, 0); + } + else { + hit = !fixmatch(p->pattern, bol, pmatch); + } + + if (hit && opt->word_regexp) { + if ((pmatch[0].rm_so < 0) || + (eol - bol) <= pmatch[0].rm_so || + (pmatch[0].rm_eo < 0) || + (eol - bol) < pmatch[0].rm_eo) + die("regexp returned nonsense"); + + /* Match beginning must be either beginning of the + * line, or at word boundary (i.e. the last char must + * not be a word char). Similarly, match end must be + * either end of the line, or at word boundary + * (i.e. the next char must not be a word char). + */ + if ( ((pmatch[0].rm_so == 0 && at_true_bol) || + !word_char(bol[pmatch[0].rm_so-1])) && + ((pmatch[0].rm_eo == (eol-bol)) || + !word_char(bol[pmatch[0].rm_eo])) ) + ; + else + hit = 0; + + if (!hit && pmatch[0].rm_so + bol + 1 < eol) { + /* There could be more than one match on the + * line, and the first match might not be + * strict word match. But later ones could be! + */ + bol = pmatch[0].rm_so + bol + 1; + at_true_bol = 0; + goto again; + } + } + return hit; +} + +static int match_expr_eval(struct grep_opt *o, + struct grep_expr *x, + char *bol, char *eol, + enum grep_context ctx, + int collect_hits) +{ + int h = 0; + + switch (x->node) { + case GREP_NODE_ATOM: + h = match_one_pattern(o, x->u.atom, bol, eol, ctx); + break; + case GREP_NODE_NOT: + h = !match_expr_eval(o, x->u.unary, bol, eol, ctx, 0); + break; + case GREP_NODE_AND: + if (!collect_hits) + return (match_expr_eval(o, x->u.binary.left, + bol, eol, ctx, 0) && + match_expr_eval(o, x->u.binary.right, + bol, eol, ctx, 0)); + h = match_expr_eval(o, x->u.binary.left, bol, eol, ctx, 0); + h &= match_expr_eval(o, x->u.binary.right, bol, eol, ctx, 0); + break; + case GREP_NODE_OR: + if (!collect_hits) + return (match_expr_eval(o, x->u.binary.left, + bol, eol, ctx, 0) || + match_expr_eval(o, x->u.binary.right, + bol, eol, ctx, 0)); + h = match_expr_eval(o, x->u.binary.left, bol, eol, ctx, 0); + x->u.binary.left->hit |= h; + h |= match_expr_eval(o, x->u.binary.right, bol, eol, ctx, 1); + break; + default: + die("Unexpected node type (internal error) %d\n", x->node); + } + if (collect_hits) + x->hit |= h; + return h; +} + +static int match_expr(struct grep_opt *opt, char *bol, char *eol, + enum grep_context ctx, int collect_hits) +{ + struct grep_expr *x = opt->pattern_expression; + return match_expr_eval(opt, x, bol, eol, ctx, collect_hits); +} + +static int match_line(struct grep_opt *opt, char *bol, char *eol, + enum grep_context ctx, int collect_hits) +{ + struct grep_pat *p; + if (opt->extended) + return match_expr(opt, bol, eol, ctx, collect_hits); + + /* we do not call with collect_hits without being extended */ + for (p = opt->pattern_list; p; p = p->next) { + if (match_one_pattern(opt, p, bol, eol, ctx)) + return 1; + } + return 0; +} + +static int grep_buffer_1(struct grep_opt *opt, const char *name, + char *buf, unsigned long size, int collect_hits) +{ + char *bol = buf; + unsigned long left = size; + unsigned lno = 1; + struct pre_context_line { + char *bol; + char *eol; + } *prev = NULL, *pcl; + unsigned last_hit = 0; + unsigned last_shown = 0; + int binary_match_only = 0; + const char *hunk_mark = ""; + unsigned count = 0; + enum grep_context ctx = GREP_CONTEXT_HEAD; + + if (buffer_is_binary(buf, size)) { + switch (opt->binary) { + case GREP_BINARY_DEFAULT: + binary_match_only = 1; + break; + case GREP_BINARY_NOMATCH: + return 0; /* Assume unmatch */ + break; + default: + break; + } + } + + if (opt->pre_context) + prev = xcalloc(opt->pre_context, sizeof(*prev)); + if (opt->pre_context || opt->post_context) + hunk_mark = "--\n"; + + while (left) { + char *eol, ch; + int hit; + + eol = end_of_line(bol, &left); + ch = *eol; + *eol = 0; + + if ((ctx == GREP_CONTEXT_HEAD) && (eol == bol)) + ctx = GREP_CONTEXT_BODY; + + hit = match_line(opt, bol, eol, ctx, collect_hits); + *eol = ch; + + if (collect_hits) + goto next_line; + + /* "grep -v -e foo -e bla" should list lines + * that do not have either, so inversion should + * be done outside. + */ + if (opt->invert) + hit = !hit; + if (opt->unmatch_name_only) { + if (hit) + return 0; + goto next_line; + } + if (hit) { + count++; + if (opt->status_only) + return 1; + if (binary_match_only) { + printf("Binary file %s matches\n", name); + return 1; + } + if (opt->name_only) { + printf("%s\n", name); + return 1; + } + /* Hit at this line. If we haven't shown the + * pre-context lines, we would need to show them. + * When asked to do "count", this still show + * the context which is nonsense, but the user + * deserves to get that ;-). + */ + if (opt->pre_context) { + unsigned from; + if (opt->pre_context < lno) + from = lno - opt->pre_context; + else + from = 1; + if (from <= last_shown) + from = last_shown + 1; + if (last_shown && from != last_shown + 1) + printf(hunk_mark); + while (from < lno) { + pcl = &prev[lno-from-1]; + show_line(opt, pcl->bol, pcl->eol, + name, from, '-'); + from++; + } + last_shown = lno-1; + } + if (last_shown && lno != last_shown + 1) + printf(hunk_mark); + if (!opt->count) + show_line(opt, bol, eol, name, lno, ':'); + last_shown = last_hit = lno; + } + else if (last_hit && + lno <= last_hit + opt->post_context) { + /* If the last hit is within the post context, + * we need to show this line. + */ + if (last_shown && lno != last_shown + 1) + printf(hunk_mark); + show_line(opt, bol, eol, name, lno, '-'); + last_shown = lno; + } + if (opt->pre_context) { + memmove(prev+1, prev, + (opt->pre_context-1) * sizeof(*prev)); + prev->bol = bol; + prev->eol = eol; + } + + next_line: + bol = eol + 1; + if (!left) + break; + left--; + lno++; + } + + free(prev); + if (collect_hits) + return 0; + + if (opt->status_only) + return 0; + if (opt->unmatch_name_only) { + /* We did not see any hit, so we want to show this */ + printf("%s\n", name); + return 1; + } + + /* NEEDSWORK: + * The real "grep -c foo *.c" gives many "bar.c:0" lines, + * which feels mostly useless but sometimes useful. Maybe + * make it another option? For now suppress them. + */ + if (opt->count && count) + printf("%s:%u\n", name, count); + return !!last_hit; +} + +static void clr_hit_marker(struct grep_expr *x) +{ + /* All-hit markers are meaningful only at the very top level + * OR node. + */ + while (1) { + x->hit = 0; + if (x->node != GREP_NODE_OR) + return; + x->u.binary.left->hit = 0; + x = x->u.binary.right; + } +} + +static int chk_hit_marker(struct grep_expr *x) +{ + /* Top level nodes have hit markers. See if they all are hits */ + while (1) { + if (x->node != GREP_NODE_OR) + return x->hit; + if (!x->u.binary.left->hit) + return 0; + x = x->u.binary.right; + } +} + +int grep_buffer(struct grep_opt *opt, const char *name, char *buf, unsigned long size) +{ + /* + * we do not have to do the two-pass grep when we do not check + * buffer-wide "all-match". + */ + if (!opt->all_match) + return grep_buffer_1(opt, name, buf, size, 0); + + /* Otherwise the toplevel "or" terms hit a bit differently. + * We first clear hit markers from them. + */ + clr_hit_marker(opt->pattern_expression); + grep_buffer_1(opt, name, buf, size, 1); + + if (!chk_hit_marker(opt->pattern_expression)) + return 0; + + return grep_buffer_1(opt, name, buf, size, 0); +} diff --git a/grep.h b/grep.h new file mode 100644 index 0000000000..d252dd25f8 --- /dev/null +++ b/grep.h @@ -0,0 +1,81 @@ +#ifndef GREP_H +#define GREP_H + +enum grep_pat_token { + GREP_PATTERN, + GREP_PATTERN_HEAD, + GREP_PATTERN_BODY, + GREP_AND, + GREP_OPEN_PAREN, + GREP_CLOSE_PAREN, + GREP_NOT, + GREP_OR, +}; + +enum grep_context { + GREP_CONTEXT_HEAD, + GREP_CONTEXT_BODY, +}; + +struct grep_pat { + struct grep_pat *next; + const char *origin; + int no; + enum grep_pat_token token; + const char *pattern; + regex_t regexp; +}; + +enum grep_expr_node { + GREP_NODE_ATOM, + GREP_NODE_NOT, + GREP_NODE_AND, + GREP_NODE_OR, +}; + +struct grep_expr { + enum grep_expr_node node; + unsigned hit; + union { + struct grep_pat *atom; + struct grep_expr *unary; + struct { + struct grep_expr *left; + struct grep_expr *right; + } binary; + } u; +}; + +struct grep_opt { + struct grep_pat *pattern_list; + struct grep_pat **pattern_tail; + struct grep_expr *pattern_expression; + int prefix_length; + regex_t regexp; + unsigned linenum:1; + unsigned invert:1; + unsigned status_only:1; + unsigned name_only:1; + unsigned unmatch_name_only:1; + unsigned count:1; + unsigned word_regexp:1; + unsigned fixed:1; + unsigned all_match:1; +#define GREP_BINARY_DEFAULT 0 +#define GREP_BINARY_NOMATCH 1 +#define GREP_BINARY_TEXT 2 + unsigned binary:2; + unsigned extended:1; + unsigned relative:1; + unsigned pathname:1; + int regflags; + unsigned pre_context; + unsigned post_context; +}; + +extern void append_grep_pattern(struct grep_opt *opt, const char *pat, const char *origin, int no, enum grep_pat_token t); +extern void compile_grep_patterns(struct grep_opt *opt); +extern void free_grep_patterns(struct grep_opt *opt); +extern int grep_buffer(struct grep_opt *opt, const char *name, char *buf, unsigned long size); + +#endif diff --git a/hash-object.c b/hash-object.c new file mode 100644 index 0000000000..5f89e64c13 --- /dev/null +++ b/hash-object.c @@ -0,0 +1,81 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + * Copyright (C) Junio C Hamano, 2005 + */ +#include "cache.h" +#include "blob.h" + +static void hash_object(const char *path, const char *type, int write_object) +{ + int fd; + struct stat st; + unsigned char sha1[20]; + fd = open(path, O_RDONLY); + if (fd < 0 || + fstat(fd, &st) < 0 || + index_fd(sha1, fd, &st, write_object, type)) + die(write_object + ? "Unable to add %s to database" + : "Unable to hash %s", path); + printf("%s\n", sha1_to_hex(sha1)); +} + +static void hash_stdin(const char *type, int write_object) +{ + unsigned char sha1[20]; + if (index_pipe(sha1, 0, type, write_object)) + die("Unable to add stdin to database"); + printf("%s\n", sha1_to_hex(sha1)); +} + +static const char hash_object_usage[] = +"git-hash-object [-t <type>] [-w] [--stdin] <file>..."; + +int main(int argc, char **argv) +{ + int i; + const char *type = blob_type; + int write_object = 0; + const char *prefix = NULL; + int prefix_length = -1; + int no_more_flags = 0; + + for (i = 1 ; i < argc; i++) { + if (!no_more_flags && argv[i][0] == '-') { + if (!strcmp(argv[i], "-t")) { + if (argc <= ++i) + usage(hash_object_usage); + type = argv[i]; + } + else if (!strcmp(argv[i], "-w")) { + if (prefix_length < 0) { + prefix = setup_git_directory(); + prefix_length = + prefix ? strlen(prefix) : 0; + } + write_object = 1; + } + else if (!strcmp(argv[i], "--")) { + no_more_flags = 1; + } + else if (!strcmp(argv[i], "--help")) + usage(hash_object_usage); + else if (!strcmp(argv[i], "--stdin")) { + hash_stdin(type, write_object); + } + else + usage(hash_object_usage); + } + else { + const char *arg = argv[i]; + if (0 <= prefix_length) + arg = prefix_filename(prefix, prefix_length, + arg); + hash_object(arg, type, write_object); + no_more_flags = 1; + } + } + return 0; +} diff --git a/help.c b/help.c new file mode 100644 index 0000000000..b6674635a2 --- /dev/null +++ b/help.c @@ -0,0 +1,233 @@ +/* + * builtin-help.c + * + * Builtin help-related commands (help, usage, version) + */ +#include "cache.h" +#include "builtin.h" +#include "exec_cmd.h" +#include "common-cmds.h" +#include <sys/ioctl.h> + +/* most GUI terminals set COLUMNS (although some don't export it) */ +static int term_columns(void) +{ + char *col_string = getenv("COLUMNS"); + int n_cols; + + if (col_string && (n_cols = atoi(col_string)) > 0) + return n_cols; + +#ifdef TIOCGWINSZ + { + struct winsize ws; + if (!ioctl(1, TIOCGWINSZ, &ws)) { + if (ws.ws_col) + return ws.ws_col; + } + } +#endif + + return 80; +} + +static void oom(void) +{ + fprintf(stderr, "git: out of memory\n"); + exit(1); +} + +static inline void mput_char(char c, unsigned int num) +{ + while(num--) + putchar(c); +} + +static struct cmdname { + size_t len; + char name[1]; +} **cmdname; +static int cmdname_alloc, cmdname_cnt; + +static void add_cmdname(const char *name, int len) +{ + struct cmdname *ent; + if (cmdname_alloc <= cmdname_cnt) { + cmdname_alloc = cmdname_alloc + 200; + cmdname = realloc(cmdname, cmdname_alloc * sizeof(*cmdname)); + if (!cmdname) + oom(); + } + ent = malloc(sizeof(*ent) + len); + if (!ent) + oom(); + ent->len = len; + memcpy(ent->name, name, len); + ent->name[len] = 0; + cmdname[cmdname_cnt++] = ent; +} + +static int cmdname_compare(const void *a_, const void *b_) +{ + struct cmdname *a = *(struct cmdname **)a_; + struct cmdname *b = *(struct cmdname **)b_; + return strcmp(a->name, b->name); +} + +static void pretty_print_string_list(struct cmdname **cmdname, int longest) +{ + int cols = 1, rows; + int space = longest + 1; /* min 1 SP between words */ + int max_cols = term_columns() - 1; /* don't print *on* the edge */ + int i, j; + + if (space < max_cols) + cols = max_cols / space; + rows = (cmdname_cnt + cols - 1) / cols; + + qsort(cmdname, cmdname_cnt, sizeof(*cmdname), cmdname_compare); + + for (i = 0; i < rows; i++) { + printf(" "); + + for (j = 0; j < cols; j++) { + int n = j * rows + i; + int size = space; + if (n >= cmdname_cnt) + break; + if (j == cols-1 || n + rows >= cmdname_cnt) + size = 1; + printf("%-*s", size, cmdname[n]->name); + } + putchar('\n'); + } +} + +static void list_commands(const char *exec_path, const char *pattern) +{ + unsigned int longest = 0; + char path[PATH_MAX]; + int dirlen; + DIR *dir = opendir(exec_path); + struct dirent *de; + + if (!dir) { + fprintf(stderr, "git: '%s': %s\n", exec_path, strerror(errno)); + exit(1); + } + + dirlen = strlen(exec_path); + if (PATH_MAX - 20 < dirlen) { + fprintf(stderr, "git: insanely long exec-path '%s'\n", + exec_path); + exit(1); + } + + memcpy(path, exec_path, dirlen); + path[dirlen++] = '/'; + + while ((de = readdir(dir)) != NULL) { + struct stat st; + int entlen; + + if (strncmp(de->d_name, "git-", 4)) + continue; + strcpy(path+dirlen, de->d_name); + if (stat(path, &st) || /* stat, not lstat */ + !S_ISREG(st.st_mode) || + !(st.st_mode & S_IXUSR)) + continue; + + entlen = strlen(de->d_name); + if (has_extension(de->d_name, ".exe")) + entlen -= 4; + + if (longest < entlen) + longest = entlen; + + add_cmdname(de->d_name + 4, entlen-4); + } + closedir(dir); + + printf("git commands available in '%s'\n", exec_path); + printf("----------------------------"); + mput_char('-', strlen(exec_path)); + putchar('\n'); + pretty_print_string_list(cmdname, longest - 4); + putchar('\n'); +} + +static void list_common_cmds_help(void) +{ + int i, longest = 0; + + for (i = 0; i < ARRAY_SIZE(common_cmds); i++) { + if (longest < strlen(common_cmds[i].name)) + longest = strlen(common_cmds[i].name); + } + + puts("The most commonly used git commands are:"); + for (i = 0; i < ARRAY_SIZE(common_cmds); i++) { + printf(" %s ", common_cmds[i].name); + mput_char(' ', longest - strlen(common_cmds[i].name)); + puts(common_cmds[i].help); + } + puts("(use 'git help -a' to get a list of all installed git commands)"); +} + +static void show_man_page(const char *git_cmd) +{ + const char *page; + + if (!strncmp(git_cmd, "git", 3)) + page = git_cmd; + else { + int page_len = strlen(git_cmd) + 4; + char *p = xmalloc(page_len + 1); + strcpy(p, "git-"); + strcpy(p + 4, git_cmd); + p[page_len] = 0; + page = p; + } + + execlp("man", "man", page, NULL); +} + +void help_unknown_cmd(const char *cmd) +{ + printf("git: '%s' is not a git-command\n\n", cmd); + list_common_cmds_help(); + exit(1); +} + +int cmd_version(int argc, const char **argv, const char *prefix) +{ + printf("git version %s\n", git_version_string); + return 0; +} + +int cmd_help(int argc, const char **argv, const char *prefix) +{ + const char *help_cmd = argc > 1 ? argv[1] : NULL; + const char *exec_path = git_exec_path(); + + if (!help_cmd) { + printf("usage: %s\n\n", git_usage_string); + list_common_cmds_help(); + exit(1); + } + + else if (!strcmp(help_cmd, "--all") || !strcmp(help_cmd, "-a")) { + printf("usage: %s\n\n", git_usage_string); + if(exec_path) + list_commands(exec_path, "git-*"); + exit(1); + } + + else + show_man_page(help_cmd); + + return 0; +} + + diff --git a/http-fetch.c b/http-fetch.c new file mode 100644 index 0000000000..9f790a08e5 --- /dev/null +++ b/http-fetch.c @@ -0,0 +1,1075 @@ +#include "cache.h" +#include "commit.h" +#include "pack.h" +#include "fetch.h" +#include "http.h" + +#define PREV_BUF_SIZE 4096 +#define RANGE_HEADER_SIZE 30 + +static int commits_on_stdin; + +static int got_alternates = -1; +static int corrupt_object_found; + +static struct curl_slist *no_pragma_header; + +struct alt_base +{ + const char *base; + int path_len; + int got_indices; + struct packed_git *packs; + struct alt_base *next; +}; + +static struct alt_base *alt; + +enum object_request_state { + WAITING, + ABORTED, + ACTIVE, + COMPLETE, +}; + +struct object_request +{ + unsigned char sha1[20]; + struct alt_base *repo; + char *url; + char filename[PATH_MAX]; + char tmpfile[PATH_MAX]; + int local; + enum object_request_state state; + CURLcode curl_result; + char errorstr[CURL_ERROR_SIZE]; + long http_code; + unsigned char real_sha1[20]; + SHA_CTX c; + z_stream stream; + int zret; + int rename; + struct active_request_slot *slot; + struct object_request *next; +}; + +struct alternates_request { + const char *base; + char *url; + struct buffer *buffer; + struct active_request_slot *slot; + int http_specific; +}; + +static struct object_request *object_queue_head; + +static size_t fwrite_sha1_file(void *ptr, size_t eltsize, size_t nmemb, + void *data) +{ + unsigned char expn[4096]; + size_t size = eltsize * nmemb; + int posn = 0; + struct object_request *obj_req = (struct object_request *)data; + do { + ssize_t retval = xwrite(obj_req->local, + (char *) ptr + posn, size - posn); + if (retval < 0) + return posn; + posn += retval; + } while (posn < size); + + obj_req->stream.avail_in = size; + obj_req->stream.next_in = ptr; + do { + obj_req->stream.next_out = expn; + obj_req->stream.avail_out = sizeof(expn); + obj_req->zret = inflate(&obj_req->stream, Z_SYNC_FLUSH); + SHA1_Update(&obj_req->c, expn, + sizeof(expn) - obj_req->stream.avail_out); + } while (obj_req->stream.avail_in && obj_req->zret == Z_OK); + data_received++; + return size; +} + +static int missing__target(int code, int result) +{ + return /* file:// URL -- do we ever use one??? */ + (result == CURLE_FILE_COULDNT_READ_FILE) || + /* http:// and https:// URL */ + (code == 404 && result == CURLE_HTTP_RETURNED_ERROR) || + /* ftp:// URL */ + (code == 550 && result == CURLE_FTP_COULDNT_RETR_FILE) + ; +} + +#define missing_target(a) missing__target((a)->http_code, (a)->curl_result) + +static void fetch_alternates(const char *base); + +static void process_object_response(void *callback_data); + +static void start_object_request(struct object_request *obj_req) +{ + char *hex = sha1_to_hex(obj_req->sha1); + char prevfile[PATH_MAX]; + char *url; + char *posn; + int prevlocal; + unsigned char prev_buf[PREV_BUF_SIZE]; + ssize_t prev_read = 0; + long prev_posn = 0; + char range[RANGE_HEADER_SIZE]; + struct curl_slist *range_header = NULL; + struct active_request_slot *slot; + + snprintf(prevfile, sizeof(prevfile), "%s.prev", obj_req->filename); + unlink(prevfile); + rename(obj_req->tmpfile, prevfile); + unlink(obj_req->tmpfile); + + if (obj_req->local != -1) + error("fd leakage in start: %d", obj_req->local); + obj_req->local = open(obj_req->tmpfile, + O_WRONLY | O_CREAT | O_EXCL, 0666); + /* This could have failed due to the "lazy directory creation"; + * try to mkdir the last path component. + */ + if (obj_req->local < 0 && errno == ENOENT) { + char *dir = strrchr(obj_req->tmpfile, '/'); + if (dir) { + *dir = 0; + mkdir(obj_req->tmpfile, 0777); + *dir = '/'; + } + obj_req->local = open(obj_req->tmpfile, + O_WRONLY | O_CREAT | O_EXCL, 0666); + } + + if (obj_req->local < 0) { + obj_req->state = ABORTED; + error("Couldn't create temporary file %s for %s: %s", + obj_req->tmpfile, obj_req->filename, strerror(errno)); + return; + } + + memset(&obj_req->stream, 0, sizeof(obj_req->stream)); + + inflateInit(&obj_req->stream); + + SHA1_Init(&obj_req->c); + + url = xmalloc(strlen(obj_req->repo->base) + 50); + obj_req->url = xmalloc(strlen(obj_req->repo->base) + 50); + strcpy(url, obj_req->repo->base); + posn = url + strlen(obj_req->repo->base); + strcpy(posn, "objects/"); + posn += 8; + memcpy(posn, hex, 2); + posn += 2; + *(posn++) = '/'; + strcpy(posn, hex + 2); + strcpy(obj_req->url, url); + + /* If a previous temp file is present, process what was already + fetched. */ + prevlocal = open(prevfile, O_RDONLY); + if (prevlocal != -1) { + do { + prev_read = xread(prevlocal, prev_buf, PREV_BUF_SIZE); + if (prev_read>0) { + if (fwrite_sha1_file(prev_buf, + 1, + prev_read, + obj_req) == prev_read) { + prev_posn += prev_read; + } else { + prev_read = -1; + } + } + } while (prev_read > 0); + close(prevlocal); + } + unlink(prevfile); + + /* Reset inflate/SHA1 if there was an error reading the previous temp + file; also rewind to the beginning of the local file. */ + if (prev_read == -1) { + memset(&obj_req->stream, 0, sizeof(obj_req->stream)); + inflateInit(&obj_req->stream); + SHA1_Init(&obj_req->c); + if (prev_posn>0) { + prev_posn = 0; + lseek(obj_req->local, SEEK_SET, 0); + ftruncate(obj_req->local, 0); + } + } + + slot = get_active_slot(); + slot->callback_func = process_object_response; + slot->callback_data = obj_req; + obj_req->slot = slot; + + curl_easy_setopt(slot->curl, CURLOPT_FILE, obj_req); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_sha1_file); + curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, obj_req->errorstr); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_pragma_header); + + /* If we have successfully processed data from a previous fetch + attempt, only fetch the data we don't already have. */ + if (prev_posn>0) { + if (get_verbosely) + fprintf(stderr, + "Resuming fetch of object %s at byte %ld\n", + hex, prev_posn); + sprintf(range, "Range: bytes=%ld-", prev_posn); + range_header = curl_slist_append(range_header, range); + curl_easy_setopt(slot->curl, + CURLOPT_HTTPHEADER, range_header); + } + + /* Try to get the request started, abort the request on error */ + obj_req->state = ACTIVE; + if (!start_active_slot(slot)) { + obj_req->state = ABORTED; + obj_req->slot = NULL; + close(obj_req->local); obj_req->local = -1; + free(obj_req->url); + return; + } +} + +static void finish_object_request(struct object_request *obj_req) +{ + struct stat st; + + fchmod(obj_req->local, 0444); + close(obj_req->local); obj_req->local = -1; + + if (obj_req->http_code == 416) { + fprintf(stderr, "Warning: requested range invalid; we may already have all the data.\n"); + } else if (obj_req->curl_result != CURLE_OK) { + if (stat(obj_req->tmpfile, &st) == 0) + if (st.st_size == 0) + unlink(obj_req->tmpfile); + return; + } + + inflateEnd(&obj_req->stream); + SHA1_Final(obj_req->real_sha1, &obj_req->c); + if (obj_req->zret != Z_STREAM_END) { + unlink(obj_req->tmpfile); + return; + } + if (hashcmp(obj_req->sha1, obj_req->real_sha1)) { + unlink(obj_req->tmpfile); + return; + } + obj_req->rename = + move_temp_to_file(obj_req->tmpfile, obj_req->filename); + + if (obj_req->rename == 0) + pull_say("got %s\n", sha1_to_hex(obj_req->sha1)); +} + +static void process_object_response(void *callback_data) +{ + struct object_request *obj_req = + (struct object_request *)callback_data; + + obj_req->curl_result = obj_req->slot->curl_result; + obj_req->http_code = obj_req->slot->http_code; + obj_req->slot = NULL; + obj_req->state = COMPLETE; + + /* Use alternates if necessary */ + if (missing_target(obj_req)) { + fetch_alternates(alt->base); + if (obj_req->repo->next != NULL) { + obj_req->repo = + obj_req->repo->next; + close(obj_req->local); + obj_req->local = -1; + start_object_request(obj_req); + return; + } + } + + finish_object_request(obj_req); +} + +static void release_object_request(struct object_request *obj_req) +{ + struct object_request *entry = object_queue_head; + + if (obj_req->local != -1) + error("fd leakage in release: %d", obj_req->local); + if (obj_req == object_queue_head) { + object_queue_head = obj_req->next; + } else { + while (entry->next != NULL && entry->next != obj_req) + entry = entry->next; + if (entry->next == obj_req) + entry->next = entry->next->next; + } + + free(obj_req->url); + free(obj_req); +} + +#ifdef USE_CURL_MULTI +void fill_active_slots(void) +{ + struct object_request *obj_req = object_queue_head; + struct active_request_slot *slot = active_queue_head; + int num_transfers; + + while (active_requests < max_requests && obj_req != NULL) { + if (obj_req->state == WAITING) { + if (has_sha1_file(obj_req->sha1)) + obj_req->state = COMPLETE; + else + start_object_request(obj_req); + curl_multi_perform(curlm, &num_transfers); + } + obj_req = obj_req->next; + } + + while (slot != NULL) { + if (!slot->in_use && slot->curl != NULL) { + curl_easy_cleanup(slot->curl); + slot->curl = NULL; + } + slot = slot->next; + } +} +#endif + +void prefetch(unsigned char *sha1) +{ + struct object_request *newreq; + struct object_request *tail; + char *filename = sha1_file_name(sha1); + + newreq = xmalloc(sizeof(*newreq)); + hashcpy(newreq->sha1, sha1); + newreq->repo = alt; + newreq->url = NULL; + newreq->local = -1; + newreq->state = WAITING; + 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) { + object_queue_head = newreq; + } else { + tail = object_queue_head; + while (tail->next != NULL) { + tail = tail->next; + } + tail->next = newreq; + } + +#ifdef USE_CURL_MULTI + fill_active_slots(); + step_active_slots(); +#endif +} + +static int fetch_index(struct alt_base *repo, unsigned char *sha1) +{ + char *hex = sha1_to_hex(sha1); + char *filename; + char *url; + char tmpfile[PATH_MAX]; + long prev_posn = 0; + char range[RANGE_HEADER_SIZE]; + struct curl_slist *range_header = NULL; + + FILE *indexfile; + struct active_request_slot *slot; + struct slot_results results; + + if (has_pack_index(sha1)) + return 0; + + if (get_verbosely) + fprintf(stderr, "Getting index for pack %s\n", hex); + + url = xmalloc(strlen(repo->base) + 64); + sprintf(url, "%s/objects/pack/pack-%s.idx", repo->base, hex); + + filename = sha1_pack_index_name(sha1); + snprintf(tmpfile, sizeof(tmpfile), "%s.temp", filename); + indexfile = fopen(tmpfile, "a"); + if (!indexfile) + return error("Unable to open local file %s for pack index", + filename); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_FILE, indexfile); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_pragma_header); + slot->local = indexfile; + + /* If there is data present from a previous transfer attempt, + resume where it left off */ + prev_posn = ftell(indexfile); + if (prev_posn>0) { + if (get_verbosely) + fprintf(stderr, + "Resuming fetch of index for pack %s at byte %ld\n", + hex, prev_posn); + sprintf(range, "Range: bytes=%ld-", prev_posn); + range_header = curl_slist_append(range_header, range); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, range_header); + } + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) { + fclose(indexfile); + return error("Unable to get pack index %s\n%s", url, + curl_errorstr); + } + } else { + fclose(indexfile); + return error("Unable to start request"); + } + + fclose(indexfile); + + return move_temp_to_file(tmpfile, filename); +} + +static int setup_index(struct alt_base *repo, unsigned char *sha1) +{ + struct packed_git *new_pack; + if (has_pack_file(sha1)) + return 0; /* don't list this as something we can get */ + + if (fetch_index(repo, sha1)) + return -1; + + new_pack = parse_pack_index(sha1); + new_pack->next = repo->packs; + repo->packs = new_pack; + return 0; +} + +static void process_alternates_response(void *callback_data) +{ + struct alternates_request *alt_req = + (struct alternates_request *)callback_data; + struct active_request_slot *slot = alt_req->slot; + struct alt_base *tail = alt; + const char *base = alt_req->base; + static const char null_byte = '\0'; + char *data; + int i = 0; + + if (alt_req->http_specific) { + if (slot->curl_result != CURLE_OK || + !alt_req->buffer->posn) { + + /* Try reusing the slot to get non-http alternates */ + alt_req->http_specific = 0; + sprintf(alt_req->url, "%s/objects/info/alternates", + base); + curl_easy_setopt(slot->curl, CURLOPT_URL, + alt_req->url); + active_requests++; + slot->in_use = 1; + if (slot->finished != NULL) + (*slot->finished) = 0; + if (!start_active_slot(slot)) { + got_alternates = -1; + slot->in_use = 0; + if (slot->finished != NULL) + (*slot->finished) = 1; + } + return; + } + } else if (slot->curl_result != CURLE_OK) { + if (!missing_target(slot)) { + got_alternates = -1; + return; + } + } + + fwrite_buffer(&null_byte, 1, 1, alt_req->buffer); + alt_req->buffer->posn--; + data = alt_req->buffer->buffer; + + while (i < alt_req->buffer->posn) { + int posn = i; + while (posn < alt_req->buffer->posn && data[posn] != '\n') + posn++; + if (data[posn] == '\n') { + int okay = 0; + int serverlen = 0; + struct alt_base *newalt; + char *target = NULL; + char *path; + if (data[i] == '/') { + /* This counts + * http://git.host/pub/scm/linux.git/ + * -----------here^ + * so memcpy(dst, base, serverlen) will + * copy up to "...git.host". + */ + const char *colon_ss = strstr(base,"://"); + if (colon_ss) { + serverlen = (strchr(colon_ss + 3, '/') + - base); + okay = 1; + } + } else if (!memcmp(data + i, "../", 3)) { + /* Relative URL; chop the corresponding + * number of subpath from base (and ../ + * from data), and concatenate the result. + * + * The code first drops ../ from data, and + * then drops one ../ from data and one path + * from base. IOW, one extra ../ is dropped + * from data than path is dropped from base. + * + * This is not wrong. The alternate in + * http://git.host/pub/scm/linux.git/ + * to borrow from + * http://git.host/pub/scm/linus.git/ + * is ../../linus.git/objects/. You need + * two ../../ to borrow from your direct + * neighbour. + */ + i += 3; + serverlen = strlen(base); + while (i + 2 < posn && + !memcmp(data + i, "../", 3)) { + do { + serverlen--; + } while (serverlen && + base[serverlen - 1] != '/'); + i += 3; + } + /* If the server got removed, give up. */ + okay = strchr(base, ':') - base + 3 < + serverlen; + } else if (alt_req->http_specific) { + char *colon = strchr(data + i, ':'); + char *slash = strchr(data + i, '/'); + if (colon && slash && colon < data + posn && + slash < data + posn && colon < slash) { + okay = 1; + } + } + /* skip "objects\n" at end */ + if (okay) { + target = xmalloc(serverlen + posn - i - 6); + memcpy(target, base, serverlen); + memcpy(target + serverlen, data + i, + posn - i - 7); + target[serverlen + posn - i - 7] = 0; + if (get_verbosely) + fprintf(stderr, + "Also look at %s\n", target); + newalt = xmalloc(sizeof(*newalt)); + newalt->next = NULL; + newalt->base = target; + newalt->got_indices = 0; + newalt->packs = NULL; + path = strstr(target, "//"); + if (path) { + path = strchr(path+2, '/'); + if (path) + newalt->path_len = strlen(path); + } + + while (tail->next != NULL) + tail = tail->next; + tail->next = newalt; + } + } + i = posn + 1; + } + + got_alternates = 1; +} + +static void fetch_alternates(const char *base) +{ + struct buffer buffer; + char *url; + char *data; + struct active_request_slot *slot; + struct alternates_request alt_req; + + /* If another request has already started fetching alternates, + wait for them to arrive and return to processing this request's + curl message */ +#ifdef USE_CURL_MULTI + while (got_alternates == 0) { + step_active_slots(); + } +#endif + + /* Nothing to do if they've already been fetched */ + if (got_alternates == 1) + return; + + /* Start the fetch */ + got_alternates = 0; + + data = xmalloc(4096); + buffer.size = 4096; + buffer.posn = 0; + buffer.buffer = data; + + if (get_verbosely) + fprintf(stderr, "Getting alternates list for %s\n", base); + + url = xmalloc(strlen(base) + 31); + sprintf(url, "%s/objects/info/http-alternates", base); + + /* Use a callback to process the result, since another request + may fail and need to have alternates loaded before continuing */ + slot = get_active_slot(); + slot->callback_func = process_alternates_response; + slot->callback_data = &alt_req; + + curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + + alt_req.base = base; + alt_req.url = url; + alt_req.buffer = &buffer; + alt_req.http_specific = 1; + alt_req.slot = slot; + + if (start_active_slot(slot)) + run_active_slot(slot); + else + got_alternates = -1; + + free(data); + free(url); +} + +static int fetch_indices(struct alt_base *repo) +{ + unsigned char sha1[20]; + char *url; + struct buffer buffer; + char *data; + int i = 0; + + struct active_request_slot *slot; + struct slot_results results; + + if (repo->got_indices) + return 0; + + data = xmalloc(4096); + buffer.size = 4096; + buffer.posn = 0; + buffer.buffer = data; + + if (get_verbosely) + fprintf(stderr, "Getting pack list for %s\n", repo->base); + + url = xmalloc(strlen(repo->base) + 21); + sprintf(url, "%s/objects/info/packs", repo->base); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL); + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) { + if (missing_target(&results)) { + repo->got_indices = 1; + free(buffer.buffer); + return 0; + } else { + repo->got_indices = 0; + free(buffer.buffer); + return error("%s", curl_errorstr); + } + } + } else { + repo->got_indices = 0; + free(buffer.buffer); + return error("Unable to start request"); + } + + data = buffer.buffer; + while (i < buffer.posn) { + switch (data[i]) { + case 'P': + i++; + if (i + 52 <= buffer.posn && + !strncmp(data + i, " pack-", 6) && + !strncmp(data + i + 46, ".pack\n", 6)) { + get_sha1_hex(data + i + 6, sha1); + setup_index(repo, sha1); + i += 51; + break; + } + default: + while (i < buffer.posn && data[i] != '\n') + i++; + } + i++; + } + + free(buffer.buffer); + repo->got_indices = 1; + return 0; +} + +static int fetch_pack(struct alt_base *repo, unsigned char *sha1) +{ + char *url; + struct packed_git *target; + struct packed_git **lst; + FILE *packfile; + char *filename; + char tmpfile[PATH_MAX]; + int ret; + long prev_posn = 0; + char range[RANGE_HEADER_SIZE]; + struct curl_slist *range_header = NULL; + + struct active_request_slot *slot; + struct slot_results results; + + if (fetch_indices(repo)) + return -1; + target = find_sha1_pack(sha1, repo->packs); + if (!target) + return -1; + + if (get_verbosely) { + fprintf(stderr, "Getting pack %s\n", + sha1_to_hex(target->sha1)); + fprintf(stderr, " which contains %s\n", + sha1_to_hex(sha1)); + } + + url = xmalloc(strlen(repo->base) + 65); + sprintf(url, "%s/objects/pack/pack-%s.pack", + repo->base, sha1_to_hex(target->sha1)); + + filename = sha1_pack_name(target->sha1); + snprintf(tmpfile, sizeof(tmpfile), "%s.temp", filename); + packfile = fopen(tmpfile, "a"); + if (!packfile) + return error("Unable to open local file %s for pack", + filename); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_FILE, packfile); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_pragma_header); + slot->local = packfile; + + /* If there is data present from a previous transfer attempt, + resume where it left off */ + prev_posn = ftell(packfile); + if (prev_posn>0) { + if (get_verbosely) + fprintf(stderr, + "Resuming fetch of pack %s at byte %ld\n", + sha1_to_hex(target->sha1), prev_posn); + sprintf(range, "Range: bytes=%ld-", prev_posn); + range_header = curl_slist_append(range_header, range); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, range_header); + } + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) { + fclose(packfile); + return error("Unable to get pack file %s\n%s", url, + curl_errorstr); + } + } else { + fclose(packfile); + return error("Unable to start request"); + } + + target->pack_size = ftell(packfile); + fclose(packfile); + + ret = move_temp_to_file(tmpfile, filename); + if (ret) + return ret; + + lst = &repo->packs; + while (*lst != target) + lst = &((*lst)->next); + *lst = (*lst)->next; + + if (verify_pack(target, 0)) + return -1; + install_packed_git(target); + + return 0; +} + +static void abort_object_request(struct object_request *obj_req) +{ + if (obj_req->local >= 0) { + close(obj_req->local); + obj_req->local = -1; + } + unlink(obj_req->tmpfile); + if (obj_req->slot) { + release_active_slot(obj_req->slot); + obj_req->slot = NULL; + } + release_object_request(obj_req); +} + +static int fetch_object(struct alt_base *repo, unsigned char *sha1) +{ + char *hex = sha1_to_hex(sha1); + int ret = 0; + struct object_request *obj_req = object_queue_head; + + while (obj_req != NULL && hashcmp(obj_req->sha1, sha1)) + obj_req = obj_req->next; + if (obj_req == NULL) + return error("Couldn't find request for %s in the queue", hex); + + if (has_sha1_file(obj_req->sha1)) { + abort_object_request(obj_req); + return 0; + } + +#ifdef USE_CURL_MULTI + while (obj_req->state == WAITING) { + step_active_slots(); + } +#else + start_object_request(obj_req); +#endif + + while (obj_req->state == ACTIVE) { + run_active_slot(obj_req->slot); + } + if (obj_req->local != -1) { + close(obj_req->local); obj_req->local = -1; + } + + if (obj_req->state == ABORTED) { + ret = error("Request for %s aborted", hex); + } else if (obj_req->curl_result != CURLE_OK && + obj_req->http_code != 416) { + if (missing_target(obj_req)) + ret = -1; /* Be silent, it is probably in a pack. */ + else + ret = error("%s (curl_result = %d, http_code = %ld, sha1 = %s)", + obj_req->errorstr, obj_req->curl_result, + obj_req->http_code, hex); + } else if (obj_req->zret != Z_STREAM_END) { + corrupt_object_found++; + ret = error("File %s (%s) corrupt", hex, obj_req->url); + } else if (hashcmp(obj_req->sha1, obj_req->real_sha1)) { + ret = error("File %s has bad hash", hex); + } else if (obj_req->rename < 0) { + ret = error("unable to write sha1 filename %s", + obj_req->filename); + } + + release_object_request(obj_req); + return ret; +} + +int fetch(unsigned char *sha1) +{ + struct alt_base *altbase = alt; + + if (!fetch_object(altbase, sha1)) + return 0; + while (altbase) { + if (!fetch_pack(altbase, sha1)) + return 0; + fetch_alternates(alt->base); + altbase = altbase->next; + } + return error("Unable to find %s under %s", sha1_to_hex(sha1), + alt->base); +} + +static inline int needs_quote(int ch) +{ + if (((ch >= 'A') && (ch <= 'Z')) + || ((ch >= 'a') && (ch <= 'z')) + || ((ch >= '0') && (ch <= '9')) + || (ch == '/') + || (ch == '-') + || (ch == '.')) + return 0; + return 1; +} + +static inline int hex(int v) +{ + if (v < 10) return '0' + v; + else return 'A' + v - 10; +} + +static char *quote_ref_url(const char *base, const char *ref) +{ + const char *cp; + char *dp, *qref; + int len, baselen, ch; + + baselen = strlen(base); + len = baselen + 6; /* "refs/" + NUL */ + for (cp = ref; (ch = *cp) != 0; cp++, len++) + if (needs_quote(ch)) + len += 2; /* extra two hex plus replacement % */ + qref = xmalloc(len); + memcpy(qref, base, baselen); + memcpy(qref + baselen, "refs/", 5); + for (cp = ref, dp = qref + baselen + 5; (ch = *cp) != 0; cp++) { + if (needs_quote(ch)) { + *dp++ = '%'; + *dp++ = hex((ch >> 4) & 0xF); + *dp++ = hex(ch & 0xF); + } + else + *dp++ = ch; + } + *dp = 0; + + return qref; +} + +int fetch_ref(char *ref, unsigned char *sha1) +{ + char *url; + char hex[42]; + struct buffer buffer; + const char *base = alt->base; + struct active_request_slot *slot; + struct slot_results results; + buffer.size = 41; + buffer.posn = 0; + buffer.buffer = hex; + hex[41] = '\0'; + + url = quote_ref_url(base, ref); + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) + return error("Couldn't get %s for %s\n%s", + url, ref, curl_errorstr); + } else { + return error("Unable to start request"); + } + + hex[40] = '\0'; + get_sha1_hex(hex, sha1); + return 0; +} + +int main(int argc, const char **argv) +{ + int commits; + const char **write_ref = NULL; + char **commit_id; + const char *url; + char *path; + int arg = 1; + int rc = 0; + + setup_git_directory(); + git_config(git_default_config); + + while (arg < argc && argv[arg][0] == '-') { + if (argv[arg][1] == 't') { + get_tree = 1; + } else if (argv[arg][1] == 'c') { + get_history = 1; + } else if (argv[arg][1] == 'a') { + get_all = 1; + get_tree = 1; + get_history = 1; + } else if (argv[arg][1] == 'v') { + get_verbosely = 1; + } else if (argv[arg][1] == 'w') { + write_ref = &argv[arg + 1]; + arg++; + } else if (!strcmp(argv[arg], "--recover")) { + get_recover = 1; + } else if (!strcmp(argv[arg], "--stdin")) { + commits_on_stdin = 1; + } + arg++; + } + if (argc < arg + 2 - commits_on_stdin) { + usage("git-http-fetch [-c] [-t] [-a] [-v] [--recover] [-w ref] [--stdin] commit-id url"); + return 1; + } + if (commits_on_stdin) { + commits = pull_targets_stdin(&commit_id, &write_ref); + } else { + commit_id = (char **) &argv[arg++]; + commits = 1; + } + url = argv[arg]; + + http_init(); + + no_pragma_header = curl_slist_append(no_pragma_header, "Pragma:"); + + alt = xmalloc(sizeof(*alt)); + alt->base = url; + alt->got_indices = 0; + alt->packs = NULL; + alt->next = NULL; + path = strstr(url, "//"); + if (path) { + path = strchr(path+2, '/'); + if (path) + alt->path_len = strlen(path); + } + + if (pull(commits, commit_id, write_ref, url)) + rc = 1; + + http_cleanup(); + + curl_slist_free_all(no_pragma_header); + + if (commits_on_stdin) + pull_targets_free(commits, commit_id, write_ref); + + if (corrupt_object_found) { + fprintf(stderr, +"Some loose object were found to be corrupt, but they might be just\n" +"a false '404 Not Found' error message sent with incorrect HTTP\n" +"status code. Suggest running git-fsck.\n"); + } + return rc; +} diff --git a/http-push.c b/http-push.c new file mode 100644 index 0000000000..b128c0146c --- /dev/null +++ b/http-push.c @@ -0,0 +1,2550 @@ +#include "cache.h" +#include "commit.h" +#include "pack.h" +#include "fetch.h" +#include "tag.h" +#include "blob.h" +#include "http.h" +#include "refs.h" +#include "diff.h" +#include "revision.h" +#include "exec_cmd.h" + +#include <expat.h> + +static const char http_push_usage[] = +"git-http-push [--all] [--force] [--verbose] <remote> [<head>...]\n"; + +#ifndef XML_STATUS_OK +enum XML_Status { + XML_STATUS_OK = 1, + XML_STATUS_ERROR = 0 +}; +#define XML_STATUS_OK 1 +#define XML_STATUS_ERROR 0 +#endif + +#define PREV_BUF_SIZE 4096 +#define RANGE_HEADER_SIZE 30 + +/* DAV methods */ +#define DAV_LOCK "LOCK" +#define DAV_MKCOL "MKCOL" +#define DAV_MOVE "MOVE" +#define DAV_PROPFIND "PROPFIND" +#define DAV_PUT "PUT" +#define DAV_UNLOCK "UNLOCK" +#define DAV_DELETE "DELETE" + +/* DAV lock flags */ +#define DAV_PROP_LOCKWR (1u << 0) +#define DAV_PROP_LOCKEX (1u << 1) +#define DAV_LOCK_OK (1u << 2) + +/* DAV XML properties */ +#define DAV_CTX_LOCKENTRY ".multistatus.response.propstat.prop.supportedlock.lockentry" +#define DAV_CTX_LOCKTYPE_WRITE ".multistatus.response.propstat.prop.supportedlock.lockentry.locktype.write" +#define DAV_CTX_LOCKTYPE_EXCLUSIVE ".multistatus.response.propstat.prop.supportedlock.lockentry.lockscope.exclusive" +#define DAV_ACTIVELOCK_OWNER ".prop.lockdiscovery.activelock.owner.href" +#define DAV_ACTIVELOCK_TIMEOUT ".prop.lockdiscovery.activelock.timeout" +#define DAV_ACTIVELOCK_TOKEN ".prop.lockdiscovery.activelock.locktoken.href" +#define DAV_PROPFIND_RESP ".multistatus.response" +#define DAV_PROPFIND_NAME ".multistatus.response.href" +#define DAV_PROPFIND_COLLECTION ".multistatus.response.propstat.prop.resourcetype.collection" + +/* DAV request body templates */ +#define PROPFIND_SUPPORTEDLOCK_REQUEST "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop xmlns:R=\"%s\">\n<D:supportedlock/>\n</D:prop>\n</D:propfind>" +#define PROPFIND_ALL_REQUEST "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/>\n</D:propfind>" +#define LOCK_REQUEST "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:lockinfo xmlns:D=\"DAV:\">\n<D:lockscope><D:exclusive/></D:lockscope>\n<D:locktype><D:write/></D:locktype>\n<D:owner>\n<D:href>mailto:%s</D:href>\n</D:owner>\n</D:lockinfo>" + +#define LOCK_TIME 600 +#define LOCK_REFRESH 30 + +/* bits #0-15 in revision.h */ + +#define LOCAL (1u<<16) +#define REMOTE (1u<<17) +#define FETCHING (1u<<18) +#define PUSHING (1u<<19) + +/* We allow "recursive" symbolic refs. Only within reason, though */ +#define MAXDEPTH 5 + +static int pushing; +static int aborted; +static signed char remote_dir_exists[256]; + +static struct curl_slist *no_pragma_header; +static struct curl_slist *default_headers; + +static int push_verbosely; +static int push_all; +static int force_all; + +static struct object_list *objects; + +struct repo +{ + char *url; + int path_len; + int has_info_refs; + int can_update_info_refs; + int has_info_packs; + struct packed_git *packs; + struct remote_lock *locks; +}; + +static struct repo *remote; + +enum transfer_state { + NEED_FETCH, + RUN_FETCH_LOOSE, + RUN_FETCH_PACKED, + NEED_PUSH, + RUN_MKCOL, + RUN_PUT, + RUN_MOVE, + ABORTED, + COMPLETE, +}; + +struct transfer_request +{ + struct object *obj; + char *url; + char *dest; + struct remote_lock *lock; + struct curl_slist *headers; + struct buffer buffer; + char filename[PATH_MAX]; + char tmpfile[PATH_MAX]; + int local_fileno; + FILE *local_stream; + enum transfer_state state; + CURLcode curl_result; + char errorstr[CURL_ERROR_SIZE]; + long http_code; + unsigned char real_sha1[20]; + SHA_CTX c; + z_stream stream; + int zret; + int rename; + void *userData; + struct active_request_slot *slot; + struct transfer_request *next; +}; + +static struct transfer_request *request_queue_head; + +struct xml_ctx +{ + char *name; + int len; + char *cdata; + void (*userFunc)(struct xml_ctx *ctx, int tag_closed); + void *userData; +}; + +struct remote_lock +{ + char *url; + char *owner; + char *token; + time_t start_time; + long timeout; + int refreshing; + struct remote_lock *next; +}; + +/* Flags that control remote_ls processing */ +#define PROCESS_FILES (1u << 0) +#define PROCESS_DIRS (1u << 1) +#define RECURSIVE (1u << 2) + +/* Flags that remote_ls passes to callback functions */ +#define IS_DIR (1u << 0) + +struct remote_ls_ctx +{ + char *path; + void (*userFunc)(struct remote_ls_ctx *ls); + void *userData; + int flags; + char *dentry_name; + int dentry_flags; + struct remote_ls_ctx *parent; +}; + +static void finish_request(struct transfer_request *request); +static void release_request(struct transfer_request *request); + +static void process_response(void *callback_data) +{ + struct transfer_request *request = + (struct transfer_request *)callback_data; + + finish_request(request); +} + +#ifdef USE_CURL_MULTI +static size_t fwrite_sha1_file(void *ptr, size_t eltsize, size_t nmemb, + void *data) +{ + unsigned char expn[4096]; + size_t size = eltsize * nmemb; + int posn = 0; + struct transfer_request *request = (struct transfer_request *)data; + do { + ssize_t retval = xwrite(request->local_fileno, + (char *) ptr + posn, size - posn); + if (retval < 0) + return posn; + posn += retval; + } while (posn < size); + + request->stream.avail_in = size; + request->stream.next_in = ptr; + do { + request->stream.next_out = expn; + request->stream.avail_out = sizeof(expn); + request->zret = inflate(&request->stream, Z_SYNC_FLUSH); + SHA1_Update(&request->c, expn, + sizeof(expn) - request->stream.avail_out); + } while (request->stream.avail_in && request->zret == Z_OK); + data_received++; + return size; +} + +static void start_fetch_loose(struct transfer_request *request) +{ + char *hex = sha1_to_hex(request->obj->sha1); + char *filename; + char prevfile[PATH_MAX]; + char *url; + char *posn; + int prevlocal; + unsigned char prev_buf[PREV_BUF_SIZE]; + ssize_t prev_read = 0; + long prev_posn = 0; + char range[RANGE_HEADER_SIZE]; + struct curl_slist *range_header = NULL; + struct active_request_slot *slot; + + filename = sha1_file_name(request->obj->sha1); + snprintf(request->filename, sizeof(request->filename), "%s", filename); + snprintf(request->tmpfile, sizeof(request->tmpfile), + "%s.temp", filename); + + snprintf(prevfile, sizeof(prevfile), "%s.prev", request->filename); + unlink(prevfile); + rename(request->tmpfile, prevfile); + unlink(request->tmpfile); + + if (request->local_fileno != -1) + error("fd leakage in start: %d", request->local_fileno); + request->local_fileno = open(request->tmpfile, + O_WRONLY | O_CREAT | O_EXCL, 0666); + /* This could have failed due to the "lazy directory creation"; + * try to mkdir the last path component. + */ + if (request->local_fileno < 0 && errno == ENOENT) { + char *dir = strrchr(request->tmpfile, '/'); + if (dir) { + *dir = 0; + mkdir(request->tmpfile, 0777); + *dir = '/'; + } + request->local_fileno = open(request->tmpfile, + O_WRONLY | O_CREAT | O_EXCL, 0666); + } + + if (request->local_fileno < 0) { + request->state = ABORTED; + error("Couldn't create temporary file %s for %s: %s", + request->tmpfile, request->filename, strerror(errno)); + return; + } + + memset(&request->stream, 0, sizeof(request->stream)); + + inflateInit(&request->stream); + + SHA1_Init(&request->c); + + url = xmalloc(strlen(remote->url) + 50); + request->url = xmalloc(strlen(remote->url) + 50); + strcpy(url, remote->url); + posn = url + strlen(remote->url); + strcpy(posn, "objects/"); + posn += 8; + memcpy(posn, hex, 2); + posn += 2; + *(posn++) = '/'; + strcpy(posn, hex + 2); + strcpy(request->url, url); + + /* If a previous temp file is present, process what was already + fetched. */ + prevlocal = open(prevfile, O_RDONLY); + if (prevlocal != -1) { + do { + prev_read = xread(prevlocal, prev_buf, PREV_BUF_SIZE); + if (prev_read>0) { + if (fwrite_sha1_file(prev_buf, + 1, + prev_read, + request) == prev_read) { + prev_posn += prev_read; + } else { + prev_read = -1; + } + } + } while (prev_read > 0); + close(prevlocal); + } + unlink(prevfile); + + /* Reset inflate/SHA1 if there was an error reading the previous temp + file; also rewind to the beginning of the local file. */ + if (prev_read == -1) { + memset(&request->stream, 0, sizeof(request->stream)); + inflateInit(&request->stream); + SHA1_Init(&request->c); + if (prev_posn>0) { + prev_posn = 0; + lseek(request->local_fileno, SEEK_SET, 0); + ftruncate(request->local_fileno, 0); + } + } + + slot = get_active_slot(); + slot->callback_func = process_response; + slot->callback_data = request; + request->slot = slot; + + curl_easy_setopt(slot->curl, CURLOPT_FILE, request); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_sha1_file); + curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, request->errorstr); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_pragma_header); + + /* If we have successfully processed data from a previous fetch + attempt, only fetch the data we don't already have. */ + if (prev_posn>0) { + if (push_verbosely) + fprintf(stderr, + "Resuming fetch of object %s at byte %ld\n", + hex, prev_posn); + sprintf(range, "Range: bytes=%ld-", prev_posn); + range_header = curl_slist_append(range_header, range); + curl_easy_setopt(slot->curl, + CURLOPT_HTTPHEADER, range_header); + } + + /* Try to get the request started, abort the request on error */ + request->state = RUN_FETCH_LOOSE; + if (!start_active_slot(slot)) { + fprintf(stderr, "Unable to start GET request\n"); + remote->can_update_info_refs = 0; + release_request(request); + } +} + +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; + struct packed_git *target; + FILE *packfile; + char *filename; + long prev_posn = 0; + char range[RANGE_HEADER_SIZE]; + struct curl_slist *range_header = NULL; + + struct transfer_request *check_request = request_queue_head; + struct active_request_slot *slot; + + target = find_sha1_pack(request->obj->sha1, remote->packs); + if (!target) { + fprintf(stderr, "Unable to fetch %s, will not be able to update server info refs\n", sha1_to_hex(request->obj->sha1)); + remote->can_update_info_refs = 0; + release_request(request); + return; + } + + fprintf(stderr, "Fetching pack %s\n", sha1_to_hex(target->sha1)); + fprintf(stderr, " which contains %s\n", sha1_to_hex(request->obj->sha1)); + + filename = sha1_pack_name(target->sha1); + snprintf(request->filename, sizeof(request->filename), "%s", filename); + snprintf(request->tmpfile, sizeof(request->tmpfile), + "%s.temp", filename); + + url = xmalloc(strlen(remote->url) + 64); + sprintf(url, "%sobjects/pack/pack-%s.pack", + remote->url, sha1_to_hex(target->sha1)); + + /* Make sure there isn't another open request for this pack */ + while (check_request) { + if (check_request->state == RUN_FETCH_PACKED && + !strcmp(check_request->url, url)) { + free(url); + release_request(request); + return; + } + check_request = check_request->next; + } + + packfile = fopen(request->tmpfile, "a"); + if (!packfile) { + fprintf(stderr, "Unable to open local file %s for pack", + filename); + remote->can_update_info_refs = 0; + free(url); + return; + } + + slot = get_active_slot(); + slot->callback_func = process_response; + slot->callback_data = request; + request->slot = slot; + request->local_stream = packfile; + request->userData = target; + + request->url = url; + curl_easy_setopt(slot->curl, CURLOPT_FILE, packfile); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_pragma_header); + slot->local = packfile; + + /* If there is data present from a previous transfer attempt, + resume where it left off */ + prev_posn = ftell(packfile); + if (prev_posn>0) { + if (push_verbosely) + fprintf(stderr, + "Resuming fetch of pack %s at byte %ld\n", + sha1_to_hex(target->sha1), prev_posn); + sprintf(range, "Range: bytes=%ld-", prev_posn); + range_header = curl_slist_append(range_header, range); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, range_header); + } + + /* Try to get the request started, abort the request on error */ + request->state = RUN_FETCH_PACKED; + if (!start_active_slot(slot)) { + fprintf(stderr, "Unable to start GET request\n"); + remote->can_update_info_refs = 0; + release_request(request); + } +} + +static void start_put(struct transfer_request *request) +{ + char *hex = sha1_to_hex(request->obj->sha1); + struct active_request_slot *slot; + char *posn; + char type[20]; + char hdr[50]; + void *unpacked; + unsigned long len; + int hdrlen; + ssize_t size; + z_stream stream; + + unpacked = read_sha1_file(request->obj->sha1, type, &len); + hdrlen = sprintf(hdr, "%s %lu", type, len) + 1; + + /* Set it up */ + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, zlib_compression_level); + size = deflateBound(&stream, len + hdrlen); + request->buffer.buffer = xmalloc(size); + + /* Compress it */ + stream.next_out = request->buffer.buffer; + stream.avail_out = size; + + /* First header.. */ + stream.next_in = (void *)hdr; + stream.avail_in = hdrlen; + while (deflate(&stream, 0) == Z_OK) + /* nothing */; + + /* Then the data itself.. */ + stream.next_in = unpacked; + stream.avail_in = len; + while (deflate(&stream, Z_FINISH) == Z_OK) + /* nothing */; + deflateEnd(&stream); + free(unpacked); + + request->buffer.size = stream.total_out; + request->buffer.posn = 0; + + request->url = xmalloc(strlen(remote->url) + + strlen(request->lock->token) + 51); + strcpy(request->url, remote->url); + posn = request->url + strlen(remote->url); + strcpy(posn, "objects/"); + posn += 8; + memcpy(posn, hex, 2); + posn += 2; + *(posn++) = '/'; + strcpy(posn, hex + 2); + request->dest = xmalloc(strlen(request->url) + 14); + sprintf(request->dest, "Destination: %s", request->url); + posn += 38; + *(posn++) = '_'; + strcpy(posn, request->lock->token); + + slot = get_active_slot(); + slot->callback_func = process_response; + slot->callback_data = request; + curl_easy_setopt(slot->curl, CURLOPT_INFILE, &request->buffer); + curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, request->buffer.size); + curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PUT); + curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1); + curl_easy_setopt(slot->curl, CURLOPT_PUT, 1); + curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 0); + curl_easy_setopt(slot->curl, CURLOPT_URL, request->url); + + if (start_active_slot(slot)) { + request->slot = slot; + request->state = RUN_PUT; + } else { + request->state = ABORTED; + free(request->url); + request->url = NULL; + } +} + +static void start_move(struct transfer_request *request) +{ + struct active_request_slot *slot; + struct curl_slist *dav_headers = NULL; + + 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_CUSTOMREQUEST, DAV_MOVE); + dav_headers = curl_slist_append(dav_headers, request->dest); + dav_headers = curl_slist_append(dav_headers, "Overwrite: T"); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null); + curl_easy_setopt(slot->curl, CURLOPT_URL, request->url); + + if (start_active_slot(slot)) { + request->slot = slot; + request->state = RUN_MOVE; + } else { + request->state = ABORTED; + free(request->url); + request->url = NULL; + } +} + +static int refresh_lock(struct remote_lock *lock) +{ + struct active_request_slot *slot; + struct slot_results results; + char *if_header; + char timeout_header[25]; + struct curl_slist *dav_headers = NULL; + int rc = 0; + + lock->refreshing = 1; + + if_header = xmalloc(strlen(lock->token) + 25); + sprintf(if_header, "If: (<opaquelocktoken:%s>)", lock->token); + sprintf(timeout_header, "Timeout: Second-%ld", lock->timeout); + dav_headers = curl_slist_append(dav_headers, if_header); + dav_headers = curl_slist_append(dav_headers, timeout_header); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null); + curl_easy_setopt(slot->curl, CURLOPT_URL, lock->url); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_LOCK); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers); + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) { + fprintf(stderr, "LOCK HTTP error %ld\n", + results.http_code); + } else { + lock->start_time = time(NULL); + rc = 1; + } + } + + lock->refreshing = 0; + curl_slist_free_all(dav_headers); + free(if_header); + + return rc; +} + +static void check_locks(void) +{ + struct remote_lock *lock = remote->locks; + time_t current_time = time(NULL); + int time_remaining; + + while (lock) { + time_remaining = lock->start_time + lock->timeout - + current_time; + if (!lock->refreshing && time_remaining < LOCK_REFRESH) { + if (!refresh_lock(lock)) { + fprintf(stderr, + "Unable to refresh lock for %s\n", + lock->url); + aborted = 1; + return; + } + } + lock = lock->next; + } +} + +static void release_request(struct transfer_request *request) +{ + struct transfer_request *entry = request_queue_head; + + if (request == request_queue_head) { + request_queue_head = request->next; + } else { + while (entry->next != NULL && entry->next != request) + entry = entry->next; + if (entry->next == request) + entry->next = entry->next->next; + } + + if (request->local_fileno != -1) + close(request->local_fileno); + if (request->local_stream) + fclose(request->local_stream); + if (request->url != NULL) + free(request->url); + free(request); +} + +static void finish_request(struct transfer_request *request) +{ + struct stat st; + struct packed_git *target; + struct packed_git **lst; + + request->curl_result = request->slot->curl_result; + request->http_code = request->slot->http_code; + request->slot = NULL; + + /* Keep locks active */ + check_locks(); + + if (request->headers != NULL) + curl_slist_free_all(request->headers); + + /* URL is reused for MOVE after PUT */ + if (request->state != RUN_PUT) { + free(request->url); + request->url = NULL; + } + + if (request->state == RUN_MKCOL) { + if (request->curl_result == CURLE_OK || + request->http_code == 405) { + remote_dir_exists[request->obj->sha1[0]] = 1; + start_put(request); + } else { + fprintf(stderr, "MKCOL %s failed, aborting (%d/%ld)\n", + sha1_to_hex(request->obj->sha1), + request->curl_result, request->http_code); + request->state = ABORTED; + aborted = 1; + } + } else if (request->state == RUN_PUT) { + if (request->curl_result == CURLE_OK) { + start_move(request); + } else { + fprintf(stderr, "PUT %s failed, aborting (%d/%ld)\n", + sha1_to_hex(request->obj->sha1), + request->curl_result, request->http_code); + request->state = ABORTED; + aborted = 1; + } + } else if (request->state == RUN_MOVE) { + if (request->curl_result == CURLE_OK) { + if (push_verbosely) + fprintf(stderr, " sent %s\n", + sha1_to_hex(request->obj->sha1)); + request->obj->flags |= REMOTE; + release_request(request); + } else { + fprintf(stderr, "MOVE %s failed, aborting (%d/%ld)\n", + sha1_to_hex(request->obj->sha1), + request->curl_result, request->http_code); + request->state = ABORTED; + aborted = 1; + } + } else if (request->state == RUN_FETCH_LOOSE) { + fchmod(request->local_fileno, 0444); + close(request->local_fileno); request->local_fileno = -1; + + if (request->curl_result != CURLE_OK && + request->http_code != 416) { + if (stat(request->tmpfile, &st) == 0) { + if (st.st_size == 0) + unlink(request->tmpfile); + } + } else { + if (request->http_code == 416) + fprintf(stderr, "Warning: requested range invalid; we may already have all the data.\n"); + + inflateEnd(&request->stream); + SHA1_Final(request->real_sha1, &request->c); + if (request->zret != Z_STREAM_END) { + unlink(request->tmpfile); + } else if (hashcmp(request->obj->sha1, request->real_sha1)) { + unlink(request->tmpfile); + } else { + request->rename = + move_temp_to_file( + request->tmpfile, + request->filename); + if (request->rename == 0) { + request->obj->flags |= (LOCAL | REMOTE); + } + } + } + + /* Try fetching packed if necessary */ + if (request->obj->flags & LOCAL) + release_request(request); + else + start_fetch_packed(request); + + } else if (request->state == RUN_FETCH_PACKED) { + if (request->curl_result != CURLE_OK) { + fprintf(stderr, "Unable to get pack file %s\n%s", + request->url, curl_errorstr); + remote->can_update_info_refs = 0; + } else { + off_t pack_size = ftell(request->local_stream); + + fclose(request->local_stream); + request->local_stream = NULL; + if (!move_temp_to_file(request->tmpfile, + request->filename)) { + target = (struct packed_git *)request->userData; + target->pack_size = pack_size; + lst = &remote->packs; + while (*lst != target) + lst = &((*lst)->next); + *lst = (*lst)->next; + + if (!verify_pack(target, 0)) + install_packed_git(target); + else + remote->can_update_info_refs = 0; + } + } + release_request(request); + } +} + +#ifdef USE_CURL_MULTI +void fill_active_slots(void) +{ + struct transfer_request *request = request_queue_head; + struct transfer_request *next; + struct active_request_slot *slot = active_queue_head; + int num_transfers; + + if (aborted) + return; + + while (active_requests < max_requests && request != NULL) { + next = request->next; + if (request->state == NEED_FETCH) { + start_fetch_loose(request); + } else if (pushing && request->state == NEED_PUSH) { + if (remote_dir_exists[request->obj->sha1[0]] == 1) { + start_put(request); + } else { + start_mkcol(request); + } + curl_multi_perform(curlm, &num_transfers); + } + request = next; + } + + while (slot != NULL) { + if (!slot->in_use && slot->curl != NULL) { + curl_easy_cleanup(slot->curl); + slot->curl = NULL; + } + slot = slot->next; + } +} +#endif + +static void get_remote_object_list(unsigned char parent); + +static void add_fetch_request(struct object *obj) +{ + struct transfer_request *request; + + check_locks(); + + /* + * Don't fetch the object if it's known to exist locally + * or is already in the request queue + */ + if (remote_dir_exists[obj->sha1[0]] == -1) + get_remote_object_list(obj->sha1[0]); + if (obj->flags & (LOCAL | FETCHING)) + return; + + obj->flags |= FETCHING; + request = xmalloc(sizeof(*request)); + request->obj = obj; + request->url = NULL; + request->lock = NULL; + request->headers = NULL; + request->local_fileno = -1; + request->local_stream = NULL; + request->state = NEED_FETCH; + 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) +{ + struct transfer_request *request = request_queue_head; + struct packed_git *target; + + /* Keep locks active */ + check_locks(); + + /* + * Don't push the object if it's known to exist on the remote + * or is already in the request queue + */ + if (remote_dir_exists[obj->sha1[0]] == -1) + get_remote_object_list(obj->sha1[0]); + if (obj->flags & (REMOTE | PUSHING)) + return 0; + target = find_sha1_pack(obj->sha1, remote->packs); + if (target) { + obj->flags |= REMOTE; + return 0; + } + + obj->flags |= PUSHING; + request = xmalloc(sizeof(*request)); + request->obj = obj; + request->url = NULL; + request->lock = lock; + request->headers = NULL; + request->local_fileno = -1; + request->local_stream = NULL; + request->state = NEED_PUSH; + request->next = request_queue_head; + request_queue_head = request; + +#ifdef USE_CURL_MULTI + fill_active_slots(); + step_active_slots(); +#endif + + return 1; +} + +static int fetch_index(unsigned char *sha1) +{ + char *hex = sha1_to_hex(sha1); + char *filename; + char *url; + char tmpfile[PATH_MAX]; + long prev_posn = 0; + char range[RANGE_HEADER_SIZE]; + struct curl_slist *range_header = NULL; + + FILE *indexfile; + struct active_request_slot *slot; + struct slot_results results; + + /* Don't use the index if the pack isn't there */ + url = xmalloc(strlen(remote->url) + 64); + sprintf(url, "%sobjects/pack/pack-%s.pack", remote->url, hex); + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 1); + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) { + free(url); + return error("Unable to verify pack %s is available", + hex); + } + } else { + return error("Unable to start request"); + } + + if (has_pack_index(sha1)) + return 0; + + if (push_verbosely) + fprintf(stderr, "Getting index for pack %s\n", hex); + + sprintf(url, "%sobjects/pack/pack-%s.idx", remote->url, hex); + + filename = sha1_pack_index_name(sha1); + snprintf(tmpfile, sizeof(tmpfile), "%s.temp", filename); + indexfile = fopen(tmpfile, "a"); + if (!indexfile) + return error("Unable to open local file %s for pack index", + filename); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 0); + curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); + curl_easy_setopt(slot->curl, CURLOPT_FILE, indexfile); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, no_pragma_header); + slot->local = indexfile; + + /* If there is data present from a previous transfer attempt, + resume where it left off */ + prev_posn = ftell(indexfile); + if (prev_posn>0) { + if (push_verbosely) + fprintf(stderr, + "Resuming fetch of index for pack %s at byte %ld\n", + hex, prev_posn); + sprintf(range, "Range: bytes=%ld-", prev_posn); + range_header = curl_slist_append(range_header, range); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, range_header); + } + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) { + free(url); + fclose(indexfile); + return error("Unable to get pack index %s\n%s", url, + curl_errorstr); + } + } else { + free(url); + fclose(indexfile); + return error("Unable to start request"); + } + + free(url); + fclose(indexfile); + + return move_temp_to_file(tmpfile, filename); +} + +static int setup_index(unsigned char *sha1) +{ + struct packed_git *new_pack; + + if (fetch_index(sha1)) + return -1; + + new_pack = parse_pack_index(sha1); + new_pack->next = remote->packs; + remote->packs = new_pack; + return 0; +} + +static int fetch_indices(void) +{ + unsigned char sha1[20]; + char *url; + struct buffer buffer; + char *data; + int i = 0; + + struct active_request_slot *slot; + struct slot_results results; + + data = xcalloc(1, 4096); + buffer.size = 4096; + buffer.posn = 0; + buffer.buffer = data; + + if (push_verbosely) + fprintf(stderr, "Getting pack list\n"); + + url = xmalloc(strlen(remote->url) + 20); + sprintf(url, "%sobjects/info/packs", remote->url); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL); + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) { + free(buffer.buffer); + free(url); + if (results.http_code == 404) + return 0; + else + return error("%s", curl_errorstr); + } + } else { + free(buffer.buffer); + free(url); + return error("Unable to start request"); + } + free(url); + + data = buffer.buffer; + while (i < buffer.posn) { + switch (data[i]) { + case 'P': + i++; + if (i + 52 < buffer.posn && + !strncmp(data + i, " pack-", 6) && + !strncmp(data + i + 46, ".pack\n", 6)) { + get_sha1_hex(data + i + 6, sha1); + setup_index(sha1); + i += 51; + break; + } + default: + while (data[i] != '\n') + i++; + } + i++; + } + + free(buffer.buffer); + return 0; +} + +static inline int needs_quote(int ch) +{ + if (((ch >= 'A') && (ch <= 'Z')) + || ((ch >= 'a') && (ch <= 'z')) + || ((ch >= '0') && (ch <= '9')) + || (ch == '/') + || (ch == '-') + || (ch == '.')) + return 0; + return 1; +} + +static inline int hex(int v) +{ + if (v < 10) return '0' + v; + else return 'A' + v - 10; +} + +static char *quote_ref_url(const char *base, const char *ref) +{ + const char *cp; + char *dp, *qref; + int len, baselen, ch; + + baselen = strlen(base); + len = baselen + 1; + for (cp = ref; (ch = *cp) != 0; cp++, len++) + if (needs_quote(ch)) + len += 2; /* extra two hex plus replacement % */ + qref = xmalloc(len); + memcpy(qref, base, baselen); + for (cp = ref, dp = qref + baselen; (ch = *cp) != 0; cp++) { + if (needs_quote(ch)) { + *dp++ = '%'; + *dp++ = hex((ch >> 4) & 0xF); + *dp++ = hex(ch & 0xF); + } + else + *dp++ = ch; + } + *dp = 0; + + return qref; +} + +int fetch_ref(char *ref, unsigned char *sha1) +{ + char *url; + char hex[42]; + struct buffer buffer; + char *base = remote->url; + struct active_request_slot *slot; + struct slot_results results; + buffer.size = 41; + buffer.posn = 0; + buffer.buffer = hex; + hex[41] = '\0'; + + url = quote_ref_url(base, ref); + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) + return error("Couldn't get %s for %s\n%s", + url, ref, curl_errorstr); + } else { + return error("Unable to start request"); + } + + hex[40] = '\0'; + get_sha1_hex(hex, sha1); + return 0; +} + +static void one_remote_object(const char *hex) +{ + unsigned char sha1[20]; + struct object *obj; + + if (get_sha1_hex(hex, sha1) != 0) + return; + + obj = lookup_object(sha1); + if (!obj) + obj = parse_object(sha1); + + /* Ignore remote objects that don't exist locally */ + if (!obj) + return; + + obj->flags |= REMOTE; + if (!object_list_contains(objects, obj)) + object_list_insert(obj, &objects); +} + +static void handle_lockprop_ctx(struct xml_ctx *ctx, int tag_closed) +{ + int *lock_flags = (int *)ctx->userData; + + if (tag_closed) { + if (!strcmp(ctx->name, DAV_CTX_LOCKENTRY)) { + if ((*lock_flags & DAV_PROP_LOCKEX) && + (*lock_flags & DAV_PROP_LOCKWR)) { + *lock_flags |= DAV_LOCK_OK; + } + *lock_flags &= DAV_LOCK_OK; + } else if (!strcmp(ctx->name, DAV_CTX_LOCKTYPE_WRITE)) { + *lock_flags |= DAV_PROP_LOCKWR; + } else if (!strcmp(ctx->name, DAV_CTX_LOCKTYPE_EXCLUSIVE)) { + *lock_flags |= DAV_PROP_LOCKEX; + } + } +} + +static void handle_new_lock_ctx(struct xml_ctx *ctx, int tag_closed) +{ + struct remote_lock *lock = (struct remote_lock *)ctx->userData; + + if (tag_closed && ctx->cdata) { + if (!strcmp(ctx->name, DAV_ACTIVELOCK_OWNER)) { + lock->owner = xmalloc(strlen(ctx->cdata) + 1); + strcpy(lock->owner, ctx->cdata); + } else if (!strcmp(ctx->name, DAV_ACTIVELOCK_TIMEOUT)) { + if (!strncmp(ctx->cdata, "Second-", 7)) + lock->timeout = + strtol(ctx->cdata + 7, NULL, 10); + } else if (!strcmp(ctx->name, DAV_ACTIVELOCK_TOKEN)) { + if (!strncmp(ctx->cdata, "opaquelocktoken:", 16)) { + lock->token = xmalloc(strlen(ctx->cdata) - 15); + strcpy(lock->token, ctx->cdata + 16); + } + } + } +} + +static void one_remote_ref(char *refname); + +static void +xml_start_tag(void *userData, const char *name, const char **atts) +{ + struct xml_ctx *ctx = (struct xml_ctx *)userData; + const char *c = strchr(name, ':'); + int new_len; + + if (c == NULL) + c = name; + else + c++; + + new_len = strlen(ctx->name) + strlen(c) + 2; + + if (new_len > ctx->len) { + ctx->name = xrealloc(ctx->name, new_len); + ctx->len = new_len; + } + strcat(ctx->name, "."); + strcat(ctx->name, c); + + free(ctx->cdata); + ctx->cdata = NULL; + + ctx->userFunc(ctx, 0); +} + +static void +xml_end_tag(void *userData, const char *name) +{ + struct xml_ctx *ctx = (struct xml_ctx *)userData; + const char *c = strchr(name, ':'); + char *ep; + + ctx->userFunc(ctx, 1); + + if (c == NULL) + c = name; + else + c++; + + ep = ctx->name + strlen(ctx->name) - strlen(c) - 1; + *ep = 0; +} + +static void +xml_cdata(void *userData, const XML_Char *s, int len) +{ + struct xml_ctx *ctx = (struct xml_ctx *)userData; + free(ctx->cdata); + ctx->cdata = xmalloc(len + 1); + strlcpy(ctx->cdata, s, len + 1); +} + +static struct remote_lock *lock_remote(const char *path, long timeout) +{ + struct active_request_slot *slot; + struct slot_results results; + struct buffer out_buffer; + struct buffer in_buffer; + char *out_data; + char *in_data; + char *url; + char *ep; + char timeout_header[25]; + struct remote_lock *lock = NULL; + XML_Parser parser = XML_ParserCreate(NULL); + enum XML_Status result; + struct curl_slist *dav_headers = NULL; + struct xml_ctx ctx; + + url = xmalloc(strlen(remote->url) + strlen(path) + 1); + sprintf(url, "%s%s", remote->url, path); + + /* Make sure leading directories exist for the remote ref */ + ep = strchr(url + strlen(remote->url) + 11, '/'); + while (ep) { + *ep = 0; + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_MKCOL); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null); + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK && + results.http_code != 405) { + fprintf(stderr, + "Unable to create branch path %s\n", + url); + free(url); + return NULL; + } + } else { + fprintf(stderr, "Unable to start MKCOL request\n"); + free(url); + return NULL; + } + *ep = '/'; + ep = strchr(ep + 1, '/'); + } + + out_buffer.size = strlen(LOCK_REQUEST) + strlen(git_default_email) - 2; + out_data = xmalloc(out_buffer.size + 1); + snprintf(out_data, out_buffer.size + 1, LOCK_REQUEST, git_default_email); + out_buffer.posn = 0; + out_buffer.buffer = out_data; + + in_buffer.size = 4096; + in_data = xmalloc(in_buffer.size); + in_buffer.posn = 0; + in_buffer.buffer = in_data; + + sprintf(timeout_header, "Timeout: Second-%ld", timeout); + dav_headers = curl_slist_append(dav_headers, timeout_header); + dav_headers = curl_slist_append(dav_headers, "Content-Type: text/xml"); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_INFILE, &out_buffer); + curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, out_buffer.size); + curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer); + curl_easy_setopt(slot->curl, CURLOPT_FILE, &in_buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_LOCK); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers); + + lock = xcalloc(1, sizeof(*lock)); + lock->timeout = -1; + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result == CURLE_OK) { + ctx.name = xcalloc(10, 1); + ctx.len = 0; + ctx.cdata = NULL; + ctx.userFunc = handle_new_lock_ctx; + ctx.userData = lock; + XML_SetUserData(parser, &ctx); + XML_SetElementHandler(parser, xml_start_tag, + xml_end_tag); + XML_SetCharacterDataHandler(parser, xml_cdata); + result = XML_Parse(parser, in_buffer.buffer, + in_buffer.posn, 1); + free(ctx.name); + if (result != XML_STATUS_OK) { + fprintf(stderr, "XML error: %s\n", + XML_ErrorString( + XML_GetErrorCode(parser))); + lock->timeout = -1; + } + } + } else { + fprintf(stderr, "Unable to start LOCK request\n"); + } + + curl_slist_free_all(dav_headers); + free(out_data); + free(in_data); + + if (lock->token == NULL || lock->timeout <= 0) { + if (lock->token != NULL) + free(lock->token); + if (lock->owner != NULL) + free(lock->owner); + free(url); + free(lock); + lock = NULL; + } else { + lock->url = url; + lock->start_time = time(NULL); + lock->next = remote->locks; + remote->locks = lock; + } + + return lock; +} + +static int unlock_remote(struct remote_lock *lock) +{ + struct active_request_slot *slot; + struct slot_results results; + struct remote_lock *prev = remote->locks; + char *lock_token_header; + struct curl_slist *dav_headers = NULL; + int rc = 0; + + lock_token_header = xmalloc(strlen(lock->token) + 31); + sprintf(lock_token_header, "Lock-Token: <opaquelocktoken:%s>", + lock->token); + dav_headers = curl_slist_append(dav_headers, lock_token_header); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null); + curl_easy_setopt(slot->curl, CURLOPT_URL, lock->url); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_UNLOCK); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers); + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result == CURLE_OK) + rc = 1; + else + fprintf(stderr, "UNLOCK HTTP error %ld\n", + results.http_code); + } else { + fprintf(stderr, "Unable to start UNLOCK request\n"); + } + + curl_slist_free_all(dav_headers); + free(lock_token_header); + + if (remote->locks == lock) { + remote->locks = lock->next; + } else { + while (prev && prev->next != lock) + prev = prev->next; + if (prev) + prev->next = prev->next->next; + } + + if (lock->owner != NULL) + free(lock->owner); + free(lock->url); + free(lock->token); + free(lock); + + return rc; +} + +static void remote_ls(const char *path, int flags, + void (*userFunc)(struct remote_ls_ctx *ls), + void *userData); + +static void process_ls_object(struct remote_ls_ctx *ls) +{ + unsigned int *parent = (unsigned int *)ls->userData; + char *path = ls->dentry_name; + char *obj_hex; + + if (!strcmp(ls->path, ls->dentry_name) && (ls->flags & IS_DIR)) { + remote_dir_exists[*parent] = 1; + return; + } + + if (strlen(path) != 49) + return; + path += 8; + obj_hex = xmalloc(strlen(path)); + strlcpy(obj_hex, path, 3); + strcpy(obj_hex + 2, path + 3); + one_remote_object(obj_hex); + free(obj_hex); +} + +static void process_ls_ref(struct remote_ls_ctx *ls) +{ + if (!strcmp(ls->path, ls->dentry_name) && (ls->dentry_flags & IS_DIR)) { + fprintf(stderr, " %s\n", ls->dentry_name); + return; + } + + if (!(ls->dentry_flags & IS_DIR)) + one_remote_ref(ls->dentry_name); +} + +static void handle_remote_ls_ctx(struct xml_ctx *ctx, int tag_closed) +{ + struct remote_ls_ctx *ls = (struct remote_ls_ctx *)ctx->userData; + + if (tag_closed) { + if (!strcmp(ctx->name, DAV_PROPFIND_RESP) && ls->dentry_name) { + if (ls->dentry_flags & IS_DIR) { + if (ls->flags & PROCESS_DIRS) { + ls->userFunc(ls); + } + if (strcmp(ls->dentry_name, ls->path) && + ls->flags & RECURSIVE) { + remote_ls(ls->dentry_name, + ls->flags, + ls->userFunc, + ls->userData); + } + } else if (ls->flags & PROCESS_FILES) { + ls->userFunc(ls); + } + } else if (!strcmp(ctx->name, DAV_PROPFIND_NAME) && ctx->cdata) { + ls->dentry_name = xmalloc(strlen(ctx->cdata) - + remote->path_len + 1); + strcpy(ls->dentry_name, ctx->cdata + remote->path_len); + } else if (!strcmp(ctx->name, DAV_PROPFIND_COLLECTION)) { + ls->dentry_flags |= IS_DIR; + } + } else if (!strcmp(ctx->name, DAV_PROPFIND_RESP)) { + free(ls->dentry_name); + ls->dentry_name = NULL; + ls->dentry_flags = 0; + } +} + +static void remote_ls(const char *path, int flags, + void (*userFunc)(struct remote_ls_ctx *ls), + void *userData) +{ + char *url = xmalloc(strlen(remote->url) + strlen(path) + 1); + struct active_request_slot *slot; + struct slot_results results; + struct buffer in_buffer; + struct buffer out_buffer; + char *in_data; + char *out_data; + XML_Parser parser = XML_ParserCreate(NULL); + enum XML_Status result; + struct curl_slist *dav_headers = NULL; + struct xml_ctx ctx; + struct remote_ls_ctx ls; + + ls.flags = flags; + ls.path = xstrdup(path); + ls.dentry_name = NULL; + ls.dentry_flags = 0; + ls.userData = userData; + ls.userFunc = userFunc; + + sprintf(url, "%s%s", remote->url, path); + + out_buffer.size = strlen(PROPFIND_ALL_REQUEST); + out_data = xmalloc(out_buffer.size + 1); + snprintf(out_data, out_buffer.size + 1, PROPFIND_ALL_REQUEST); + out_buffer.posn = 0; + out_buffer.buffer = out_data; + + in_buffer.size = 4096; + in_data = xmalloc(in_buffer.size); + in_buffer.posn = 0; + in_buffer.buffer = in_data; + + dav_headers = curl_slist_append(dav_headers, "Depth: 1"); + dav_headers = curl_slist_append(dav_headers, "Content-Type: text/xml"); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_INFILE, &out_buffer); + curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, out_buffer.size); + curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer); + curl_easy_setopt(slot->curl, CURLOPT_FILE, &in_buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PROPFIND); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers); + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result == CURLE_OK) { + ctx.name = xcalloc(10, 1); + ctx.len = 0; + ctx.cdata = NULL; + ctx.userFunc = handle_remote_ls_ctx; + ctx.userData = &ls; + XML_SetUserData(parser, &ctx); + XML_SetElementHandler(parser, xml_start_tag, + xml_end_tag); + XML_SetCharacterDataHandler(parser, xml_cdata); + result = XML_Parse(parser, in_buffer.buffer, + in_buffer.posn, 1); + free(ctx.name); + + if (result != XML_STATUS_OK) { + fprintf(stderr, "XML error: %s\n", + XML_ErrorString( + XML_GetErrorCode(parser))); + } + } + } else { + fprintf(stderr, "Unable to start PROPFIND request\n"); + } + + free(ls.path); + free(url); + free(out_data); + free(in_buffer.buffer); + curl_slist_free_all(dav_headers); +} + +static void get_remote_object_list(unsigned char parent) +{ + char path[] = "objects/XX/"; + static const char hex[] = "0123456789abcdef"; + unsigned int val = parent; + + path[8] = hex[val >> 4]; + path[9] = hex[val & 0xf]; + remote_dir_exists[val] = 0; + remote_ls(path, (PROCESS_FILES | PROCESS_DIRS), + process_ls_object, &val); +} + +static int locking_available(void) +{ + struct active_request_slot *slot; + struct slot_results results; + struct buffer in_buffer; + struct buffer out_buffer; + char *in_data; + char *out_data; + XML_Parser parser = XML_ParserCreate(NULL); + enum XML_Status result; + struct curl_slist *dav_headers = NULL; + struct xml_ctx ctx; + int lock_flags = 0; + + out_buffer.size = + strlen(PROPFIND_SUPPORTEDLOCK_REQUEST) + + strlen(remote->url) - 2; + out_data = xmalloc(out_buffer.size + 1); + snprintf(out_data, out_buffer.size + 1, + PROPFIND_SUPPORTEDLOCK_REQUEST, remote->url); + out_buffer.posn = 0; + out_buffer.buffer = out_data; + + in_buffer.size = 4096; + in_data = xmalloc(in_buffer.size); + in_buffer.posn = 0; + in_buffer.buffer = in_data; + + dav_headers = curl_slist_append(dav_headers, "Depth: 0"); + dav_headers = curl_slist_append(dav_headers, "Content-Type: text/xml"); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_INFILE, &out_buffer); + curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, out_buffer.size); + curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer); + curl_easy_setopt(slot->curl, CURLOPT_FILE, &in_buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_URL, remote->url); + curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PROPFIND); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers); + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result == CURLE_OK) { + ctx.name = xcalloc(10, 1); + ctx.len = 0; + ctx.cdata = NULL; + ctx.userFunc = handle_lockprop_ctx; + ctx.userData = &lock_flags; + XML_SetUserData(parser, &ctx); + XML_SetElementHandler(parser, xml_start_tag, + xml_end_tag); + result = XML_Parse(parser, in_buffer.buffer, + in_buffer.posn, 1); + free(ctx.name); + + if (result != XML_STATUS_OK) { + fprintf(stderr, "XML error: %s\n", + XML_ErrorString( + XML_GetErrorCode(parser))); + lock_flags = 0; + } + } + } else { + fprintf(stderr, "Unable to start PROPFIND request\n"); + } + + free(out_data); + free(in_buffer.buffer); + curl_slist_free_all(dav_headers); + + return lock_flags; +} + +static struct object_list **add_one_object(struct object *obj, struct object_list **p) +{ + struct object_list *entry = xmalloc(sizeof(struct object_list)); + entry->item = obj; + entry->next = *p; + *p = entry; + return &entry->next; +} + +static struct object_list **process_blob(struct blob *blob, + struct object_list **p, + struct name_path *path, + const char *name) +{ + struct object *obj = &blob->object; + + obj->flags |= LOCAL; + + if (obj->flags & (UNINTERESTING | SEEN)) + return p; + + obj->flags |= SEEN; + return add_one_object(obj, p); +} + +static struct object_list **process_tree(struct tree *tree, + struct object_list **p, + struct name_path *path, + const char *name) +{ + struct object *obj = &tree->object; + struct tree_desc desc; + struct name_entry entry; + struct name_path me; + + obj->flags |= LOCAL; + + if (obj->flags & (UNINTERESTING | SEEN)) + return p; + if (parse_tree(tree) < 0) + die("bad tree object %s", sha1_to_hex(obj->sha1)); + + obj->flags |= SEEN; + name = xstrdup(name); + p = add_one_object(obj, p); + me.up = path; + me.elem = name; + me.elem_len = strlen(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(lookup_blob(entry.sha1), p, &me, name); + } + free(tree->buffer); + tree->buffer = NULL; + return p; +} + +static int get_delta(struct rev_info *revs, struct remote_lock *lock) +{ + int i; + struct commit *commit; + struct object_list **p = &objects; + int count = 0; + + while ((commit = get_revision(revs)) != NULL) { + p = process_tree(commit->tree, p, NULL, ""); + commit->object.flags |= LOCAL; + if (!(commit->object.flags & UNINTERESTING)) + count += add_send_request(&commit->object, lock); + } + + for (i = 0; i < revs->pending.nr; i++) { + struct object_array_entry *entry = revs->pending.objects + i; + struct object *obj = entry->item; + const char *name = entry->name; + + if (obj->flags & (UNINTERESTING | SEEN)) + continue; + if (obj->type == OBJ_TAG) { + obj->flags |= SEEN; + p = add_one_object(obj, p); + continue; + } + if (obj->type == OBJ_TREE) { + p = process_tree((struct tree *)obj, p, NULL, name); + continue; + } + if (obj->type == OBJ_BLOB) { + p = process_blob((struct blob *)obj, p, NULL, name); + continue; + } + die("unknown pending object %s (%s)", sha1_to_hex(obj->sha1), name); + } + + while (objects) { + if (!(objects->item->flags & UNINTERESTING)) + count += add_send_request(objects->item, lock); + objects = objects->next; + } + + return count; +} + +static int update_remote(unsigned char *sha1, struct remote_lock *lock) +{ + struct active_request_slot *slot; + struct slot_results results; + char *out_data; + char *if_header; + struct buffer out_buffer; + struct curl_slist *dav_headers = NULL; + int i; + + if_header = xmalloc(strlen(lock->token) + 25); + sprintf(if_header, "If: (<opaquelocktoken:%s>)", lock->token); + dav_headers = curl_slist_append(dav_headers, if_header); + + out_buffer.size = 41; + out_data = xmalloc(out_buffer.size + 1); + i = snprintf(out_data, out_buffer.size + 1, "%s\n", sha1_to_hex(sha1)); + if (i != out_buffer.size) { + fprintf(stderr, "Unable to initialize PUT request body\n"); + return 0; + } + out_buffer.posn = 0; + out_buffer.buffer = out_data; + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_INFILE, &out_buffer); + curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, out_buffer.size); + curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PUT); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers); + curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1); + curl_easy_setopt(slot->curl, CURLOPT_PUT, 1); + curl_easy_setopt(slot->curl, CURLOPT_URL, lock->url); + + if (start_active_slot(slot)) { + run_active_slot(slot); + free(out_data); + free(if_header); + if (results.curl_result != CURLE_OK) { + fprintf(stderr, + "PUT error: curl result=%d, HTTP code=%ld\n", + results.curl_result, results.http_code); + /* We should attempt recovery? */ + return 0; + } + } else { + free(out_data); + free(if_header); + fprintf(stderr, "Unable to start PUT request\n"); + return 0; + } + + return 1; +} + +static struct ref *local_refs, **local_tail; +static struct ref *remote_refs, **remote_tail; + +static int one_local_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + struct ref *ref; + int len = strlen(refname) + 1; + ref = xcalloc(1, sizeof(*ref) + len); + hashcpy(ref->new_sha1, sha1); + memcpy(ref->name, refname, len); + *local_tail = ref; + local_tail = &ref->next; + return 0; +} + +static void one_remote_ref(char *refname) +{ + struct ref *ref; + unsigned char remote_sha1[20]; + struct object *obj; + int len = strlen(refname) + 1; + + if (fetch_ref(refname, remote_sha1) != 0) { + fprintf(stderr, + "Unable to fetch ref %s from %s\n", + refname, remote->url); + return; + } + + /* + * Fetch a copy of the object if it doesn't exist locally - it + * may be required for updating server info later. + */ + if (remote->can_update_info_refs && !has_sha1_file(remote_sha1)) { + obj = lookup_unknown_object(remote_sha1); + if (obj) { + fprintf(stderr, " fetch %s for %s\n", + sha1_to_hex(remote_sha1), refname); + add_fetch_request(obj); + } + } + + ref = xcalloc(1, sizeof(*ref) + len); + hashcpy(ref->old_sha1, remote_sha1); + memcpy(ref->name, refname, len); + *remote_tail = ref; + remote_tail = &ref->next; +} + +static void get_local_heads(void) +{ + local_tail = &local_refs; + for_each_ref(one_local_ref, NULL); +} + +static void get_dav_remote_heads(void) +{ + remote_tail = &remote_refs; + remote_ls("refs/", (PROCESS_FILES | PROCESS_DIRS | RECURSIVE), process_ls_ref, NULL); +} + +static int is_zero_sha1(const unsigned char *sha1) +{ + int i; + + for (i = 0; i < 20; i++) { + if (*sha1++) + return 0; + } + return 1; +} + +static void unmark_and_free(struct commit_list *list, unsigned int mark) +{ + while (list) { + struct commit_list *temp = list; + temp->item->object.flags &= ~mark; + list = temp->next; + free(temp); + } +} + +static int ref_newer(const unsigned char *new_sha1, + const unsigned char *old_sha1) +{ + struct object *o; + struct commit *old, *new; + struct commit_list *list, *used; + int found = 0; + + /* Both new and old must be commit-ish and new is descendant of + * old. Otherwise we require --force. + */ + o = deref_tag(parse_object(old_sha1), NULL, 0); + if (!o || o->type != OBJ_COMMIT) + return 0; + old = (struct commit *) o; + + o = deref_tag(parse_object(new_sha1), NULL, 0); + if (!o || o->type != OBJ_COMMIT) + return 0; + new = (struct commit *) o; + + if (parse_commit(new) < 0) + return 0; + + used = list = NULL; + commit_list_insert(new, &list); + while (list) { + new = pop_most_recent_commit(&list, TMP_MARK); + commit_list_insert(new, &used); + if (new == old) { + found = 1; + break; + } + } + unmark_and_free(list, TMP_MARK); + unmark_and_free(used, TMP_MARK); + return found; +} + +static void mark_edge_parents_uninteresting(struct commit *commit) +{ + struct commit_list *parents; + + for (parents = commit->parents; parents; parents = parents->next) { + struct commit *parent = parents->item; + if (!(parent->object.flags & UNINTERESTING)) + continue; + mark_tree_uninteresting(parent->tree); + } +} + +static void mark_edges_uninteresting(struct commit_list *list) +{ + for ( ; list; list = list->next) { + struct commit *commit = list->item; + + if (commit->object.flags & UNINTERESTING) { + mark_tree_uninteresting(commit->tree); + continue; + } + mark_edge_parents_uninteresting(commit); + } +} + +static void add_remote_info_ref(struct remote_ls_ctx *ls) +{ + struct buffer *buf = (struct buffer *)ls->userData; + unsigned char remote_sha1[20]; + struct object *o; + int len; + char *ref_info; + + if (fetch_ref(ls->dentry_name, remote_sha1) != 0) { + fprintf(stderr, + "Unable to fetch ref %s from %s\n", + ls->dentry_name, remote->url); + aborted = 1; + return; + } + + o = parse_object(remote_sha1); + if (!o) { + fprintf(stderr, + "Unable to parse object %s for remote ref %s\n", + sha1_to_hex(remote_sha1), ls->dentry_name); + aborted = 1; + return; + } + + len = strlen(ls->dentry_name) + 42; + ref_info = xcalloc(len + 1, 1); + sprintf(ref_info, "%s %s\n", + sha1_to_hex(remote_sha1), ls->dentry_name); + fwrite_buffer(ref_info, 1, len, buf); + free(ref_info); + + if (o->type == OBJ_TAG) { + o = deref_tag(o, ls->dentry_name, 0); + if (o) { + len = strlen(ls->dentry_name) + 45; + ref_info = xcalloc(len + 1, 1); + sprintf(ref_info, "%s %s^{}\n", + sha1_to_hex(o->sha1), ls->dentry_name); + fwrite_buffer(ref_info, 1, len, buf); + free(ref_info); + } + } +} + +static void update_remote_info_refs(struct remote_lock *lock) +{ + struct buffer buffer; + struct active_request_slot *slot; + struct slot_results results; + char *if_header; + struct curl_slist *dav_headers = NULL; + + buffer.buffer = xcalloc(1, 4096); + buffer.size = 4096; + buffer.posn = 0; + remote_ls("refs/", (PROCESS_FILES | RECURSIVE), + add_remote_info_ref, &buffer); + if (!aborted) { + if_header = xmalloc(strlen(lock->token) + 25); + sprintf(if_header, "If: (<opaquelocktoken:%s>)", lock->token); + dav_headers = curl_slist_append(dav_headers, if_header); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_INFILE, &buffer); + curl_easy_setopt(slot->curl, CURLOPT_INFILESIZE, buffer.posn); + curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, fread_buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_PUT); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, dav_headers); + curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 1); + curl_easy_setopt(slot->curl, CURLOPT_PUT, 1); + curl_easy_setopt(slot->curl, CURLOPT_URL, lock->url); + + buffer.posn = 0; + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) { + fprintf(stderr, + "PUT error: curl result=%d, HTTP code=%ld\n", + results.curl_result, results.http_code); + } + } + free(if_header); + } + free(buffer.buffer); +} + +static int remote_exists(const char *path) +{ + char *url = xmalloc(strlen(remote->url) + strlen(path) + 1); + struct active_request_slot *slot; + struct slot_results results; + + sprintf(url, "%s%s", remote->url, path); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 1); + + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.http_code == 404) + return 0; + else if (results.curl_result == CURLE_OK) + return 1; + else + fprintf(stderr, "HEAD HTTP error %ld\n", results.http_code); + } else { + fprintf(stderr, "Unable to start HEAD request\n"); + } + + return -1; +} + +static void fetch_symref(const char *path, char **symref, unsigned char *sha1) +{ + char *url; + struct buffer buffer; + struct active_request_slot *slot; + struct slot_results results; + + url = xmalloc(strlen(remote->url) + strlen(path) + 1); + sprintf(url, "%s%s", remote->url, path); + + buffer.size = 4096; + buffer.posn = 0; + buffer.buffer = xmalloc(buffer.size); + + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_FILE, &buffer); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, NULL); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + if (start_active_slot(slot)) { + run_active_slot(slot); + if (results.curl_result != CURLE_OK) { + die("Couldn't get %s for remote symref\n%s", + url, curl_errorstr); + } + } else { + die("Unable to start remote symref request"); + } + free(url); + + if (*symref != NULL) + free(*symref); + *symref = NULL; + hashclr(sha1); + + if (buffer.posn == 0) + return; + + /* If it's a symref, set the refname; otherwise try for a sha1 */ + if (!strncmp((char *)buffer.buffer, "ref: ", 5)) { + *symref = xmalloc(buffer.posn - 5); + strlcpy(*symref, (char *)buffer.buffer + 5, buffer.posn - 5); + } else { + get_sha1_hex(buffer.buffer, sha1); + } + + free(buffer.buffer); +} + +static int verify_merge_base(unsigned char *head_sha1, unsigned char *branch_sha1) +{ + struct commit *head = lookup_commit(head_sha1); + struct commit *branch = lookup_commit(branch_sha1); + struct commit_list *merge_bases = get_merge_bases(head, branch, 1); + + return (merge_bases && !merge_bases->next && merge_bases->item == branch); +} + +static int delete_remote_branch(char *pattern, int force) +{ + struct ref *refs = remote_refs; + struct ref *remote_ref = NULL; + unsigned char head_sha1[20]; + char *symref = NULL; + int match; + int patlen = strlen(pattern); + int i; + struct active_request_slot *slot; + struct slot_results results; + char *url; + + /* Find the remote branch(es) matching the specified branch name */ + for (match = 0; refs; refs = refs->next) { + char *name = refs->name; + int namelen = strlen(name); + if (namelen < patlen || + memcmp(name + namelen - patlen, pattern, patlen)) + continue; + if (namelen != patlen && name[namelen - patlen - 1] != '/') + continue; + match++; + remote_ref = refs; + } + if (match == 0) + return error("No remote branch matches %s", pattern); + if (match != 1) + return error("More than one remote branch matches %s", + pattern); + + /* + * Remote HEAD must be a symref (not exactly foolproof; a remote + * symlink to a symref will look like a symref) + */ + fetch_symref("HEAD", &symref, head_sha1); + if (!symref) + return error("Remote HEAD is not a symref"); + + /* Remote branch must not be the remote HEAD */ + for (i=0; symref && i<MAXDEPTH; i++) { + if (!strcmp(remote_ref->name, symref)) + return error("Remote branch %s is the current HEAD", + remote_ref->name); + fetch_symref(symref, &symref, head_sha1); + } + + /* Run extra sanity checks if delete is not forced */ + if (!force) { + /* Remote HEAD must resolve to a known object */ + if (symref) + return error("Remote HEAD symrefs too deep"); + if (is_zero_sha1(head_sha1)) + return error("Unable to resolve remote HEAD"); + if (!has_sha1_file(head_sha1)) + return error("Remote HEAD resolves to object %s\nwhich does not exist locally, perhaps you need to fetch?", sha1_to_hex(head_sha1)); + + /* Remote branch must resolve to a known object */ + if (is_zero_sha1(remote_ref->old_sha1)) + return error("Unable to resolve remote branch %s", + remote_ref->name); + if (!has_sha1_file(remote_ref->old_sha1)) + return error("Remote branch %s resolves to object %s\nwhich does not exist locally, perhaps you need to fetch?", remote_ref->name, sha1_to_hex(remote_ref->old_sha1)); + + /* Remote branch must be an ancestor of remote HEAD */ + if (!verify_merge_base(head_sha1, remote_ref->old_sha1)) { + return error("The branch '%s' is not a strict subset of your current HEAD.\nIf you are sure you want to delete it, run:\n\t'git http-push -D %s %s'", remote_ref->name, remote->url, pattern); + } + } + + /* Send delete request */ + fprintf(stderr, "Removing remote branch '%s'\n", remote_ref->name); + url = xmalloc(strlen(remote->url) + strlen(remote_ref->name) + 1); + sprintf(url, "%s%s", remote->url, remote_ref->name); + slot = get_active_slot(); + slot->results = &results; + curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite_null); + curl_easy_setopt(slot->curl, CURLOPT_URL, url); + curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, DAV_DELETE); + if (start_active_slot(slot)) { + run_active_slot(slot); + free(url); + if (results.curl_result != CURLE_OK) + return error("DELETE request failed (%d/%ld)\n", + results.curl_result, results.http_code); + } else { + free(url); + return error("Unable to start DELETE request"); + } + + return 0; +} + +int main(int argc, char **argv) +{ + struct transfer_request *request; + struct transfer_request *next_request; + int nr_refspec = 0; + char **refspec = NULL; + struct remote_lock *ref_lock = NULL; + struct remote_lock *info_ref_lock = NULL; + struct rev_info revs; + int delete_branch = 0; + int force_delete = 0; + int objects_to_send; + int rc = 0; + int i; + int new_refs; + struct ref *ref; + + setup_git_directory(); + + remote = xcalloc(sizeof(*remote), 1); + + argv++; + for (i = 1; i < argc; i++, argv++) { + char *arg = *argv; + + if (*arg == '-') { + if (!strcmp(arg, "--all")) { + push_all = 1; + continue; + } + if (!strcmp(arg, "--force")) { + force_all = 1; + continue; + } + if (!strcmp(arg, "--verbose")) { + push_verbosely = 1; + continue; + } + if (!strcmp(arg, "-d")) { + delete_branch = 1; + continue; + } + if (!strcmp(arg, "-D")) { + delete_branch = 1; + force_delete = 1; + continue; + } + } + if (!remote->url) { + char *path = strstr(arg, "//"); + remote->url = arg; + if (path) { + path = strchr(path+2, '/'); + if (path) + remote->path_len = strlen(path); + } + continue; + } + refspec = argv; + nr_refspec = argc - i; + break; + } + + if (!remote->url) + usage(http_push_usage); + + if (delete_branch && nr_refspec != 1) + die("You must specify only one branch name when deleting a remote branch"); + + memset(remote_dir_exists, -1, 256); + + http_init(); + + no_pragma_header = curl_slist_append(no_pragma_header, "Pragma:"); + default_headers = curl_slist_append(default_headers, "Range:"); + default_headers = curl_slist_append(default_headers, "Destination:"); + default_headers = curl_slist_append(default_headers, "If:"); + default_headers = curl_slist_append(default_headers, + "Pragma: no-cache"); + + /* Verify DAV compliance/lock support */ + if (!locking_available()) { + fprintf(stderr, "Error: no DAV locking support on remote repo %s\n", remote->url); + rc = 1; + goto cleanup; + } + + /* Check whether the remote has server info files */ + remote->can_update_info_refs = 0; + remote->has_info_refs = remote_exists("info/refs"); + remote->has_info_packs = remote_exists("objects/info/packs"); + if (remote->has_info_refs) { + info_ref_lock = lock_remote("info/refs", LOCK_TIME); + if (info_ref_lock) + remote->can_update_info_refs = 1; + } + if (remote->has_info_packs) + fetch_indices(); + + /* Get a list of all local and remote heads to validate refspecs */ + get_local_heads(); + fprintf(stderr, "Fetching remote heads...\n"); + get_dav_remote_heads(); + + /* Remove a remote branch if -d or -D was specified */ + if (delete_branch) { + if (delete_remote_branch(refspec[0], force_delete) == -1) + fprintf(stderr, "Unable to delete remote branch %s\n", + refspec[0]); + goto cleanup; + } + + /* match them up */ + if (!remote_tail) + remote_tail = &remote_refs; + if (match_refs(local_refs, remote_refs, &remote_tail, + nr_refspec, refspec, push_all)) + return -1; + if (!remote_refs) { + fprintf(stderr, "No refs in common and none specified; doing nothing.\n"); + return 0; + } + + new_refs = 0; + for (ref = remote_refs; ref; ref = ref->next) { + char old_hex[60], *new_hex; + const char *commit_argv[4]; + int commit_argc; + char *new_sha1_hex, *old_sha1_hex; + + if (!ref->peer_ref) + continue; + if (!hashcmp(ref->old_sha1, ref->peer_ref->new_sha1)) { + if (push_verbosely || 1) + fprintf(stderr, "'%s': up-to-date\n", ref->name); + continue; + } + + if (!force_all && + !is_zero_sha1(ref->old_sha1) && + !ref->force) { + if (!has_sha1_file(ref->old_sha1) || + !ref_newer(ref->peer_ref->new_sha1, + ref->old_sha1)) { + /* We do not have the remote ref, or + * we know that the remote ref is not + * an ancestor of what we are trying to + * push. Either way this can be losing + * commits at the remote end and likely + * we were not up to date to begin with. + */ + error("remote '%s' is not a strict " + "subset of local ref '%s'. " + "maybe you are not up-to-date and " + "need to pull first?", + ref->name, + ref->peer_ref->name); + rc = -2; + continue; + } + } + hashcpy(ref->new_sha1, ref->peer_ref->new_sha1); + if (is_zero_sha1(ref->new_sha1)) { + error("cannot happen anymore"); + rc = -3; + continue; + } + new_refs++; + strcpy(old_hex, sha1_to_hex(ref->old_sha1)); + new_hex = sha1_to_hex(ref->new_sha1); + + fprintf(stderr, "updating '%s'", ref->name); + if (strcmp(ref->name, ref->peer_ref->name)) + fprintf(stderr, " using '%s'", ref->peer_ref->name); + fprintf(stderr, "\n from %s\n to %s\n", old_hex, new_hex); + + + /* Lock remote branch ref */ + ref_lock = lock_remote(ref->name, LOCK_TIME); + if (ref_lock == NULL) { + fprintf(stderr, "Unable to lock remote branch %s\n", + ref->name); + rc = 1; + continue; + } + + /* Set up revision info for this refspec */ + commit_argc = 3; + new_sha1_hex = xstrdup(sha1_to_hex(ref->new_sha1)); + old_sha1_hex = NULL; + commit_argv[1] = "--objects"; + commit_argv[2] = new_sha1_hex; + if (!push_all && !is_zero_sha1(ref->old_sha1)) { + old_sha1_hex = xmalloc(42); + sprintf(old_sha1_hex, "^%s", + sha1_to_hex(ref->old_sha1)); + commit_argv[3] = old_sha1_hex; + commit_argc++; + } + init_revisions(&revs, setup_git_directory()); + setup_revisions(commit_argc, commit_argv, &revs, NULL); + free(new_sha1_hex); + if (old_sha1_hex) { + free(old_sha1_hex); + commit_argv[1] = NULL; + } + + /* Generate a list of objects that need to be pushed */ + pushing = 0; + prepare_revision_walk(&revs); + mark_edges_uninteresting(revs.commits); + objects_to_send = get_delta(&revs, ref_lock); + finish_all_active_slots(); + + /* Push missing objects to remote, this would be a + convenient time to pack them first if appropriate. */ + pushing = 1; + 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 */ + if (aborted || !update_remote(ref->new_sha1, ref_lock)) { + rc = 1; + goto unlock; + } + + unlock: + if (!rc) + fprintf(stderr, " done\n"); + unlock_remote(ref_lock); + check_locks(); + } + + /* Update remote server info if appropriate */ + if (remote->has_info_refs && new_refs) { + if (info_ref_lock && remote->can_update_info_refs) { + fprintf(stderr, "Updating remote server info\n"); + update_remote_info_refs(info_ref_lock); + } else { + fprintf(stderr, "Unable to update server info\n"); + } + } + if (info_ref_lock) + unlock_remote(info_ref_lock); + + cleanup: + free(remote); + + curl_slist_free_all(no_pragma_header); + curl_slist_free_all(default_headers); + + http_cleanup(); + + request = request_queue_head; + while (request != NULL) { + next_request = request->next; + release_request(request); + request = next_request; + } + + return rc; +} diff --git a/http.c b/http.c new file mode 100644 index 0000000000..576740feff --- /dev/null +++ b/http.c @@ -0,0 +1,496 @@ +#include "http.h" + +int data_received; +int active_requests = 0; + +#ifdef USE_CURL_MULTI +int max_requests = -1; +CURLM *curlm; +#endif +#ifndef NO_CURL_EASY_DUPHANDLE +CURL *curl_default; +#endif +char curl_errorstr[CURL_ERROR_SIZE]; + +int curl_ssl_verify = -1; +char *ssl_cert = NULL; +#if LIBCURL_VERSION_NUM >= 0x070902 +char *ssl_key = NULL; +#endif +#if LIBCURL_VERSION_NUM >= 0x070908 +char *ssl_capath = NULL; +#endif +char *ssl_cainfo = NULL; +long curl_low_speed_limit = -1; +long curl_low_speed_time = -1; +int curl_ftp_no_epsv = 0; + +struct curl_slist *pragma_header; + +struct active_request_slot *active_queue_head = NULL; + +size_t fread_buffer(void *ptr, size_t eltsize, size_t nmemb, + struct buffer *buffer) +{ + size_t size = eltsize * nmemb; + if (size > buffer->size - buffer->posn) + size = buffer->size - buffer->posn; + memcpy(ptr, (char *) buffer->buffer + buffer->posn, size); + buffer->posn += size; + return size; +} + +size_t fwrite_buffer(const void *ptr, size_t eltsize, + size_t nmemb, struct buffer *buffer) +{ + size_t size = eltsize * nmemb; + if (size > buffer->size - buffer->posn) { + buffer->size = buffer->size * 3 / 2; + if (buffer->size < buffer->posn + size) + buffer->size = buffer->posn + size; + buffer->buffer = xrealloc(buffer->buffer, buffer->size); + } + memcpy((char *) buffer->buffer + buffer->posn, ptr, size); + buffer->posn += size; + data_received++; + return size; +} + +size_t fwrite_null(const void *ptr, size_t eltsize, + size_t nmemb, struct buffer *buffer) +{ + data_received++; + return eltsize * nmemb; +} + +static void finish_active_slot(struct active_request_slot *slot); + +#ifdef USE_CURL_MULTI +static void process_curl_messages(void) +{ + int num_messages; + struct active_request_slot *slot; + CURLMsg *curl_message = curl_multi_info_read(curlm, &num_messages); + + while (curl_message != NULL) { + if (curl_message->msg == CURLMSG_DONE) { + int curl_result = curl_message->data.result; + slot = active_queue_head; + while (slot != NULL && + slot->curl != curl_message->easy_handle) + slot = slot->next; + if (slot != NULL) { + curl_multi_remove_handle(curlm, slot->curl); + slot->curl_result = curl_result; + finish_active_slot(slot); + } else { + fprintf(stderr, "Received DONE message for unknown request!\n"); + } + } else { + fprintf(stderr, "Unknown CURL message received: %d\n", + (int)curl_message->msg); + } + curl_message = curl_multi_info_read(curlm, &num_messages); + } +} +#endif + +static int http_options(const char *var, const char *value) +{ + if (!strcmp("http.sslverify", var)) { + if (curl_ssl_verify == -1) { + curl_ssl_verify = git_config_bool(var, value); + } + return 0; + } + + if (!strcmp("http.sslcert", var)) { + if (ssl_cert == NULL) { + ssl_cert = xmalloc(strlen(value)+1); + strcpy(ssl_cert, value); + } + return 0; + } +#if LIBCURL_VERSION_NUM >= 0x070902 + if (!strcmp("http.sslkey", var)) { + if (ssl_key == NULL) { + ssl_key = xmalloc(strlen(value)+1); + strcpy(ssl_key, value); + } + return 0; + } +#endif +#if LIBCURL_VERSION_NUM >= 0x070908 + if (!strcmp("http.sslcapath", var)) { + if (ssl_capath == NULL) { + ssl_capath = xmalloc(strlen(value)+1); + strcpy(ssl_capath, value); + } + return 0; + } +#endif + if (!strcmp("http.sslcainfo", var)) { + if (ssl_cainfo == NULL) { + ssl_cainfo = xmalloc(strlen(value)+1); + strcpy(ssl_cainfo, value); + } + return 0; + } + +#ifdef USE_CURL_MULTI + if (!strcmp("http.maxrequests", var)) { + if (max_requests == -1) + max_requests = git_config_int(var, value); + return 0; + } +#endif + + if (!strcmp("http.lowspeedlimit", var)) { + if (curl_low_speed_limit == -1) + curl_low_speed_limit = (long)git_config_int(var, value); + return 0; + } + if (!strcmp("http.lowspeedtime", var)) { + if (curl_low_speed_time == -1) + curl_low_speed_time = (long)git_config_int(var, value); + return 0; + } + + if (!strcmp("http.noepsv", var)) { + curl_ftp_no_epsv = git_config_bool(var, value); + return 0; + } + + /* Fall back on the default ones */ + return git_default_config(var, value); +} + +static CURL* get_curl_handle(void) +{ + CURL* result = curl_easy_init(); + + curl_easy_setopt(result, CURLOPT_SSL_VERIFYPEER, curl_ssl_verify); +#if LIBCURL_VERSION_NUM >= 0x070907 + curl_easy_setopt(result, CURLOPT_NETRC, CURL_NETRC_OPTIONAL); +#endif + + if (ssl_cert != NULL) + curl_easy_setopt(result, CURLOPT_SSLCERT, ssl_cert); +#if LIBCURL_VERSION_NUM >= 0x070902 + if (ssl_key != NULL) + curl_easy_setopt(result, CURLOPT_SSLKEY, ssl_key); +#endif +#if LIBCURL_VERSION_NUM >= 0x070908 + if (ssl_capath != NULL) + curl_easy_setopt(result, CURLOPT_CAPATH, ssl_capath); +#endif + if (ssl_cainfo != NULL) + curl_easy_setopt(result, CURLOPT_CAINFO, ssl_cainfo); + curl_easy_setopt(result, CURLOPT_FAILONERROR, 1); + + if (curl_low_speed_limit > 0 && curl_low_speed_time > 0) { + curl_easy_setopt(result, CURLOPT_LOW_SPEED_LIMIT, + curl_low_speed_limit); + curl_easy_setopt(result, CURLOPT_LOW_SPEED_TIME, + curl_low_speed_time); + } + + curl_easy_setopt(result, CURLOPT_FOLLOWLOCATION, 1); + + if (getenv("GIT_CURL_VERBOSE")) + curl_easy_setopt(result, CURLOPT_VERBOSE, 1); + + curl_easy_setopt(result, CURLOPT_USERAGENT, GIT_USER_AGENT); + + if (curl_ftp_no_epsv) + curl_easy_setopt(result, CURLOPT_FTP_USE_EPSV, 0); + + return result; +} + +void http_init(void) +{ + char *low_speed_limit; + char *low_speed_time; + + curl_global_init(CURL_GLOBAL_ALL); + + pragma_header = curl_slist_append(pragma_header, "Pragma: no-cache"); + +#ifdef USE_CURL_MULTI + { + char *http_max_requests = getenv("GIT_HTTP_MAX_REQUESTS"); + if (http_max_requests != NULL) + max_requests = atoi(http_max_requests); + } + + curlm = curl_multi_init(); + if (curlm == NULL) { + fprintf(stderr, "Error creating curl multi handle.\n"); + exit(1); + } +#endif + + if (getenv("GIT_SSL_NO_VERIFY")) + curl_ssl_verify = 0; + + ssl_cert = getenv("GIT_SSL_CERT"); +#if LIBCURL_VERSION_NUM >= 0x070902 + ssl_key = getenv("GIT_SSL_KEY"); +#endif +#if LIBCURL_VERSION_NUM >= 0x070908 + ssl_capath = getenv("GIT_SSL_CAPATH"); +#endif + ssl_cainfo = getenv("GIT_SSL_CAINFO"); + + low_speed_limit = getenv("GIT_HTTP_LOW_SPEED_LIMIT"); + if (low_speed_limit != NULL) + curl_low_speed_limit = strtol(low_speed_limit, NULL, 10); + low_speed_time = getenv("GIT_HTTP_LOW_SPEED_TIME"); + if (low_speed_time != NULL) + curl_low_speed_time = strtol(low_speed_time, NULL, 10); + + git_config(http_options); + + if (curl_ssl_verify == -1) + curl_ssl_verify = 1; + +#ifdef USE_CURL_MULTI + if (max_requests < 1) + max_requests = DEFAULT_MAX_REQUESTS; +#endif + + if (getenv("GIT_CURL_FTP_NO_EPSV")) + curl_ftp_no_epsv = 1; + +#ifndef NO_CURL_EASY_DUPHANDLE + curl_default = get_curl_handle(); +#endif +} + +void http_cleanup(void) +{ + struct active_request_slot *slot = active_queue_head; +#ifdef USE_CURL_MULTI + char *wait_url; +#endif + + while (slot != NULL) { +#ifdef USE_CURL_MULTI + if (slot->in_use) { + curl_easy_getinfo(slot->curl, + CURLINFO_EFFECTIVE_URL, + &wait_url); + fprintf(stderr, "Waiting for %s\n", wait_url); + run_active_slot(slot); + } +#endif + if (slot->curl != NULL) + curl_easy_cleanup(slot->curl); + slot = slot->next; + } + +#ifndef NO_CURL_EASY_DUPHANDLE + curl_easy_cleanup(curl_default); +#endif + +#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) +{ + struct active_request_slot *slot = active_queue_head; + struct active_request_slot *newslot; + +#ifdef USE_CURL_MULTI + int num_transfers; + + /* Wait for a slot to open up if the queue is full */ + while (active_requests >= max_requests) { + curl_multi_perform(curlm, &num_transfers); + if (num_transfers < active_requests) { + process_curl_messages(); + } + } +#endif + + while (slot != NULL && slot->in_use) { + slot = slot->next; + } + if (slot == NULL) { + newslot = xmalloc(sizeof(*newslot)); + newslot->curl = NULL; + newslot->in_use = 0; + newslot->next = NULL; + + slot = active_queue_head; + if (slot == NULL) { + active_queue_head = newslot; + } else { + while (slot->next != NULL) { + slot = slot->next; + } + slot->next = newslot; + } + slot = newslot; + } + + if (slot->curl == NULL) { +#ifdef NO_CURL_EASY_DUPHANDLE + slot->curl = get_curl_handle(); +#else + slot->curl = curl_easy_duphandle(curl_default); +#endif + } + + active_requests++; + slot->in_use = 1; + slot->local = NULL; + slot->results = NULL; + 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_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; +} + +int start_active_slot(struct active_request_slot *slot) +{ +#ifdef USE_CURL_MULTI + CURLMcode curlm_result = curl_multi_add_handle(curlm, slot->curl); + + if (curlm_result != CURLM_OK && + curlm_result != CURLM_CALL_MULTI_PERFORM) { + active_requests--; + slot->in_use = 0; + return 0; + } +#endif + return 1; +} + +#ifdef USE_CURL_MULTI +void step_active_slots(void) +{ + int num_transfers; + CURLMcode curlm_result; + + do { + curlm_result = curl_multi_perform(curlm, &num_transfers); + } while (curlm_result == CURLM_CALL_MULTI_PERFORM); + if (num_transfers < active_requests) { + process_curl_messages(); + fill_active_slots(); + } +} +#endif + +void run_active_slot(struct active_request_slot *slot) +{ +#ifdef USE_CURL_MULTI + long last_pos = 0; + long current_pos; + fd_set readfds; + fd_set writefds; + fd_set excfds; + int max_fd; + struct timeval select_timeout; + int finished = 0; + + slot->finished = &finished; + while (!finished) { + data_received = 0; + step_active_slots(); + + if (!data_received && slot->local != NULL) { + current_pos = ftell(slot->local); + if (current_pos > last_pos) + data_received++; + last_pos = current_pos; + } + + if (slot->in_use && !data_received) { + max_fd = 0; + FD_ZERO(&readfds); + FD_ZERO(&writefds); + FD_ZERO(&excfds); + select_timeout.tv_sec = 0; + select_timeout.tv_usec = 50000; + select(max_fd, &readfds, &writefds, + &excfds, &select_timeout); + } + } +#else + while (slot->in_use) { + slot->curl_result = curl_easy_perform(slot->curl); + finish_active_slot(slot); + } +#endif +} + +static void closedown_active_slot(struct active_request_slot *slot) +{ + active_requests--; + slot->in_use = 0; +} + +void release_active_slot(struct active_request_slot *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) +{ + closedown_active_slot(slot); + curl_easy_getinfo(slot->curl, CURLINFO_HTTP_CODE, &slot->http_code); + + if (slot->finished != NULL) + (*slot->finished) = 1; + + /* Store slot results so they can be read after the slot is reused */ + if (slot->results != NULL) { + slot->results->curl_result = slot->curl_result; + slot->results->http_code = slot->http_code; + } + + /* Run callback if appropriate */ + if (slot->callback_func != NULL) { + slot->callback_func(slot->callback_data); + } +} + +void finish_all_active_slots(void) +{ + struct active_request_slot *slot = active_queue_head; + + while (slot != NULL) + if (slot->in_use) { + run_active_slot(slot); + slot = active_queue_head; + } else { + slot = slot->next; + } +} diff --git a/http.h b/http.h new file mode 100644 index 0000000000..324fcf4f54 --- /dev/null +++ b/http.h @@ -0,0 +1,108 @@ +#ifndef HTTP_H +#define HTTP_H + +#include "cache.h" + +#include <curl/curl.h> +#include <curl/easy.h> + +#if LIBCURL_VERSION_NUM >= 0x070908 +#define USE_CURL_MULTI +#define DEFAULT_MAX_REQUESTS 5 +#endif + +#if LIBCURL_VERSION_NUM < 0x070704 +#define curl_global_cleanup() do { /* nothing */ } while(0) +#endif +#if LIBCURL_VERSION_NUM < 0x070800 +#define curl_global_init(a) do { /* nothing */ } while(0) +#endif + +#if (LIBCURL_VERSION_NUM < 0x070c04) || (LIBCURL_VERSION_NUM == 0x071000) +#define NO_CURL_EASY_DUPHANDLE +#endif + +#if LIBCURL_VERSION_NUM < 0x070a03 +#define CURLE_HTTP_RETURNED_ERROR CURLE_HTTP_NOT_FOUND +#endif + +struct slot_results +{ + CURLcode curl_result; + long http_code; +}; + +struct active_request_slot +{ + CURL *curl; + FILE *local; + int in_use; + CURLcode curl_result; + long http_code; + int *finished; + struct slot_results *results; + void *callback_data; + void (*callback_func)(void *data); + struct active_request_slot *next; +}; + +struct buffer +{ + size_t posn; + size_t size; + void *buffer; +}; + +/* Curl request read/write callbacks */ +extern size_t fread_buffer(void *ptr, size_t eltsize, size_t nmemb, + struct buffer *buffer); +extern size_t fwrite_buffer(const void *ptr, size_t eltsize, + size_t nmemb, struct buffer *buffer); +extern size_t fwrite_null(const void *ptr, size_t eltsize, + size_t nmemb, struct buffer *buffer); + +/* Slot lifecycle functions */ +extern struct active_request_slot *get_active_slot(void); +extern int start_active_slot(struct active_request_slot *slot); +extern void run_active_slot(struct active_request_slot *slot); +extern void finish_all_active_slots(void); +extern void release_active_slot(struct active_request_slot *slot); + +#ifdef USE_CURL_MULTI +extern void fill_active_slots(void); +extern void step_active_slots(void); +#endif + +extern void http_init(void); +extern void http_cleanup(void); + +extern int data_received; +extern int active_requests; + +#ifdef USE_CURL_MULTI +extern int max_requests; +extern CURLM *curlm; +#endif +#ifndef NO_CURL_EASY_DUPHANDLE +extern CURL *curl_default; +#endif +extern char curl_errorstr[CURL_ERROR_SIZE]; + +extern int curl_ssl_verify; +extern char *ssl_cert; +#if LIBCURL_VERSION_NUM >= 0x070902 +extern char *ssl_key; +#endif +#if LIBCURL_VERSION_NUM >= 0x070908 +extern char *ssl_capath; +#endif +extern char *ssl_cainfo; +extern long curl_low_speed_limit; +extern long curl_low_speed_time; + +extern struct curl_slist *pragma_header; +extern struct curl_slist *no_range_header; + +extern struct active_request_slot *active_queue_head; + +#endif /* HTTP_H */ diff --git a/ident.c b/ident.c new file mode 100644 index 0000000000..bb03bddd34 --- /dev/null +++ b/ident.c @@ -0,0 +1,248 @@ +/* + * ident.c + * + * create git identifier lines of the form "name <email> date" + * + * Copyright (C) 2005 Linus Torvalds + */ +#include "cache.h" + +static char git_default_date[50]; + +static void copy_gecos(struct passwd *w, char *name, int sz) +{ + char *src, *dst; + int len, nlen; + + nlen = strlen(w->pw_name); + + /* Traditionally GECOS field had office phone numbers etc, separated + * with commas. Also & stands for capitalized form of the login name. + */ + + for (len = 0, dst = name, src = w->pw_gecos; len < sz; src++) { + int ch = *src; + if (ch != '&') { + *dst++ = ch; + if (ch == 0 || ch == ',') + break; + len++; + continue; + } + if (len + nlen < sz) { + /* Sorry, Mr. McDonald... */ + *dst++ = toupper(*w->pw_name); + memcpy(dst, w->pw_name + 1, nlen - 1); + dst += nlen - 1; + } + } + if (len < sz) + name[len] = 0; + else + die("Your parents must have hated you!"); + +} + +static void copy_email(struct passwd *pw) +{ + /* + * Make up a fake email address + * (name + '@' + hostname [+ '.' + domainname]) + */ + int len = strlen(pw->pw_name); + if (len > sizeof(git_default_email)/2) + die("Your sysadmin must hate you!"); + memcpy(git_default_email, pw->pw_name, len); + git_default_email[len++] = '@'; + gethostname(git_default_email + len, sizeof(git_default_email) - len); + if (!strchr(git_default_email+len, '.')) { + struct hostent *he = gethostbyname(git_default_email + len); + char *domainname; + + len = strlen(git_default_email); + git_default_email[len++] = '.'; + if (he && (domainname = strchr(he->h_name, '.'))) + strlcpy(git_default_email + len, domainname + 1, + sizeof(git_default_email) - len); + else + strlcpy(git_default_email + len, "(none)", + sizeof(git_default_email) - len); + } +} + +static void setup_ident(void) +{ + struct passwd *pw = NULL; + + /* Get the name ("gecos") */ + if (!git_default_name[0]) { + pw = getpwuid(getuid()); + if (!pw) + die("You don't exist. Go away!"); + copy_gecos(pw, git_default_name, sizeof(git_default_name)); + } + + if (!git_default_email[0]) { + if (!pw) + pw = getpwuid(getuid()); + if (!pw) + die("You don't exist. Go away!"); + copy_email(pw); + } + + /* And set the default date */ + if (!git_default_date[0]) + datestamp(git_default_date, sizeof(git_default_date)); +} + +static int add_raw(char *buf, int size, int offset, const char *str) +{ + int len = strlen(str); + if (offset + len > size) + return size; + memcpy(buf + offset, str, len); + return offset + len; +} + +static int crud(unsigned char c) +{ + static char crud_array[256]; + static int crud_array_initialized = 0; + + if (!crud_array_initialized) { + int k; + + for (k = 0; k <= 31; ++k) crud_array[k] = 1; + crud_array[' '] = 1; + crud_array['.'] = 1; + crud_array[','] = 1; + crud_array[':'] = 1; + crud_array[';'] = 1; + crud_array['<'] = 1; + crud_array['>'] = 1; + crud_array['"'] = 1; + crud_array['\''] = 1; + crud_array_initialized = 1; + } + return crud_array[c]; +} + +/* + * Copy over a string to the destination, but avoid special + * characters ('\n', '<' and '>') and remove crud at the end + */ +static int copy(char *buf, int size, int offset, const char *src) +{ + int i, len; + unsigned char c; + + /* Remove crud from the beginning.. */ + while ((c = *src) != 0) { + if (!crud(c)) + break; + src++; + } + + /* Remove crud from the end.. */ + len = strlen(src); + while (len > 0) { + c = src[len-1]; + if (!crud(c)) + break; + --len; + } + + /* + * Copy the rest to the buffer, but avoid the special + * characters '\n' '<' and '>' that act as delimiters on + * a identification line + */ + for (i = 0; i < len; i++) { + c = *src++; + switch (c) { + case '\n': case '<': case '>': + continue; + } + if (offset >= size) + return size; + buf[offset++] = c; + } + return offset; +} + +static const char au_env[] = "GIT_AUTHOR_NAME"; +static const char co_env[] = "GIT_COMMITTER_NAME"; +static const char *env_hint = +"\n" +"*** Your name cannot be determined from your system services (gecos).\n" +"\n" +"Run\n" +"\n" +" git config user.email \"you@email.com\"\n" +" git config user.name \"Your Name\"\n" +"\n" +"To set the identity in this repository.\n" +"Add --global to set your account\'s default\n" +"\n"; + +const char *fmt_ident(const char *name, const char *email, + const char *date_str, int error_on_no_name) +{ + static char buffer[1000]; + char date[50]; + int i; + + setup_ident(); + if (!name) + name = git_default_name; + if (!email) + email = git_default_email; + + if (!*name) { + struct passwd *pw; + + if (0 <= error_on_no_name && + name == git_default_name && env_hint) { + fprintf(stderr, env_hint, au_env, co_env); + env_hint = NULL; /* warn only once, for "git-var -l" */ + } + if (0 < error_on_no_name) + die("empty ident %s <%s> not allowed", name, email); + pw = getpwuid(getuid()); + if (!pw) + die("You don't exist. Go away!"); + strlcpy(git_default_name, pw->pw_name, + sizeof(git_default_name)); + name = git_default_name; + } + + strcpy(date, git_default_date); + if (date_str) + parse_date(date_str, date, sizeof(date)); + + i = copy(buffer, sizeof(buffer), 0, name); + i = add_raw(buffer, sizeof(buffer), i, " <"); + i = copy(buffer, sizeof(buffer), i, email); + i = add_raw(buffer, sizeof(buffer), i, "> "); + i = copy(buffer, sizeof(buffer), i, date); + if (i >= sizeof(buffer)) + die("Impossibly long personal identifier"); + buffer[i] = 0; + return buffer; +} + +const char *git_author_info(int error_on_no_name) +{ + return fmt_ident(getenv("GIT_AUTHOR_NAME"), + getenv("GIT_AUTHOR_EMAIL"), + getenv("GIT_AUTHOR_DATE"), + error_on_no_name); +} + +const char *git_committer_info(int error_on_no_name) +{ + return fmt_ident(getenv("GIT_COMMITTER_NAME"), + getenv("GIT_COMMITTER_EMAIL"), + getenv("GIT_COMMITTER_DATE"), + error_on_no_name); +} diff --git a/imap-send.c b/imap-send.c new file mode 100644 index 0000000000..3eaf025720 --- /dev/null +++ b/imap-send.c @@ -0,0 +1,1345 @@ +/* + * git-imap-send - drops patches into an imap Drafts folder + * derived from isync/mbsync - mailbox synchronizer + * + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * Copyright (C) 2004 Theodore Y. Ts'o <tytso@mit.edu> + * Copyright (C) 2006 Mike McCormack + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "cache.h" + +typedef struct store_conf { + char *name; + const char *path; /* should this be here? its interpretation is driver-specific */ + char *map_inbox; + char *trash; + unsigned max_size; /* off_t is overkill */ + unsigned trash_remote_new:1, trash_only_new:1; +} store_conf_t; + +typedef struct string_list { + struct string_list *next; + char string[1]; +} string_list_t; + +typedef struct channel_conf { + struct channel_conf *next; + char *name; + store_conf_t *master, *slave; + char *master_name, *slave_name; + char *sync_state; + string_list_t *patterns; + int mops, sops; + unsigned max_messages; /* for slave only */ +} channel_conf_t; + +typedef struct group_conf { + struct group_conf *next; + char *name; + string_list_t *channels; +} group_conf_t; + +/* For message->status */ +#define M_RECENT (1<<0) /* unsyncable flag; maildir_* depend on this being 1<<0 */ +#define M_DEAD (1<<1) /* expunged */ +#define M_FLAGS (1<<2) /* flags fetched */ + +typedef struct message { + struct message *next; + /* string_list_t *keywords; */ + size_t size; /* zero implies "not fetched" */ + int uid; + unsigned char flags, status; +} message_t; + +typedef struct store { + store_conf_t *conf; /* foreign */ + + /* currently open mailbox */ + const char *name; /* foreign! maybe preset? */ + char *path; /* own */ + message_t *msgs; /* own */ + int uidvalidity; + unsigned char opts; /* maybe preset? */ + /* note that the following do _not_ reflect stats from msgs, but mailbox totals */ + int count; /* # of messages */ + int recent; /* # of recent messages - don't trust this beyond the initial read */ +} store_t; + +typedef struct { + char *data; + int len; + unsigned char flags; + unsigned int crlf:1; +} msg_data_t; + +#define DRV_OK 0 +#define DRV_MSG_BAD -1 +#define DRV_BOX_BAD -2 +#define DRV_STORE_BAD -3 + +static int Verbose, Quiet; + +static void imap_info( const char *, ... ); +static void imap_warn( const char *, ... ); + +static char *next_arg( char ** ); + +static void free_generic_messages( message_t * ); + +static int nfsnprintf( char *buf, int blen, const char *fmt, ... ); + + +static void arc4_init( void ); +static unsigned char arc4_getbyte( void ); + +typedef struct imap_server_conf { + char *name; + char *tunnel; + char *host; + int port; + char *user; + char *pass; +} imap_server_conf_t; + +typedef struct imap_store_conf { + store_conf_t gen; + imap_server_conf_t *server; + unsigned use_namespace:1; +} imap_store_conf_t; + +#define NIL (void*)0x1 +#define LIST (void*)0x2 + +typedef struct _list { + struct _list *next, *child; + char *val; + int len; +} list_t; + +typedef struct { + int fd; +} Socket_t; + +typedef struct { + Socket_t sock; + int bytes; + int offset; + char buf[1024]; +} buffer_t; + +struct imap_cmd; + +typedef struct imap { + int uidnext; /* from SELECT responses */ + list_t *ns_personal, *ns_other, *ns_shared; /* NAMESPACE info */ + unsigned caps, rcaps; /* CAPABILITY results */ + /* command queue */ + int nexttag, num_in_progress, literal_pending; + struct imap_cmd *in_progress, **in_progress_append; + buffer_t buf; /* this is BIG, so put it last */ +} imap_t; + +typedef struct imap_store { + store_t gen; + int uidvalidity; + imap_t *imap; + const char *prefix; + unsigned /*currentnc:1,*/ trashnc:1; +} imap_store_t; + +struct imap_cmd_cb { + int (*cont)( imap_store_t *ctx, struct imap_cmd *cmd, const char *prompt ); + void (*done)( imap_store_t *ctx, struct imap_cmd *cmd, int response); + void *ctx; + char *data; + int dlen; + int uid; + unsigned create:1, trycreate:1; +}; + +struct imap_cmd { + struct imap_cmd *next; + struct imap_cmd_cb cb; + char *cmd; + int tag; +}; + +#define CAP(cap) (imap->caps & (1 << (cap))) + +enum CAPABILITY { + NOLOGIN = 0, + UIDPLUS, + LITERALPLUS, + NAMESPACE, +}; + +static const char *cap_list[] = { + "LOGINDISABLED", + "UIDPLUS", + "LITERAL+", + "NAMESPACE", +}; + +#define RESP_OK 0 +#define RESP_NO 1 +#define RESP_BAD 2 + +static int get_cmd_result( imap_store_t *ctx, struct imap_cmd *tcmd ); + + +static const char *Flags[] = { + "Draft", + "Flagged", + "Answered", + "Seen", + "Deleted", +}; + +static void +socket_perror( const char *func, Socket_t *sock, int ret ) +{ + if (ret < 0) + perror( func ); + else + fprintf( stderr, "%s: unexpected EOF\n", func ); +} + +static int +socket_read( Socket_t *sock, char *buf, int len ) +{ + int n = xread( sock->fd, buf, len ); + if (n <= 0) { + socket_perror( "read", sock, n ); + close( sock->fd ); + sock->fd = -1; + } + return n; +} + +static int +socket_write( Socket_t *sock, const char *buf, int len ) +{ + int n = write_in_full( sock->fd, buf, len ); + if (n != len) { + socket_perror( "write", sock, n ); + close( sock->fd ); + sock->fd = -1; + } + return n; +} + +/* simple line buffering */ +static int +buffer_gets( buffer_t * b, char **s ) +{ + int n; + int start = b->offset; + + *s = b->buf + start; + + for (;;) { + /* make sure we have enough data to read the \r\n sequence */ + if (b->offset + 1 >= b->bytes) { + if (start) { + /* shift down used bytes */ + *s = b->buf; + + assert( start <= b->bytes ); + n = b->bytes - start; + + if (n) + memmove(b->buf, b->buf + start, n); + b->offset -= start; + b->bytes = n; + start = 0; + } + + n = socket_read( &b->sock, b->buf + b->bytes, + sizeof(b->buf) - b->bytes ); + + if (n <= 0) + return -1; + + b->bytes += n; + } + + if (b->buf[b->offset] == '\r') { + assert( b->offset + 1 < b->bytes ); + if (b->buf[b->offset + 1] == '\n') { + b->buf[b->offset] = 0; /* terminate the string */ + b->offset += 2; /* next line */ + if (Verbose) + puts( *s ); + return 0; + } + } + + b->offset++; + } + /* not reached */ +} + +static void +imap_info( const char *msg, ... ) +{ + va_list va; + + if (!Quiet) { + va_start( va, msg ); + vprintf( msg, va ); + va_end( va ); + fflush( stdout ); + } +} + +static void +imap_warn( const char *msg, ... ) +{ + va_list va; + + if (Quiet < 2) { + va_start( va, msg ); + vfprintf( stderr, msg, va ); + va_end( va ); + } +} + +static char * +next_arg( char **s ) +{ + char *ret; + + if (!s || !*s) + return NULL; + while (isspace( (unsigned char) **s )) + (*s)++; + if (!**s) { + *s = NULL; + return NULL; + } + if (**s == '"') { + ++*s; + ret = *s; + *s = strchr( *s, '"' ); + } else { + ret = *s; + while (**s && !isspace( (unsigned char) **s )) + (*s)++; + } + if (*s) { + if (**s) + *(*s)++ = 0; + if (!**s) + *s = NULL; + } + return ret; +} + +static void +free_generic_messages( message_t *msgs ) +{ + message_t *tmsg; + + for (; msgs; msgs = tmsg) { + tmsg = msgs->next; + free( msgs ); + } +} + +static int +nfsnprintf( char *buf, int blen, const char *fmt, ... ) +{ + int ret; + va_list va; + + va_start( va, fmt ); + if (blen <= 0 || (unsigned)(ret = vsnprintf( buf, blen, fmt, va )) >= (unsigned)blen) + die( "Fatal: buffer too small. Please report a bug.\n"); + va_end( va ); + return ret; +} + +static struct { + unsigned char i, j, s[256]; +} rs; + +static void +arc4_init( void ) +{ + int i, fd; + unsigned char j, si, dat[128]; + + if ((fd = open( "/dev/urandom", O_RDONLY )) < 0 && (fd = open( "/dev/random", O_RDONLY )) < 0) { + fprintf( stderr, "Fatal: no random number source available.\n" ); + exit( 3 ); + } + if (read_in_full( fd, dat, 128 ) != 128) { + fprintf( stderr, "Fatal: cannot read random number source.\n" ); + exit( 3 ); + } + close( fd ); + + for (i = 0; i < 256; i++) + rs.s[i] = i; + for (i = j = 0; i < 256; i++) { + si = rs.s[i]; + j += si + dat[i & 127]; + rs.s[i] = rs.s[j]; + rs.s[j] = si; + } + rs.i = rs.j = 0; + + for (i = 0; i < 256; i++) + arc4_getbyte(); +} + +static unsigned char +arc4_getbyte( void ) +{ + unsigned char si, sj; + + rs.i++; + si = rs.s[rs.i]; + rs.j += si; + sj = rs.s[rs.j]; + rs.s[rs.i] = sj; + rs.s[rs.j] = si; + return rs.s[(si + sj) & 0xff]; +} + +static struct imap_cmd * +v_issue_imap_cmd( imap_store_t *ctx, struct imap_cmd_cb *cb, + const char *fmt, va_list ap ) +{ + imap_t *imap = ctx->imap; + struct imap_cmd *cmd; + int n, bufl; + char buf[1024]; + + cmd = xmalloc( sizeof(struct imap_cmd) ); + nfvasprintf( &cmd->cmd, fmt, ap ); + cmd->tag = ++imap->nexttag; + + if (cb) + cmd->cb = *cb; + else + memset( &cmd->cb, 0, sizeof(cmd->cb) ); + + while (imap->literal_pending) + get_cmd_result( ctx, NULL ); + + bufl = nfsnprintf( buf, sizeof(buf), cmd->cb.data ? CAP(LITERALPLUS) ? + "%d %s{%d+}\r\n" : "%d %s{%d}\r\n" : "%d %s\r\n", + cmd->tag, cmd->cmd, cmd->cb.dlen ); + if (Verbose) { + if (imap->num_in_progress) + printf( "(%d in progress) ", imap->num_in_progress ); + if (memcmp( cmd->cmd, "LOGIN", 5 )) + printf( ">>> %s", buf ); + else + printf( ">>> %d LOGIN <user> <pass>\n", cmd->tag ); + } + if (socket_write( &imap->buf.sock, buf, bufl ) != bufl) { + free( cmd->cmd ); + free( cmd ); + if (cb && cb->data) + free( cb->data ); + return NULL; + } + if (cmd->cb.data) { + if (CAP(LITERALPLUS)) { + n = socket_write( &imap->buf.sock, cmd->cb.data, cmd->cb.dlen ); + free( cmd->cb.data ); + if (n != cmd->cb.dlen || + (n = socket_write( &imap->buf.sock, "\r\n", 2 )) != 2) + { + free( cmd->cmd ); + free( cmd ); + return NULL; + } + cmd->cb.data = NULL; + } else + imap->literal_pending = 1; + } else if (cmd->cb.cont) + imap->literal_pending = 1; + cmd->next = NULL; + *imap->in_progress_append = cmd; + imap->in_progress_append = &cmd->next; + imap->num_in_progress++; + return cmd; +} + +static struct imap_cmd * +issue_imap_cmd( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... ) +{ + struct imap_cmd *ret; + va_list ap; + + va_start( ap, fmt ); + ret = v_issue_imap_cmd( ctx, cb, fmt, ap ); + va_end( ap ); + return ret; +} + +static int +imap_exec( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... ) +{ + va_list ap; + struct imap_cmd *cmdp; + + va_start( ap, fmt ); + cmdp = v_issue_imap_cmd( ctx, cb, fmt, ap ); + va_end( ap ); + if (!cmdp) + return RESP_BAD; + + return get_cmd_result( ctx, cmdp ); +} + +static int +imap_exec_m( imap_store_t *ctx, struct imap_cmd_cb *cb, const char *fmt, ... ) +{ + va_list ap; + struct imap_cmd *cmdp; + + va_start( ap, fmt ); + cmdp = v_issue_imap_cmd( ctx, cb, fmt, ap ); + va_end( ap ); + if (!cmdp) + return DRV_STORE_BAD; + + switch (get_cmd_result( ctx, cmdp )) { + case RESP_BAD: return DRV_STORE_BAD; + case RESP_NO: return DRV_MSG_BAD; + default: return DRV_OK; + } +} + +static int +is_atom( list_t *list ) +{ + return list && list->val && list->val != NIL && list->val != LIST; +} + +static int +is_list( list_t *list ) +{ + return list && list->val == LIST; +} + +static void +free_list( list_t *list ) +{ + list_t *tmp; + + for (; list; list = tmp) { + tmp = list->next; + if (is_list( list )) + free_list( list->child ); + else if (is_atom( list )) + free( list->val ); + free( list ); + } +} + +static int +parse_imap_list_l( imap_t *imap, char **sp, list_t **curp, int level ) +{ + list_t *cur; + char *s = *sp, *p; + int n, bytes; + + for (;;) { + while (isspace( (unsigned char)*s )) + s++; + if (level && *s == ')') { + s++; + break; + } + *curp = cur = xmalloc( sizeof(*cur) ); + curp = &cur->next; + cur->val = NULL; /* for clean bail */ + if (*s == '(') { + /* sublist */ + s++; + cur->val = LIST; + if (parse_imap_list_l( imap, &s, &cur->child, level + 1 )) + goto bail; + } else if (imap && *s == '{') { + /* literal */ + bytes = cur->len = strtol( s + 1, &s, 10 ); + if (*s != '}') + goto bail; + + s = cur->val = xmalloc( cur->len ); + + /* dump whats left over in the input buffer */ + n = imap->buf.bytes - imap->buf.offset; + + if (n > bytes) + /* the entire message fit in the buffer */ + n = bytes; + + memcpy( s, imap->buf.buf + imap->buf.offset, n ); + s += n; + bytes -= n; + + /* mark that we used part of the buffer */ + imap->buf.offset += n; + + /* now read the rest of the message */ + while (bytes > 0) { + if ((n = socket_read (&imap->buf.sock, s, bytes)) <= 0) + goto bail; + s += n; + bytes -= n; + } + + if (buffer_gets( &imap->buf, &s )) + goto bail; + } else if (*s == '"') { + /* quoted string */ + s++; + p = s; + for (; *s != '"'; s++) + if (!*s) + goto bail; + cur->len = s - p; + s++; + cur->val = xmalloc( cur->len + 1 ); + memcpy( cur->val, p, cur->len ); + cur->val[cur->len] = 0; + } else { + /* atom */ + p = s; + for (; *s && !isspace( (unsigned char)*s ); s++) + if (level && *s == ')') + break; + cur->len = s - p; + if (cur->len == 3 && !memcmp ("NIL", p, 3)) + cur->val = NIL; + else { + cur->val = xmalloc( cur->len + 1 ); + memcpy( cur->val, p, cur->len ); + cur->val[cur->len] = 0; + } + } + + if (!level) + break; + if (!*s) + goto bail; + } + *sp = s; + *curp = NULL; + return 0; + + bail: + *curp = NULL; + return -1; +} + +static list_t * +parse_imap_list( imap_t *imap, char **sp ) +{ + list_t *head; + + if (!parse_imap_list_l( imap, sp, &head, 0 )) + return head; + free_list( head ); + return NULL; +} + +static list_t * +parse_list( char **sp ) +{ + return parse_imap_list( NULL, sp ); +} + +static void +parse_capability( imap_t *imap, char *cmd ) +{ + char *arg; + unsigned i; + + imap->caps = 0x80000000; + while ((arg = next_arg( &cmd ))) + for (i = 0; i < ARRAY_SIZE(cap_list); i++) + if (!strcmp( cap_list[i], arg )) + imap->caps |= 1 << i; + imap->rcaps = imap->caps; +} + +static int +parse_response_code( imap_store_t *ctx, struct imap_cmd_cb *cb, char *s ) +{ + imap_t *imap = ctx->imap; + char *arg, *p; + + if (*s != '[') + return RESP_OK; /* no response code */ + s++; + if (!(p = strchr( s, ']' ))) { + fprintf( stderr, "IMAP error: malformed response code\n" ); + return RESP_BAD; + } + *p++ = 0; + arg = next_arg( &s ); + if (!strcmp( "UIDVALIDITY", arg )) { + if (!(arg = next_arg( &s )) || !(ctx->gen.uidvalidity = atoi( arg ))) { + fprintf( stderr, "IMAP error: malformed UIDVALIDITY status\n" ); + return RESP_BAD; + } + } else if (!strcmp( "UIDNEXT", arg )) { + if (!(arg = next_arg( &s )) || !(imap->uidnext = atoi( arg ))) { + fprintf( stderr, "IMAP error: malformed NEXTUID status\n" ); + return RESP_BAD; + } + } else if (!strcmp( "CAPABILITY", arg )) { + parse_capability( imap, s ); + } else if (!strcmp( "ALERT", arg )) { + /* RFC2060 says that these messages MUST be displayed + * to the user + */ + for (; isspace( (unsigned char)*p ); p++); + fprintf( stderr, "*** IMAP ALERT *** %s\n", p ); + } else if (cb && cb->ctx && !strcmp( "APPENDUID", arg )) { + if (!(arg = next_arg( &s )) || !(ctx->gen.uidvalidity = atoi( arg )) || + !(arg = next_arg( &s )) || !(*(int *)cb->ctx = atoi( arg ))) + { + fprintf( stderr, "IMAP error: malformed APPENDUID status\n" ); + return RESP_BAD; + } + } + return RESP_OK; +} + +static int +get_cmd_result( imap_store_t *ctx, struct imap_cmd *tcmd ) +{ + imap_t *imap = ctx->imap; + struct imap_cmd *cmdp, **pcmdp, *ncmdp; + char *cmd, *arg, *arg1, *p; + int n, resp, resp2, tag; + + for (;;) { + if (buffer_gets( &imap->buf, &cmd )) + return RESP_BAD; + + arg = next_arg( &cmd ); + if (*arg == '*') { + arg = next_arg( &cmd ); + if (!arg) { + fprintf( stderr, "IMAP error: unable to parse untagged response\n" ); + return RESP_BAD; + } + + if (!strcmp( "NAMESPACE", arg )) { + imap->ns_personal = parse_list( &cmd ); + imap->ns_other = parse_list( &cmd ); + imap->ns_shared = parse_list( &cmd ); + } else if (!strcmp( "OK", arg ) || !strcmp( "BAD", arg ) || + !strcmp( "NO", arg ) || !strcmp( "BYE", arg )) { + if ((resp = parse_response_code( ctx, NULL, cmd )) != RESP_OK) + return resp; + } else if (!strcmp( "CAPABILITY", arg )) + parse_capability( imap, cmd ); + else if ((arg1 = next_arg( &cmd ))) { + if (!strcmp( "EXISTS", arg1 )) + ctx->gen.count = atoi( arg ); + else if (!strcmp( "RECENT", arg1 )) + ctx->gen.recent = atoi( arg ); + } else { + fprintf( stderr, "IMAP error: unable to parse untagged response\n" ); + return RESP_BAD; + } + } else if (!imap->in_progress) { + fprintf( stderr, "IMAP error: unexpected reply: %s %s\n", arg, cmd ? cmd : "" ); + return RESP_BAD; + } else if (*arg == '+') { + /* This can happen only with the last command underway, as + it enforces a round-trip. */ + cmdp = (struct imap_cmd *)((char *)imap->in_progress_append - + offsetof(struct imap_cmd, next)); + if (cmdp->cb.data) { + n = socket_write( &imap->buf.sock, cmdp->cb.data, cmdp->cb.dlen ); + free( cmdp->cb.data ); + cmdp->cb.data = NULL; + if (n != (int)cmdp->cb.dlen) + return RESP_BAD; + } else if (cmdp->cb.cont) { + if (cmdp->cb.cont( ctx, cmdp, cmd )) + return RESP_BAD; + } else { + fprintf( stderr, "IMAP error: unexpected command continuation request\n" ); + return RESP_BAD; + } + if (socket_write( &imap->buf.sock, "\r\n", 2 ) != 2) + return RESP_BAD; + if (!cmdp->cb.cont) + imap->literal_pending = 0; + if (!tcmd) + return DRV_OK; + } else { + tag = atoi( arg ); + for (pcmdp = &imap->in_progress; (cmdp = *pcmdp); pcmdp = &cmdp->next) + if (cmdp->tag == tag) + goto gottag; + fprintf( stderr, "IMAP error: unexpected tag %s\n", arg ); + return RESP_BAD; + gottag: + if (!(*pcmdp = cmdp->next)) + imap->in_progress_append = pcmdp; + imap->num_in_progress--; + if (cmdp->cb.cont || cmdp->cb.data) + imap->literal_pending = 0; + arg = next_arg( &cmd ); + if (!strcmp( "OK", arg )) + resp = DRV_OK; + else { + if (!strcmp( "NO", arg )) { + if (cmdp->cb.create && cmd && (cmdp->cb.trycreate || !memcmp( cmd, "[TRYCREATE]", 11 ))) { /* SELECT, APPEND or UID COPY */ + p = strchr( cmdp->cmd, '"' ); + if (!issue_imap_cmd( ctx, NULL, "CREATE \"%.*s\"", strchr( p + 1, '"' ) - p + 1, p )) { + resp = RESP_BAD; + goto normal; + } + /* not waiting here violates the spec, but a server that does not + grok this nonetheless violates it too. */ + cmdp->cb.create = 0; + if (!(ncmdp = issue_imap_cmd( ctx, &cmdp->cb, "%s", cmdp->cmd ))) { + resp = RESP_BAD; + goto normal; + } + free( cmdp->cmd ); + free( cmdp ); + if (!tcmd) + return 0; /* ignored */ + if (cmdp == tcmd) + tcmd = ncmdp; + continue; + } + resp = RESP_NO; + } else /*if (!strcmp( "BAD", arg ))*/ + resp = RESP_BAD; + fprintf( stderr, "IMAP command '%s' returned response (%s) - %s\n", + memcmp (cmdp->cmd, "LOGIN", 5) ? + cmdp->cmd : "LOGIN <user> <pass>", + arg, cmd ? cmd : ""); + } + if ((resp2 = parse_response_code( ctx, &cmdp->cb, cmd )) > resp) + resp = resp2; + normal: + if (cmdp->cb.done) + cmdp->cb.done( ctx, cmdp, resp ); + if (cmdp->cb.data) + free( cmdp->cb.data ); + free( cmdp->cmd ); + free( cmdp ); + if (!tcmd || tcmd == cmdp) + return resp; + } + } + /* not reached */ +} + +static void +imap_close_server( imap_store_t *ictx ) +{ + imap_t *imap = ictx->imap; + + if (imap->buf.sock.fd != -1) { + imap_exec( ictx, NULL, "LOGOUT" ); + close( imap->buf.sock.fd ); + } + free_list( imap->ns_personal ); + free_list( imap->ns_other ); + free_list( imap->ns_shared ); + free( imap ); +} + +static void +imap_close_store( store_t *ctx ) +{ + imap_close_server( (imap_store_t *)ctx ); + free_generic_messages( ctx->msgs ); + free( ctx ); +} + +static store_t * +imap_open_store( imap_server_conf_t *srvc ) +{ + imap_store_t *ctx; + imap_t *imap; + char *arg, *rsp; + struct hostent *he; + struct sockaddr_in addr; + int s, a[2], preauth; + pid_t pid; + + ctx = xcalloc( sizeof(*ctx), 1 ); + + ctx->imap = imap = xcalloc( sizeof(*imap), 1 ); + imap->buf.sock.fd = -1; + imap->in_progress_append = &imap->in_progress; + + /* open connection to IMAP server */ + + if (srvc->tunnel) { + imap_info( "Starting tunnel '%s'... ", srvc->tunnel ); + + if (socketpair( PF_UNIX, SOCK_STREAM, 0, a )) { + perror( "socketpair" ); + exit( 1 ); + } + + 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] ); + close( a[1] ); + execl( "/bin/sh", "sh", "-c", srvc->tunnel, NULL ); + _exit( 127 ); + } + + close (a[0]); + + imap->buf.sock.fd = a[1]; + + imap_info( "ok\n" ); + } else { + memset( &addr, 0, sizeof(addr) ); + addr.sin_port = htons( srvc->port ); + addr.sin_family = AF_INET; + + imap_info( "Resolving %s... ", srvc->host ); + he = gethostbyname( srvc->host ); + if (!he) { + perror( "gethostbyname" ); + goto bail; + } + imap_info( "ok\n" ); + + addr.sin_addr.s_addr = *((int *) he->h_addr_list[0]); + + s = socket( PF_INET, SOCK_STREAM, 0 ); + + imap_info( "Connecting to %s:%hu... ", inet_ntoa( addr.sin_addr ), ntohs( addr.sin_port ) ); + if (connect( s, (struct sockaddr *)&addr, sizeof(addr) )) { + close( s ); + perror( "connect" ); + goto bail; + } + imap_info( "ok\n" ); + + imap->buf.sock.fd = s; + + } + + /* read the greeting string */ + if (buffer_gets( &imap->buf, &rsp )) { + fprintf( stderr, "IMAP error: no greeting response\n" ); + goto bail; + } + arg = next_arg( &rsp ); + if (!arg || *arg != '*' || (arg = next_arg( &rsp )) == NULL) { + fprintf( stderr, "IMAP error: invalid greeting response\n" ); + goto bail; + } + preauth = 0; + if (!strcmp( "PREAUTH", arg )) + preauth = 1; + else if (strcmp( "OK", arg ) != 0) { + fprintf( stderr, "IMAP error: unknown greeting response\n" ); + goto bail; + } + parse_response_code( ctx, NULL, rsp ); + if (!imap->caps && imap_exec( ctx, NULL, "CAPABILITY" ) != RESP_OK) + goto bail; + + if (!preauth) { + + imap_info ("Logging in...\n"); + if (!srvc->user) { + fprintf( stderr, "Skipping server %s, no user\n", srvc->host ); + goto bail; + } + if (!srvc->pass) { + char prompt[80]; + sprintf( prompt, "Password (%s@%s): ", srvc->user, srvc->host ); + arg = getpass( prompt ); + if (!arg) { + perror( "getpass" ); + exit( 1 ); + } + if (!*arg) { + fprintf( stderr, "Skipping account %s@%s, no password\n", srvc->user, srvc->host ); + goto bail; + } + /* + * getpass() returns a pointer to a static buffer. make a copy + * for long term storage. + */ + srvc->pass = xstrdup( arg ); + } + if (CAP(NOLOGIN)) { + fprintf( stderr, "Skipping account %s@%s, server forbids LOGIN\n", srvc->user, srvc->host ); + goto bail; + } + imap_warn( "*** IMAP Warning *** Password is being sent in the clear\n" ); + if (imap_exec( ctx, NULL, "LOGIN \"%s\" \"%s\"", srvc->user, srvc->pass ) != RESP_OK) { + fprintf( stderr, "IMAP error: LOGIN failed\n" ); + goto bail; + } + } /* !preauth */ + + ctx->prefix = ""; + ctx->trashnc = 1; + return (store_t *)ctx; + + bail: + imap_close_store( &ctx->gen ); + return NULL; +} + +static int +imap_make_flags( int flags, char *buf ) +{ + const char *s; + unsigned i, d; + + for (i = d = 0; i < ARRAY_SIZE(Flags); i++) + if (flags & (1 << i)) { + buf[d++] = ' '; + buf[d++] = '\\'; + for (s = Flags[i]; *s; s++) + buf[d++] = *s; + } + buf[0] = '('; + buf[d++] = ')'; + return d; +} + +#define TUIDL 8 + +static int +imap_store_msg( store_t *gctx, msg_data_t *data, int *uid ) +{ + imap_store_t *ctx = (imap_store_t *)gctx; + imap_t *imap = ctx->imap; + struct imap_cmd_cb cb; + char *fmap, *buf; + const char *prefix, *box; + int ret, i, j, d, len, extra, nocr; + int start, sbreak = 0, ebreak = 0; + char flagstr[128], tuid[TUIDL * 2 + 1]; + + memset( &cb, 0, sizeof(cb) ); + + fmap = data->data; + len = data->len; + nocr = !data->crlf; + extra = 0, i = 0; + if (!CAP(UIDPLUS) && uid) { + nloop: + start = i; + while (i < len) + if (fmap[i++] == '\n') { + extra += nocr; + if (i - 2 + nocr == start) { + sbreak = ebreak = i - 2 + nocr; + goto mktid; + } + if (!memcmp( fmap + start, "X-TUID: ", 8 )) { + extra -= (ebreak = i) - (sbreak = start) + nocr; + goto mktid; + } + goto nloop; + } + /* invalid message */ + free( fmap ); + return DRV_MSG_BAD; + mktid: + for (j = 0; j < TUIDL; j++) + sprintf( tuid + j * 2, "%02x", arc4_getbyte() ); + extra += 8 + TUIDL * 2 + 2; + } + if (nocr) + for (; i < len; i++) + if (fmap[i] == '\n') + extra++; + + cb.dlen = len + extra; + buf = cb.data = xmalloc( cb.dlen ); + i = 0; + if (!CAP(UIDPLUS) && uid) { + if (nocr) { + for (; i < sbreak; i++) + if (fmap[i] == '\n') { + *buf++ = '\r'; + *buf++ = '\n'; + } else + *buf++ = fmap[i]; + } else { + memcpy( buf, fmap, sbreak ); + buf += sbreak; + } + memcpy( buf, "X-TUID: ", 8 ); + buf += 8; + memcpy( buf, tuid, TUIDL * 2 ); + buf += TUIDL * 2; + *buf++ = '\r'; + *buf++ = '\n'; + i = ebreak; + } + if (nocr) { + for (; i < len; i++) + if (fmap[i] == '\n') { + *buf++ = '\r'; + *buf++ = '\n'; + } else + *buf++ = fmap[i]; + } else + memcpy( buf, fmap + i, len - i ); + + free( fmap ); + + d = 0; + if (data->flags) { + d = imap_make_flags( data->flags, flagstr ); + flagstr[d++] = ' '; + } + flagstr[d] = 0; + + if (!uid) { + box = gctx->conf->trash; + prefix = ctx->prefix; + cb.create = 1; + if (ctx->trashnc) + imap->caps = imap->rcaps & ~(1 << LITERALPLUS); + } else { + box = gctx->name; + prefix = !strcmp( box, "INBOX" ) ? "" : ctx->prefix; + cb.create = 0; + } + cb.ctx = uid; + ret = imap_exec_m( ctx, &cb, "APPEND \"%s%s\" %s", prefix, box, flagstr ); + imap->caps = imap->rcaps; + if (ret != DRV_OK) + return ret; + if (!uid) + ctx->trashnc = 0; + else + gctx->count++; + + return DRV_OK; +} + +#define CHUNKSIZE 0x1000 + +static int +read_message( FILE *f, msg_data_t *msg ) +{ + int len, r; + + memset( msg, 0, sizeof *msg ); + len = CHUNKSIZE; + msg->data = xmalloc( len+1 ); + msg->data[0] = 0; + + while(!feof( f )) { + if (msg->len >= len) { + void *p; + len += CHUNKSIZE; + p = xrealloc(msg->data, len+1); + if (!p) + break; + msg->data = p; + } + r = fread( &msg->data[msg->len], 1, len - msg->len, f ); + if (r <= 0) + break; + msg->len += r; + } + msg->data[msg->len] = 0; + return msg->len; +} + +static int +count_messages( msg_data_t *msg ) +{ + int count = 0; + char *p = msg->data; + + while (1) { + if (!strncmp( "From ", p, 5 )) { + count++; + p += 5; + } + p = strstr( p+5, "\nFrom "); + if (!p) + break; + p++; + } + return count; +} + +static int +split_msg( msg_data_t *all_msgs, msg_data_t *msg, int *ofs ) +{ + char *p, *data; + + memset( msg, 0, sizeof *msg ); + if (*ofs >= all_msgs->len) + return 0; + + data = &all_msgs->data[ *ofs ]; + msg->len = all_msgs->len - *ofs; + + if (msg->len < 5 || strncmp( data, "From ", 5 )) + return 0; + + p = strchr( data, '\n' ); + if (p) { + p = &p[1]; + msg->len -= p-data; + *ofs += p-data; + data = p; + } + + p = strstr( data, "\nFrom " ); + if (p) + msg->len = &p[1] - data; + + msg->data = xmalloc( msg->len + 1 ); + if (!msg->data) + return 0; + + memcpy( msg->data, data, msg->len ); + msg->data[ msg->len ] = 0; + + *ofs += msg->len; + return 1; +} + +static imap_server_conf_t server = +{ + NULL, /* name */ + NULL, /* tunnel */ + NULL, /* host */ + 0, /* port */ + NULL, /* user */ + NULL, /* pass */ +}; + +static char *imap_folder; + +static int +git_imap_config(const char *key, const char *val) +{ + char imap_key[] = "imap."; + + if (strncmp( key, imap_key, sizeof imap_key - 1 )) + return 0; + key += sizeof imap_key - 1; + + if (!strcmp( "folder", key )) { + imap_folder = xstrdup( val ); + } else if (!strcmp( "host", key )) { + { + if (!strncmp( "imap:", val, 5 )) + val += 5; + if (!server.port) + server.port = 143; + } + if (!strncmp( "//", val, 2 )) + val += 2; + server.host = xstrdup( val ); + } + else if (!strcmp( "user", key )) + server.user = xstrdup( val ); + else if (!strcmp( "pass", key )) + server.pass = xstrdup( val ); + else if (!strcmp( "port", key )) + server.port = git_config_int( key, val ); + else if (!strcmp( "tunnel", key )) + server.tunnel = xstrdup( val ); + return 0; +} + +int +main(int argc, char **argv) +{ + msg_data_t all_msgs, msg; + store_t *ctx = NULL; + int uid = 0; + int ofs = 0; + int r; + int total, n = 0; + + /* init the random number generator */ + arc4_init(); + + git_config( git_imap_config ); + + if (!imap_folder) { + fprintf( stderr, "no imap store specified\n" ); + return 1; + } + + /* read the messages */ + if (!read_message( stdin, &all_msgs )) { + fprintf(stderr,"nothing to send\n"); + return 1; + } + + total = count_messages( &all_msgs ); + if (!total) { + fprintf(stderr,"no messages to send\n"); + return 1; + } + + /* write it to the imap server */ + ctx = imap_open_store( &server ); + if (!ctx) { + fprintf( stderr,"failed to open store\n"); + return 1; + } + + fprintf( stderr, "sending %d message%s\n", total, (total!=1)?"s":"" ); + ctx->name = imap_folder; + while (1) { + unsigned percent = n * 100 / total; + fprintf( stderr, "%4u%% (%d/%d) done\r", percent, n, total ); + if (!split_msg( &all_msgs, &msg, &ofs )) + break; + r = imap_store_msg( ctx, &msg, &uid ); + if (r != DRV_OK) break; + n++; + } + fprintf( stderr,"\n" ); + + imap_close_store( ctx ); + + return 0; +} diff --git a/index-pack.c b/index-pack.c new file mode 100644 index 0000000000..72e0962415 --- /dev/null +++ b/index-pack.c @@ -0,0 +1,948 @@ +#include "cache.h" +#include "delta.h" +#include "pack.h" +#include "csum-file.h" +#include "blob.h" +#include "commit.h" +#include "tag.h" +#include "tree.h" + +static const char index_pack_usage[] = +"git-index-pack [-v] [-o <index-file>] [{ ---keep | --keep=<msg> }] { <pack-file> | --stdin [--fix-thin] [<pack-file>] }"; + +struct object_entry +{ + unsigned long offset; + unsigned long size; + unsigned int hdr_size; + enum object_type type; + enum object_type real_type; + unsigned char sha1[20]; +}; + +union delta_base { + unsigned char sha1[20]; + unsigned long offset; +}; + +/* + * Even if sizeof(union delta_base) == 24 on 64-bit archs, we really want + * to memcmp() only the first 20 bytes. + */ +#define UNION_BASE_SZ 20 + +struct delta_entry +{ + union delta_base base; + int obj_no; +}; + +static struct object_entry *objects; +static struct delta_entry *deltas; +static int nr_objects; +static int nr_deltas; +static int nr_resolved_deltas; + +static int from_stdin; +static int verbose; + +static volatile sig_atomic_t progress_update; + +static void progress_interval(int signum) +{ + progress_update = 1; +} + +static void setup_progress_signal(void) +{ + struct sigaction sa; + struct itimerval v; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = progress_interval; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction(SIGALRM, &sa, NULL); + + v.it_interval.tv_sec = 1; + v.it_interval.tv_usec = 0; + v.it_value = v.it_interval; + setitimer(ITIMER_REAL, &v, NULL); + +} + +static unsigned display_progress(unsigned n, unsigned total, unsigned last_pc) +{ + unsigned percent = n * 100 / total; + if (percent != last_pc || progress_update) { + fprintf(stderr, "%4u%% (%u/%u) done\r", percent, n, total); + progress_update = 0; + } + return percent; +} + +/* We always read in 4kB chunks. */ +static unsigned char input_buffer[4096]; +static unsigned long input_offset, input_len, consumed_bytes; +static SHA_CTX input_ctx; +static int input_fd, output_fd, pack_fd; + +/* Discard current buffer used content. */ +static void flush(void) +{ + if (input_offset) { + if (output_fd >= 0) + write_or_die(output_fd, input_buffer, input_offset); + SHA1_Update(&input_ctx, input_buffer, input_offset); + memmove(input_buffer, input_buffer + input_offset, input_len); + input_offset = 0; + } +} + +/* + * Make sure at least "min" bytes are available in the buffer, and + * return the pointer to the buffer. + */ +static void *fill(int min) +{ + if (min <= input_len) + return input_buffer + input_offset; + if (min > sizeof(input_buffer)) + die("cannot fill %d bytes", min); + flush(); + do { + int ret = xread(input_fd, input_buffer + input_len, + sizeof(input_buffer) - input_len); + if (ret <= 0) { + if (!ret) + die("early EOF"); + die("read error on input: %s", strerror(errno)); + } + input_len += ret; + } while (input_len < min); + return input_buffer; +} + +static void use(int bytes) +{ + if (bytes > input_len) + die("used more bytes than were available"); + input_len -= bytes; + input_offset += bytes; + consumed_bytes += bytes; +} + +static const char *open_pack_file(const char *pack_name) +{ + if (from_stdin) { + input_fd = 0; + if (!pack_name) { + static char tmpfile[PATH_MAX]; + snprintf(tmpfile, sizeof(tmpfile), + "%s/pack_XXXXXX", get_object_directory()); + output_fd = mkstemp(tmpfile); + pack_name = xstrdup(tmpfile); + } else + output_fd = open(pack_name, O_CREAT|O_EXCL|O_RDWR, 0600); + if (output_fd < 0) + die("unable to create %s: %s\n", pack_name, strerror(errno)); + pack_fd = output_fd; + } else { + input_fd = open(pack_name, O_RDONLY); + if (input_fd < 0) + die("cannot open packfile '%s': %s", + pack_name, strerror(errno)); + output_fd = -1; + pack_fd = input_fd; + } + SHA1_Init(&input_ctx); + return pack_name; +} + +static void parse_pack_header(void) +{ + struct pack_header *hdr = fill(sizeof(struct pack_header)); + + /* Header consistency check */ + if (hdr->hdr_signature != htonl(PACK_SIGNATURE)) + die("pack signature mismatch"); + if (!pack_version_ok(hdr->hdr_version)) + die("pack version %d unsupported", ntohl(hdr->hdr_version)); + + nr_objects = ntohl(hdr->hdr_entries); + use(sizeof(struct pack_header)); +} + +static void bad_object(unsigned long offset, const char *format, + ...) NORETURN __attribute__((format (printf, 2, 3))); + +static void bad_object(unsigned long offset, const char *format, ...) +{ + va_list params; + char buf[1024]; + + va_start(params, format); + vsnprintf(buf, sizeof(buf), format, params); + va_end(params); + die("pack has bad object at offset %lu: %s", offset, buf); +} + +static void *unpack_entry_data(unsigned long offset, unsigned long size) +{ + z_stream stream; + void *buf = xmalloc(size); + + memset(&stream, 0, sizeof(stream)); + stream.next_out = buf; + stream.avail_out = size; + stream.next_in = fill(1); + stream.avail_in = input_len; + inflateInit(&stream); + + for (;;) { + int ret = inflate(&stream, 0); + use(input_len - stream.avail_in); + if (stream.total_out == size && ret == Z_STREAM_END) + break; + if (ret != Z_OK) + bad_object(offset, "inflate returned %d", ret); + stream.next_in = fill(1); + stream.avail_in = input_len; + } + inflateEnd(&stream); + return buf; +} + +static void *unpack_raw_entry(struct object_entry *obj, union delta_base *delta_base) +{ + unsigned char *p, c; + unsigned long size, base_offset; + unsigned shift; + + obj->offset = consumed_bytes; + + p = fill(1); + c = *p; + use(1); + obj->type = (c >> 4) & 7; + size = (c & 15); + shift = 4; + while (c & 0x80) { + p = fill(1); + c = *p; + use(1); + size += (c & 0x7fUL) << shift; + shift += 7; + } + obj->size = size; + + switch (obj->type) { + case OBJ_REF_DELTA: + hashcpy(delta_base->sha1, fill(20)); + use(20); + break; + case OBJ_OFS_DELTA: + memset(delta_base, 0, sizeof(*delta_base)); + p = fill(1); + c = *p; + use(1); + base_offset = c & 127; + while (c & 128) { + base_offset += 1; + if (!base_offset || base_offset & ~(~0UL >> 7)) + bad_object(obj->offset, "offset value overflow for delta base object"); + p = fill(1); + c = *p; + use(1); + base_offset = (base_offset << 7) + (c & 127); + } + delta_base->offset = obj->offset - base_offset; + if (delta_base->offset >= obj->offset) + bad_object(obj->offset, "delta base offset is out of bound"); + break; + case OBJ_COMMIT: + case OBJ_TREE: + case OBJ_BLOB: + case OBJ_TAG: + break; + default: + bad_object(obj->offset, "unknown object type %d", obj->type); + } + obj->hdr_size = consumed_bytes - obj->offset; + + return unpack_entry_data(obj->offset, obj->size); +} + +static void *get_data_from_pack(struct object_entry *obj) +{ + unsigned long from = obj[0].offset + obj[0].hdr_size; + unsigned long len = obj[1].offset - from; + unsigned char *src, *data; + z_stream stream; + int st; + + src = xmalloc(len); + if (pread(pack_fd, src, len, from) != len) + die("cannot pread pack file: %s", strerror(errno)); + data = xmalloc(obj->size); + memset(&stream, 0, sizeof(stream)); + stream.next_out = data; + stream.avail_out = obj->size; + stream.next_in = src; + stream.avail_in = len; + inflateInit(&stream); + while ((st = inflate(&stream, Z_FINISH)) == Z_OK); + inflateEnd(&stream); + if (st != Z_STREAM_END || stream.total_out != obj->size) + die("serious inflate inconsistency"); + free(src); + return data; +} + +static int find_delta(const union delta_base *base) +{ + int first = 0, last = nr_deltas; + + while (first < last) { + int next = (first + last) / 2; + struct delta_entry *delta = &deltas[next]; + int cmp; + + cmp = memcmp(base, &delta->base, UNION_BASE_SZ); + if (!cmp) + return next; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + return -first-1; +} + +static int find_delta_children(const union delta_base *base, + int *first_index, int *last_index) +{ + int first = find_delta(base); + int last = first; + int end = nr_deltas - 1; + + if (first < 0) + return -1; + while (first > 0 && !memcmp(&deltas[first - 1].base, base, UNION_BASE_SZ)) + --first; + while (last < end && !memcmp(&deltas[last + 1].base, base, UNION_BASE_SZ)) + ++last; + *first_index = first; + *last_index = last; + return 0; +} + +static void sha1_object(const void *data, unsigned long size, + enum object_type type, unsigned char *sha1) +{ + SHA_CTX ctx; + char header[50]; + int header_size; + const char *type_str; + + switch (type) { + case OBJ_COMMIT: type_str = commit_type; break; + case OBJ_TREE: type_str = tree_type; break; + case OBJ_BLOB: type_str = blob_type; break; + case OBJ_TAG: type_str = tag_type; break; + default: + die("bad type %d", type); + } + + header_size = sprintf(header, "%s %lu", type_str, size) + 1; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, header, header_size); + SHA1_Update(&ctx, data, size); + SHA1_Final(sha1, &ctx); +} + +static void resolve_delta(struct object_entry *delta_obj, void *base_data, + unsigned long base_size, enum object_type type) +{ + void *delta_data; + unsigned long delta_size; + void *result; + unsigned long result_size; + union delta_base delta_base; + int j, first, last; + + delta_obj->real_type = type; + delta_data = get_data_from_pack(delta_obj); + delta_size = delta_obj->size; + result = patch_delta(base_data, base_size, delta_data, delta_size, + &result_size); + free(delta_data); + if (!result) + bad_object(delta_obj->offset, "failed to apply delta"); + sha1_object(result, result_size, type, delta_obj->sha1); + nr_resolved_deltas++; + + hashcpy(delta_base.sha1, delta_obj->sha1); + if (!find_delta_children(&delta_base, &first, &last)) { + for (j = first; j <= last; j++) { + struct object_entry *child = objects + deltas[j].obj_no; + if (child->real_type == OBJ_REF_DELTA) + resolve_delta(child, result, result_size, type); + } + } + + memset(&delta_base, 0, sizeof(delta_base)); + delta_base.offset = delta_obj->offset; + if (!find_delta_children(&delta_base, &first, &last)) { + for (j = first; j <= last; j++) { + struct object_entry *child = objects + deltas[j].obj_no; + if (child->real_type == OBJ_OFS_DELTA) + resolve_delta(child, result, result_size, type); + } + } + + free(result); +} + +static int compare_delta_entry(const void *a, const void *b) +{ + const struct delta_entry *delta_a = a; + const struct delta_entry *delta_b = b; + return memcmp(&delta_a->base, &delta_b->base, UNION_BASE_SZ); +} + +/* Parse all objects and return the pack content SHA1 hash */ +static void parse_pack_objects(unsigned char *sha1) +{ + int i, percent = -1; + struct delta_entry *delta = deltas; + void *data; + struct stat st; + + /* + * First pass: + * - find locations of all objects; + * - calculate SHA1 of all non-delta objects; + * - remember base (SHA1 or offset) for all deltas. + */ + if (verbose) + fprintf(stderr, "Indexing %d objects.\n", nr_objects); + for (i = 0; i < nr_objects; i++) { + struct object_entry *obj = &objects[i]; + data = unpack_raw_entry(obj, &delta->base); + obj->real_type = obj->type; + if (obj->type == OBJ_REF_DELTA || obj->type == OBJ_OFS_DELTA) { + nr_deltas++; + delta->obj_no = i; + delta++; + } else + sha1_object(data, obj->size, obj->type, obj->sha1); + free(data); + if (verbose) + percent = display_progress(i+1, nr_objects, percent); + } + objects[i].offset = consumed_bytes; + if (verbose) + fputc('\n', stderr); + + /* Check pack integrity */ + flush(); + SHA1_Final(sha1, &input_ctx); + if (hashcmp(fill(20), sha1)) + die("pack is corrupted (SHA1 mismatch)"); + use(20); + + /* If input_fd is a file, we should have reached its end now. */ + if (fstat(input_fd, &st)) + die("cannot fstat packfile: %s", strerror(errno)); + if (S_ISREG(st.st_mode) && st.st_size != consumed_bytes) + die("pack has junk at the end"); + + if (!nr_deltas) + return; + + /* Sort deltas by base SHA1/offset for fast searching */ + qsort(deltas, nr_deltas, sizeof(struct delta_entry), + compare_delta_entry); + + /* + * Second pass: + * - for all non-delta objects, look if it is used as a base for + * deltas; + * - if used as a base, uncompress the object and apply all deltas, + * recursively checking if the resulting object is used as a base + * for some more deltas. + */ + if (verbose) + fprintf(stderr, "Resolving %d deltas.\n", nr_deltas); + for (i = 0; i < nr_objects; i++) { + struct object_entry *obj = &objects[i]; + union delta_base base; + int j, ref, ref_first, ref_last, ofs, ofs_first, ofs_last; + + if (obj->type == OBJ_REF_DELTA || obj->type == OBJ_OFS_DELTA) + continue; + hashcpy(base.sha1, obj->sha1); + ref = !find_delta_children(&base, &ref_first, &ref_last); + memset(&base, 0, sizeof(base)); + base.offset = obj->offset; + ofs = !find_delta_children(&base, &ofs_first, &ofs_last); + if (!ref && !ofs) + continue; + data = get_data_from_pack(obj); + if (ref) + for (j = ref_first; j <= ref_last; j++) { + struct object_entry *child = objects + deltas[j].obj_no; + if (child->real_type == OBJ_REF_DELTA) + resolve_delta(child, data, + obj->size, obj->type); + } + if (ofs) + for (j = ofs_first; j <= ofs_last; j++) { + struct object_entry *child = objects + deltas[j].obj_no; + if (child->real_type == OBJ_OFS_DELTA) + resolve_delta(child, data, + obj->size, obj->type); + } + free(data); + if (verbose) + percent = display_progress(nr_resolved_deltas, + nr_deltas, percent); + } + if (verbose && nr_resolved_deltas == nr_deltas) + fputc('\n', stderr); +} + +static int write_compressed(int fd, void *in, unsigned int size) +{ + z_stream stream; + unsigned long maxsize; + void *out; + + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, zlib_compression_level); + maxsize = deflateBound(&stream, size); + out = xmalloc(maxsize); + + /* Compress it */ + stream.next_in = in; + stream.avail_in = size; + stream.next_out = out; + stream.avail_out = maxsize; + while (deflate(&stream, Z_FINISH) == Z_OK); + deflateEnd(&stream); + + size = stream.total_out; + write_or_die(fd, out, size); + free(out); + return size; +} + +static void append_obj_to_pack(void *buf, + unsigned long size, enum object_type type) +{ + struct object_entry *obj = &objects[nr_objects++]; + unsigned char header[10]; + unsigned long s = size; + int n = 0; + unsigned char c = (type << 4) | (s & 15); + s >>= 4; + while (s) { + header[n++] = c | 0x80; + c = s & 0x7f; + s >>= 7; + } + header[n++] = c; + write_or_die(output_fd, header, n); + obj[1].offset = obj[0].offset + n; + obj[1].offset += write_compressed(output_fd, buf, size); + sha1_object(buf, size, type, obj->sha1); +} + +static int delta_pos_compare(const void *_a, const void *_b) +{ + struct delta_entry *a = *(struct delta_entry **)_a; + struct delta_entry *b = *(struct delta_entry **)_b; + return a->obj_no - b->obj_no; +} + +static void fix_unresolved_deltas(int nr_unresolved) +{ + struct delta_entry **sorted_by_pos; + int i, n = 0, percent = -1; + + /* + * Since many unresolved deltas may well be themselves base objects + * for more unresolved deltas, we really want to include the + * smallest number of base objects that would cover as much delta + * as possible by picking the + * trunc deltas first, allowing for other deltas to resolve without + * additional base objects. Since most base objects are to be found + * before deltas depending on them, a good heuristic is to start + * resolving deltas in the same order as their position in the pack. + */ + sorted_by_pos = xmalloc(nr_unresolved * sizeof(*sorted_by_pos)); + for (i = 0; i < nr_deltas; i++) { + if (objects[deltas[i].obj_no].real_type != OBJ_REF_DELTA) + continue; + sorted_by_pos[n++] = &deltas[i]; + } + qsort(sorted_by_pos, n, sizeof(*sorted_by_pos), delta_pos_compare); + + for (i = 0; i < n; i++) { + struct delta_entry *d = sorted_by_pos[i]; + void *data; + unsigned long size; + char type[10]; + enum object_type obj_type; + int j, first, last; + + if (objects[d->obj_no].real_type != OBJ_REF_DELTA) + continue; + data = read_sha1_file(d->base.sha1, type, &size); + if (!data) + continue; + if (!strcmp(type, blob_type)) obj_type = OBJ_BLOB; + else if (!strcmp(type, tree_type)) obj_type = OBJ_TREE; + else if (!strcmp(type, commit_type)) obj_type = OBJ_COMMIT; + else if (!strcmp(type, tag_type)) obj_type = OBJ_TAG; + else die("base object %s is of type '%s'", + sha1_to_hex(d->base.sha1), type); + + find_delta_children(&d->base, &first, &last); + for (j = first; j <= last; j++) { + struct object_entry *child = objects + deltas[j].obj_no; + if (child->real_type == OBJ_REF_DELTA) + resolve_delta(child, data, size, obj_type); + } + + append_obj_to_pack(data, size, obj_type); + free(data); + if (verbose) + percent = display_progress(nr_resolved_deltas, + nr_deltas, percent); + } + free(sorted_by_pos); + if (verbose) + fputc('\n', stderr); +} + +static void readjust_pack_header_and_sha1(unsigned char *sha1) +{ + struct pack_header hdr; + SHA_CTX ctx; + int size; + + /* Rewrite pack header with updated object number */ + if (lseek(output_fd, 0, SEEK_SET) != 0) + die("cannot seek back: %s", strerror(errno)); + if (read_in_full(output_fd, &hdr, sizeof(hdr)) != sizeof(hdr)) + die("cannot read pack header back: %s", strerror(errno)); + hdr.hdr_entries = htonl(nr_objects); + if (lseek(output_fd, 0, SEEK_SET) != 0) + die("cannot seek back: %s", strerror(errno)); + write_or_die(output_fd, &hdr, sizeof(hdr)); + if (lseek(output_fd, 0, SEEK_SET) != 0) + die("cannot seek back: %s", strerror(errno)); + + /* Recompute and store the new pack's SHA1 */ + SHA1_Init(&ctx); + do { + unsigned char *buf[4096]; + size = xread(output_fd, buf, sizeof(buf)); + if (size < 0) + die("cannot read pack data back: %s", strerror(errno)); + SHA1_Update(&ctx, buf, size); + } while (size > 0); + SHA1_Final(sha1, &ctx); + write_or_die(output_fd, sha1, 20); +} + +static int sha1_compare(const void *_a, const void *_b) +{ + struct object_entry *a = *(struct object_entry **)_a; + struct object_entry *b = *(struct object_entry **)_b; + return hashcmp(a->sha1, b->sha1); +} + +/* + * On entry *sha1 contains the pack content SHA1 hash, on exit it is + * the SHA1 hash of sorted object names. + */ +static const char *write_index_file(const char *index_name, unsigned char *sha1) +{ + struct sha1file *f; + struct object_entry **sorted_by_sha, **list, **last; + unsigned int array[256]; + int i, fd; + SHA_CTX ctx; + + if (nr_objects) { + sorted_by_sha = + xcalloc(nr_objects, sizeof(struct object_entry *)); + list = sorted_by_sha; + last = sorted_by_sha + nr_objects; + for (i = 0; i < nr_objects; ++i) + sorted_by_sha[i] = &objects[i]; + qsort(sorted_by_sha, nr_objects, sizeof(sorted_by_sha[0]), + sha1_compare); + + } + else + sorted_by_sha = list = last = NULL; + + if (!index_name) { + static char tmpfile[PATH_MAX]; + snprintf(tmpfile, sizeof(tmpfile), + "%s/index_XXXXXX", get_object_directory()); + fd = mkstemp(tmpfile); + index_name = xstrdup(tmpfile); + } else { + unlink(index_name); + fd = open(index_name, O_CREAT|O_EXCL|O_WRONLY, 0600); + } + if (fd < 0) + die("unable to create %s: %s", index_name, strerror(errno)); + f = sha1fd(fd, index_name); + + /* + * Write the first-level table (the list is sorted, + * but we use a 256-entry lookup to be able to avoid + * having to do eight extra binary search iterations). + */ + for (i = 0; i < 256; i++) { + struct object_entry **next = list; + while (next < last) { + struct object_entry *obj = *next; + if (obj->sha1[0] != i) + break; + next++; + } + array[i] = htonl(next - sorted_by_sha); + list = next; + } + sha1write(f, array, 256 * sizeof(int)); + + /* recompute the SHA1 hash of sorted object names. + * currently pack-objects does not do this, but that + * can be fixed. + */ + SHA1_Init(&ctx); + /* + * Write the actual SHA1 entries.. + */ + list = sorted_by_sha; + for (i = 0; i < nr_objects; i++) { + struct object_entry *obj = *list++; + unsigned int offset = htonl(obj->offset); + sha1write(f, &offset, 4); + sha1write(f, obj->sha1, 20); + SHA1_Update(&ctx, obj->sha1, 20); + } + sha1write(f, sha1, 20); + sha1close(f, NULL, 1); + free(sorted_by_sha); + SHA1_Final(sha1, &ctx); + return index_name; +} + +static void final(const char *final_pack_name, const char *curr_pack_name, + const char *final_index_name, const char *curr_index_name, + const char *keep_name, const char *keep_msg, + unsigned char *sha1) +{ + char *report = "pack"; + char name[PATH_MAX]; + int err; + + if (!from_stdin) { + close(input_fd); + } else { + err = close(output_fd); + if (err) + die("error while closing pack file: %s", strerror(errno)); + chmod(curr_pack_name, 0444); + } + + if (keep_msg) { + int keep_fd, keep_msg_len = strlen(keep_msg); + if (!keep_name) { + snprintf(name, sizeof(name), "%s/pack/pack-%s.keep", + get_object_directory(), sha1_to_hex(sha1)); + keep_name = name; + } + keep_fd = open(keep_name, O_RDWR|O_CREAT|O_EXCL, 0600); + if (keep_fd < 0) { + if (errno != EEXIST) + die("cannot write keep file"); + } else { + if (keep_msg_len > 0) { + write_or_die(keep_fd, keep_msg, keep_msg_len); + write_or_die(keep_fd, "\n", 1); + } + close(keep_fd); + report = "keep"; + } + } + + if (final_pack_name != curr_pack_name) { + if (!final_pack_name) { + snprintf(name, sizeof(name), "%s/pack/pack-%s.pack", + get_object_directory(), sha1_to_hex(sha1)); + final_pack_name = name; + } + if (move_temp_to_file(curr_pack_name, final_pack_name)) + die("cannot store pack file"); + } + + chmod(curr_index_name, 0444); + if (final_index_name != curr_index_name) { + if (!final_index_name) { + snprintf(name, sizeof(name), "%s/pack/pack-%s.idx", + get_object_directory(), sha1_to_hex(sha1)); + final_index_name = name; + } + if (move_temp_to_file(curr_index_name, final_index_name)) + die("cannot store index file"); + } + + if (!from_stdin) { + printf("%s\n", sha1_to_hex(sha1)); + } else { + char buf[48]; + int len = snprintf(buf, sizeof(buf), "%s\t%s\n", + report, sha1_to_hex(sha1)); + write_or_die(1, buf, len); + + /* + * Let's just mimic git-unpack-objects here and write + * the last part of the input buffer to stdout. + */ + while (input_len) { + err = xwrite(1, input_buffer + input_offset, input_len); + if (err <= 0) + break; + input_len -= err; + input_offset += err; + } + } +} + +int main(int argc, char **argv) +{ + int i, fix_thin_pack = 0; + const char *curr_pack, *pack_name = NULL; + const char *curr_index, *index_name = NULL; + const char *keep_name = NULL, *keep_msg = NULL; + char *index_name_buf = NULL, *keep_name_buf = NULL; + unsigned char sha1[20]; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg == '-') { + if (!strcmp(arg, "--stdin")) { + from_stdin = 1; + } else if (!strcmp(arg, "--fix-thin")) { + fix_thin_pack = 1; + } else if (!strcmp(arg, "--keep")) { + keep_msg = ""; + } else if (!strncmp(arg, "--keep=", 7)) { + keep_msg = arg + 7; + } else if (!strncmp(arg, "--pack_header=", 14)) { + struct pack_header *hdr; + char *c; + + hdr = (struct pack_header *)input_buffer; + hdr->hdr_signature = htonl(PACK_SIGNATURE); + hdr->hdr_version = htonl(strtoul(arg + 14, &c, 10)); + if (*c != ',') + die("bad %s", arg); + hdr->hdr_entries = htonl(strtoul(c + 1, &c, 10)); + if (*c) + die("bad %s", arg); + input_len = sizeof(*hdr); + } else if (!strcmp(arg, "-v")) { + verbose = 1; + } else if (!strcmp(arg, "-o")) { + if (index_name || (i+1) >= argc) + usage(index_pack_usage); + index_name = argv[++i]; + } else + usage(index_pack_usage); + continue; + } + + if (pack_name) + usage(index_pack_usage); + pack_name = arg; + } + + if (!pack_name && !from_stdin) + usage(index_pack_usage); + if (fix_thin_pack && !from_stdin) + die("--fix-thin cannot be used without --stdin"); + if (!index_name && pack_name) { + int len = strlen(pack_name); + if (!has_extension(pack_name, ".pack")) + die("packfile name '%s' does not end with '.pack'", + pack_name); + index_name_buf = xmalloc(len); + memcpy(index_name_buf, pack_name, len - 5); + strcpy(index_name_buf + len - 5, ".idx"); + index_name = index_name_buf; + } + if (keep_msg && !keep_name && pack_name) { + int len = strlen(pack_name); + if (!has_extension(pack_name, ".pack")) + die("packfile name '%s' does not end with '.pack'", + pack_name); + keep_name_buf = xmalloc(len); + memcpy(keep_name_buf, pack_name, len - 5); + strcpy(keep_name_buf + len - 5, ".keep"); + keep_name = keep_name_buf; + } + + curr_pack = open_pack_file(pack_name); + parse_pack_header(); + objects = xmalloc((nr_objects + 1) * sizeof(struct object_entry)); + deltas = xmalloc(nr_objects * sizeof(struct delta_entry)); + if (verbose) + setup_progress_signal(); + parse_pack_objects(sha1); + if (nr_deltas != nr_resolved_deltas) { + if (fix_thin_pack) { + int nr_unresolved = nr_deltas - nr_resolved_deltas; + int nr_objects_initial = nr_objects; + if (nr_unresolved <= 0) + die("confusion beyond insanity"); + objects = xrealloc(objects, + (nr_objects + nr_unresolved + 1) + * sizeof(*objects)); + fix_unresolved_deltas(nr_unresolved); + if (verbose) + fprintf(stderr, "%d objects were added to complete this thin pack.\n", + nr_objects - nr_objects_initial); + readjust_pack_header_and_sha1(sha1); + } + if (nr_deltas != nr_resolved_deltas) + die("pack has %d unresolved deltas", + nr_deltas - nr_resolved_deltas); + } else { + /* Flush remaining pack final 20-byte SHA1. */ + flush(); + } + free(deltas); + curr_index = write_index_file(index_name, sha1); + final(pack_name, curr_pack, + index_name, curr_index, + keep_name, keep_msg, + sha1); + free(objects); + free(index_name_buf); + free(keep_name_buf); + + return 0; +} diff --git a/interpolate.c b/interpolate.c new file mode 100644 index 0000000000..f992ef7753 --- /dev/null +++ b/interpolate.c @@ -0,0 +1,106 @@ +/* + * Copyright 2006 Jon Loeliger + */ + +#include "git-compat-util.h" +#include "interpolate.h" + + +void interp_set_entry(struct interp *table, int slot, const char *value) +{ + char *oldval = table[slot].value; + char *newval = NULL; + + if (oldval) + free(oldval); + + if (value) + newval = xstrdup(value); + + table[slot].value = newval; +} + + +void interp_clear_table(struct interp *table, int ninterps) +{ + int i; + + for (i = 0; i < ninterps; i++) { + interp_set_entry(table, i, NULL); + } +} + + +/* + * Convert a NUL-terminated string in buffer orig + * into the supplied buffer, result, whose length is reslen, + * performing substitutions on %-named sub-strings from + * the table, interps, with ninterps entries. + * + * Example interps: + * { + * { "%H", "example.org"}, + * { "%port", "123"}, + * { "%%", "%"}, + * } + * + * Returns 1 on a successful substitution pass that fits in result, + * Returns 0 on a failed or overflowing substitution pass. + */ + +int interpolate(char *result, int reslen, + const char *orig, + const struct interp *interps, int ninterps) +{ + const char *src = orig; + char *dest = result; + int newlen = 0; + char *name, *value; + int namelen, valuelen; + int i; + char c; + + memset(result, 0, reslen); + + while ((c = *src) && newlen < reslen - 1) { + if (c == '%') { + /* Try to match an interpolation string. */ + for (i = 0; i < ninterps; i++) { + name = interps[i].name; + namelen = strlen(name); + if (strncmp(src, name, namelen) == 0) { + break; + } + } + + /* Check for valid interpolation. */ + if (i < ninterps) { + value = interps[i].value; + valuelen = strlen(value); + + if (newlen + valuelen < reslen - 1) { + /* Substitute. */ + strncpy(dest, value, valuelen); + newlen += valuelen; + dest += valuelen; + src += namelen; + } else { + /* Something's not fitting. */ + return 0; + } + + } else { + /* Skip bogus interpolation. */ + *dest++ = *src++; + newlen++; + } + + } else { + /* Straight copy one non-interpolation character. */ + *dest++ = *src++; + newlen++; + } + } + + return newlen < reslen - 1; +} diff --git a/interpolate.h b/interpolate.h new file mode 100644 index 0000000000..190a180b58 --- /dev/null +++ b/interpolate.h @@ -0,0 +1,26 @@ +/* + * Copyright 2006 Jon Loeliger + */ + +#ifndef INTERPOLATE_H +#define INTERPOLATE_H + +/* + * Convert a NUL-terminated string in buffer orig, + * performing substitutions on %-named sub-strings from + * the interpretation table. + */ + +struct interp { + char *name; + char *value; +}; + +extern void interp_set_entry(struct interp *table, int slot, const char *value); +extern void interp_clear_table(struct interp *table, int ninterps); + +extern int interpolate(char *result, int reslen, + const char *orig, + const struct interp *interps, int ninterps); + +#endif /* INTERPOLATE_H */ diff --git a/list-objects.c b/list-objects.c new file mode 100644 index 0000000000..f1fa21c397 --- /dev/null +++ b/list-objects.c @@ -0,0 +1,140 @@ +#include "cache.h" +#include "tag.h" +#include "commit.h" +#include "tree.h" +#include "blob.h" +#include "diff.h" +#include "tree-walk.h" +#include "revision.h" +#include "list-objects.h" + +static void process_blob(struct rev_info *revs, + struct blob *blob, + struct object_array *p, + struct name_path *path, + const char *name) +{ + struct object *obj = &blob->object; + + if (!revs->blob_objects) + return; + if (obj->flags & (UNINTERESTING | SEEN)) + return; + obj->flags |= SEEN; + name = xstrdup(name); + add_object(obj, p, path, name); +} + +static void process_tree(struct rev_info *revs, + struct tree *tree, + struct object_array *p, + struct name_path *path, + const char *name) +{ + struct object *obj = &tree->object; + struct tree_desc desc; + struct name_entry entry; + struct name_path me; + + if (!revs->tree_objects) + return; + if (obj->flags & (UNINTERESTING | SEEN)) + return; + if (parse_tree(tree) < 0) + die("bad tree object %s", sha1_to_hex(obj->sha1)); + obj->flags |= SEEN; + name = xstrdup(name); + add_object(obj, p, path, name); + me.up = path; + me.elem = name; + me.elem_len = strlen(name); + + desc.buf = tree->buffer; + desc.size = tree->size; + + while (tree_entry(&desc, &entry)) { + if (S_ISDIR(entry.mode)) + process_tree(revs, + lookup_tree(entry.sha1), + p, &me, entry.path); + else + process_blob(revs, + lookup_blob(entry.sha1), + p, &me, entry.path); + } + free(tree->buffer); + tree->buffer = NULL; +} + +static void mark_edge_parents_uninteresting(struct commit *commit, + struct rev_info *revs, + show_edge_fn show_edge) +{ + struct commit_list *parents; + + for (parents = commit->parents; parents; parents = parents->next) { + struct commit *parent = parents->item; + if (!(parent->object.flags & UNINTERESTING)) + continue; + mark_tree_uninteresting(parent->tree); + if (revs->edge_hint && !(parent->object.flags & SHOWN)) { + parent->object.flags |= SHOWN; + show_edge(parent); + } + } +} + +void mark_edges_uninteresting(struct commit_list *list, + struct rev_info *revs, + show_edge_fn show_edge) +{ + for ( ; list; list = list->next) { + struct commit *commit = list->item; + + if (commit->object.flags & UNINTERESTING) { + mark_tree_uninteresting(commit->tree); + continue; + } + mark_edge_parents_uninteresting(commit, revs, show_edge); + } +} + +void traverse_commit_list(struct rev_info *revs, + void (*show_commit)(struct commit *), + void (*show_object)(struct object_array_entry *)) +{ + int i; + struct commit *commit; + struct object_array objects = { 0, 0, NULL }; + + while ((commit = get_revision(revs)) != NULL) { + process_tree(revs, commit->tree, &objects, NULL, ""); + show_commit(commit); + } + for (i = 0; i < revs->pending.nr; i++) { + struct object_array_entry *pending = revs->pending.objects + i; + struct object *obj = pending->item; + const char *name = pending->name; + if (obj->flags & (UNINTERESTING | SEEN)) + continue; + if (obj->type == OBJ_TAG) { + obj->flags |= SEEN; + add_object_array(obj, name, &objects); + continue; + } + if (obj->type == OBJ_TREE) { + process_tree(revs, (struct tree *)obj, &objects, + NULL, name); + continue; + } + if (obj->type == OBJ_BLOB) { + process_blob(revs, (struct blob *)obj, &objects, + NULL, name); + continue; + } + die("unknown pending object %s (%s)", + sha1_to_hex(obj->sha1), name); + } + for (i = 0; i < objects.nr; i++) + show_object(&objects.objects[i]); +} diff --git a/list-objects.h b/list-objects.h new file mode 100644 index 0000000000..0f41391ecc --- /dev/null +++ b/list-objects.h @@ -0,0 +1,12 @@ +#ifndef LIST_OBJECTS_H +#define LIST_OBJECTS_H + +typedef void (*show_commit_fn)(struct commit *); +typedef void (*show_object_fn)(struct object_array_entry *); +typedef void (*show_edge_fn)(struct commit *); + +void traverse_commit_list(struct rev_info *revs, show_commit_fn, show_object_fn); + +void mark_edges_uninteresting(struct commit_list *, struct rev_info *, show_edge_fn); + +#endif diff --git a/local-fetch.c b/local-fetch.c new file mode 100644 index 0000000000..7cfe8b3587 --- /dev/null +++ b/local-fetch.c @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "commit.h" +#include "fetch.h" + +static int use_link; +static int use_symlink; +static int use_filecopy = 1; +static int commits_on_stdin; + +static const char *path; /* "Remote" git repository */ + +void prefetch(unsigned char *sha1) +{ +} + +static struct packed_git *packs; + +static void setup_index(unsigned char *sha1) +{ + struct packed_git *new_pack; + char filename[PATH_MAX]; + strcpy(filename, path); + strcat(filename, "/objects/pack/pack-"); + strcat(filename, sha1_to_hex(sha1)); + strcat(filename, ".idx"); + new_pack = parse_pack_index_file(sha1, filename); + new_pack->next = packs; + packs = new_pack; +} + +static int setup_indices(void) +{ + DIR *dir; + struct dirent *de; + char filename[PATH_MAX]; + unsigned char sha1[20]; + sprintf(filename, "%s/objects/pack/", path); + dir = opendir(filename); + if (!dir) + return -1; + while ((de = readdir(dir)) != NULL) { + int namelen = strlen(de->d_name); + if (namelen != 50 || + !has_extension(de->d_name, ".pack")) + continue; + get_sha1_hex(de->d_name + 5, sha1); + setup_index(sha1); + } + closedir(dir); + return 0; +} + +static int copy_file(const char *source, char *dest, const char *hex, + int warn_if_not_exists) +{ + safe_create_leading_directories(dest); + if (use_link) { + if (!link(source, dest)) { + pull_say("link %s\n", hex); + return 0; + } + /* If we got ENOENT there is no point continuing. */ + if (errno == ENOENT) { + if (warn_if_not_exists) + fprintf(stderr, "does not exist %s\n", source); + return -1; + } + } + if (use_symlink) { + struct stat st; + if (stat(source, &st)) { + if (!warn_if_not_exists && errno == ENOENT) + return -1; + fprintf(stderr, "cannot stat %s: %s\n", source, + strerror(errno)); + return -1; + } + if (!symlink(source, dest)) { + pull_say("symlink %s\n", hex); + return 0; + } + } + if (use_filecopy) { + int ifd, ofd, status = 0; + + ifd = open(source, O_RDONLY); + if (ifd < 0) { + if (!warn_if_not_exists && errno == ENOENT) + return -1; + fprintf(stderr, "cannot open %s\n", source); + return -1; + } + ofd = open(dest, O_WRONLY | O_CREAT | O_EXCL, 0666); + if (ofd < 0) { + fprintf(stderr, "cannot open %s\n", dest); + close(ifd); + return -1; + } + status = copy_fd(ifd, ofd); + close(ofd); + if (status) + fprintf(stderr, "cannot write %s\n", dest); + else + pull_say("copy %s\n", hex); + return status; + } + fprintf(stderr, "failed to copy %s with given copy methods.\n", hex); + return -1; +} + +static int fetch_pack(const unsigned char *sha1) +{ + struct packed_git *target; + char filename[PATH_MAX]; + if (setup_indices()) + return -1; + target = find_sha1_pack(sha1, packs); + if (!target) + return error("Couldn't find %s: not separate or in any pack", + sha1_to_hex(sha1)); + if (get_verbosely) { + fprintf(stderr, "Getting pack %s\n", + sha1_to_hex(target->sha1)); + fprintf(stderr, " which contains %s\n", + sha1_to_hex(sha1)); + } + sprintf(filename, "%s/objects/pack/pack-%s.pack", + path, sha1_to_hex(target->sha1)); + copy_file(filename, sha1_pack_name(target->sha1), + sha1_to_hex(target->sha1), 1); + sprintf(filename, "%s/objects/pack/pack-%s.idx", + path, sha1_to_hex(target->sha1)); + copy_file(filename, sha1_pack_index_name(target->sha1), + sha1_to_hex(target->sha1), 1); + install_packed_git(target); + return 0; +} + +static int fetch_file(const unsigned char *sha1) +{ + static int object_name_start = -1; + static char filename[PATH_MAX]; + char *hex = sha1_to_hex(sha1); + char *dest_filename = sha1_file_name(sha1); + + if (object_name_start < 0) { + strcpy(filename, path); /* e.g. git.git */ + strcat(filename, "/objects/"); + object_name_start = strlen(filename); + } + filename[object_name_start+0] = hex[0]; + filename[object_name_start+1] = hex[1]; + filename[object_name_start+2] = '/'; + strcpy(filename + object_name_start + 3, hex + 2); + return copy_file(filename, dest_filename, hex, 0); +} + +int fetch(unsigned char *sha1) +{ + if (has_sha1_file(sha1)) + return 0; + else + return fetch_file(sha1) && fetch_pack(sha1); +} + +int fetch_ref(char *ref, unsigned char *sha1) +{ + static int ref_name_start = -1; + static char filename[PATH_MAX]; + static char hex[41]; + int ifd; + + if (ref_name_start < 0) { + sprintf(filename, "%s/refs/", path); + ref_name_start = strlen(filename); + } + strcpy(filename + ref_name_start, ref); + ifd = open(filename, O_RDONLY); + if (ifd < 0) { + close(ifd); + fprintf(stderr, "cannot open %s\n", filename); + return -1; + } + if (read_in_full(ifd, hex, 40) != 40 || get_sha1_hex(hex, sha1)) { + close(ifd); + fprintf(stderr, "cannot read from %s\n", filename); + return -1; + } + close(ifd); + pull_say("ref %s\n", sha1_to_hex(sha1)); + return 0; +} + +static const char local_pull_usage[] = +"git-local-fetch [-c] [-t] [-a] [-v] [-w filename] [--recover] [-l] [-s] [-n] [--stdin] commit-id path"; + +/* + * By default we only use file copy. + * If -l is specified, a hard link is attempted. + * If -s is specified, then a symlink is attempted. + * If -n is _not_ specified, then a regular file-to-file copy is done. + */ +int main(int argc, const char **argv) +{ + int commits; + const char **write_ref = NULL; + char **commit_id; + int arg = 1; + + setup_git_directory(); + git_config(git_default_config); + + while (arg < argc && argv[arg][0] == '-') { + if (argv[arg][1] == 't') + get_tree = 1; + else if (argv[arg][1] == 'c') + get_history = 1; + else if (argv[arg][1] == 'a') { + get_all = 1; + get_tree = 1; + get_history = 1; + } + else if (argv[arg][1] == 'l') + use_link = 1; + else if (argv[arg][1] == 's') + use_symlink = 1; + else if (argv[arg][1] == 'n') + use_filecopy = 0; + else if (argv[arg][1] == 'v') + get_verbosely = 1; + else if (argv[arg][1] == 'w') + write_ref = &argv[++arg]; + else if (!strcmp(argv[arg], "--recover")) + get_recover = 1; + else if (!strcmp(argv[arg], "--stdin")) + commits_on_stdin = 1; + else + usage(local_pull_usage); + arg++; + } + if (argc < arg + 2 - commits_on_stdin) + usage(local_pull_usage); + if (commits_on_stdin) { + commits = pull_targets_stdin(&commit_id, &write_ref); + } else { + commit_id = (char **) &argv[arg++]; + commits = 1; + } + path = argv[arg]; + + if (pull(commits, commit_id, write_ref, path)) + return 1; + + if (commits_on_stdin) + pull_targets_free(commits, commit_id, write_ref); + + return 0; +} diff --git a/lockfile.c b/lockfile.c new file mode 100644 index 0000000000..4824f4dc02 --- /dev/null +++ b/lockfile.c @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2005, Junio C Hamano + */ +#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); +} + +static int lock_file(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->on_list) { + lk->next = lock_file_list; + lock_file_list = lk; + lk->on_list = 1; + } + if (lock_file_list) { + 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); + } + else + lk->filename[0] = 0; + return fd; +} + +int hold_lock_file_for_update(struct lock_file *lk, const char *path, int die_on_error) +{ + int fd = lock_file(lk, path); + if (fd < 0 && die_on_error) + die("unable to create '%s.lock': %s", path, strerror(errno)); + 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; +} + diff --git a/log-tree.c b/log-tree.c new file mode 100644 index 0000000000..ac86194047 --- /dev/null +++ b/log-tree.c @@ -0,0 +1,372 @@ +#include "cache.h" +#include "diff.h" +#include "commit.h" +#include "log-tree.h" +#include "reflog-walk.h" + +static void show_parents(struct commit *commit, int abbrev) +{ + struct commit_list *p; + for (p = commit->parents; p ; p = p->next) { + struct commit *parent = p->item; + printf(" %s", diff_unique_abbrev(parent->object.sha1, abbrev)); + } +} + +/* + * Search for "^[-A-Za-z]+: [^@]+@" pattern. It usually matches + * Signed-off-by: and Acked-by: lines. + */ +static int detect_any_signoff(char *letter, int size) +{ + char ch, *cp; + int seen_colon = 0; + int seen_at = 0; + int seen_name = 0; + int seen_head = 0; + + cp = letter + size; + while (letter <= --cp && (ch = *cp) == '\n') + continue; + + while (letter <= cp) { + ch = *cp--; + if (ch == '\n') + break; + + if (!seen_at) { + if (ch == '@') + seen_at = 1; + continue; + } + if (!seen_colon) { + if (ch == '@') + return 0; + else if (ch == ':') + seen_colon = 1; + else + seen_name = 1; + continue; + } + if (('A' <= ch && ch <= 'Z') || + ('a' <= ch && ch <= 'z') || + ch == '-') { + seen_head = 1; + continue; + } + /* no empty last line doesn't match */ + return 0; + } + return seen_head && seen_name; +} + +static int append_signoff(char *buf, int buf_sz, int at, const char *signoff) +{ + static const char signed_off_by[] = "Signed-off-by: "; + int signoff_len = strlen(signoff); + int has_signoff = 0; + char *cp = buf; + + /* Do we have enough space to add it? */ + if (buf_sz - at <= strlen(signed_off_by) + signoff_len + 3) + return at; + + /* First see if we already have the sign-off by the signer */ + while ((cp = strstr(cp, signed_off_by))) { + + has_signoff = 1; + + cp += strlen(signed_off_by); + if (cp + signoff_len >= buf + at) + break; + if (strncmp(cp, signoff, signoff_len)) + continue; + if (!isspace(cp[signoff_len])) + continue; + /* we already have him */ + return at; + } + + if (!has_signoff) + has_signoff = detect_any_signoff(buf, at); + + if (!has_signoff) + buf[at++] = '\n'; + + 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; +} + +static unsigned int digits_in_number(unsigned int number) +{ + unsigned int i = 10, result = 1; + while (i <= number) { + i *= 10; + result++; + } + return result; +} + +void show_log(struct rev_info *opt, const char *sep) +{ + static char this_header[16384]; + struct log_info *log = opt->loginfo; + struct commit *commit = log->commit, *parent = log->parent; + int abbrev = opt->diffopt.abbrev; + int abbrev_commit = opt->abbrev_commit ? opt->abbrev : 40; + const char *extra; + int len; + const char *subject = NULL, *extra_headers = opt->extra_headers; + + opt->loginfo = NULL; + if (!opt->verbose_header) { + if (opt->left_right) { + if (commit->object.flags & BOUNDARY) + putchar('-'); + else if (commit->object.flags & SYMMETRIC_LEFT) + putchar('<'); + else + putchar('>'); + } + fputs(diff_unique_abbrev(commit->object.sha1, abbrev_commit), stdout); + if (opt->parents) + show_parents(commit, abbrev_commit); + putchar(opt->diffopt.line_termination); + return; + } + + /* + * The "oneline" format has several special cases: + * - The pretty-printed commit lacks a newline at the end + * of the buffer, but we do want to make sure that we + * have a newline there. If the separator isn't already + * a newline, add an extra one. + * - unlike other log messages, the one-line format does + * not have an empty line between entries. + */ + extra = ""; + if (*sep != '\n' && opt->commit_format == CMIT_FMT_ONELINE) + extra = "\n"; + if (opt->shown_one && opt->commit_format != CMIT_FMT_ONELINE) + putchar(opt->diffopt.line_termination); + opt->shown_one = 1; + + /* + * Print header line of header.. + */ + + if (opt->commit_format == CMIT_FMT_EMAIL) { + char *sha1 = sha1_to_hex(commit->object.sha1); + if (opt->total > 0) { + static char buffer[64]; + snprintf(buffer, sizeof(buffer), + "Subject: [PATCH %0*d/%d] ", + digits_in_number(opt->total), + opt->nr, opt->total); + subject = buffer; + } else if (opt->total == 0) + subject = "Subject: [PATCH] "; + else + subject = "Subject: "; + + printf("From %s Mon Sep 17 00:00:00 2001\n", sha1); + if (opt->message_id) + printf("Message-Id: <%s>\n", opt->message_id); + if (opt->ref_message_id) + printf("In-Reply-To: <%s>\nReferences: <%s>\n", + opt->ref_message_id, opt->ref_message_id); + if (opt->mime_boundary) { + 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" + "\n" + "This is a multi-part message in MIME " + "format.\n" + "--%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); + extra_headers = subject_buffer; + + snprintf(buffer, sizeof(buffer) - 1, + "--%s%s\n" + "Content-Type: text/x-patch;\n" + " name=\"%s.diff\"\n" + "Content-Transfer-Encoding: 8bit\n" + "Content-Disposition: inline;\n" + " filename=\"%s.diff\"\n\n", + mime_boundary_leader, opt->mime_boundary, + sha1, sha1); + opt->diffopt.stat_sep = buffer; + } + } else { + fputs(diff_get_color(opt->diffopt.color_diff, DIFF_COMMIT), + stdout); + if (opt->commit_format != CMIT_FMT_ONELINE) + fputs("commit ", stdout); + if (opt->left_right) { + if (commit->object.flags & BOUNDARY) + putchar('-'); + else if (commit->object.flags & SYMMETRIC_LEFT) + putchar('<'); + else + putchar('>'); + } + fputs(diff_unique_abbrev(commit->object.sha1, abbrev_commit), + stdout); + if (opt->parents) + show_parents(commit, abbrev_commit); + if (parent) + printf(" (from %s)", + diff_unique_abbrev(parent->object.sha1, + abbrev_commit)); + printf("%s", + diff_get_color(opt->diffopt.color_diff, DIFF_RESET)); + putchar(opt->commit_format == CMIT_FMT_ONELINE ? ' ' : '\n'); + if (opt->reflog_info) { + show_reflog_message(opt->reflog_info, + opt->commit_format == CMIT_FMT_ONELINE, + opt->relative_date); + if (opt->commit_format == CMIT_FMT_ONELINE) { + printf("%s", sep); + return; + } + } + } + + /* + * And then the pretty-printed message itself + */ + len = pretty_print_commit(opt->commit_format, commit, ~0u, this_header, + sizeof(this_header), abbrev, subject, + extra_headers, opt->relative_date); + + if (opt->add_signoff) + len = append_signoff(this_header, sizeof(this_header), len, + opt->add_signoff); + printf("%s%s%s", this_header, extra, sep); +} + +int log_tree_diff_flush(struct rev_info *opt) +{ + diffcore_std(&opt->diffopt); + + if (diff_queue_is_empty()) { + int saved_fmt = opt->diffopt.output_format; + opt->diffopt.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_flush(&opt->diffopt); + opt->diffopt.output_format = saved_fmt; + return 0; + } + + if (opt->loginfo && !opt->no_commit_id) { + /* When showing a verbose header (i.e. log message), + * and not in --pretty=oneline format, we would want + * an extra newline between the end of log and the + * output for readability. + */ + show_log(opt, opt->diffopt.msg_sep); + if (opt->verbose_header && + opt->commit_format != CMIT_FMT_ONELINE) { + int pch = DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_PATCH; + if ((pch & opt->diffopt.output_format) == pch) + printf("---"); + putchar('\n'); + } + } + diff_flush(&opt->diffopt); + return 1; +} + +static int do_diff_combined(struct rev_info *opt, struct commit *commit) +{ + unsigned const char *sha1 = commit->object.sha1; + + diff_tree_combined_merge(sha1, opt->dense_combined_merges, opt); + return !opt->loginfo; +} + +/* + * Show the diff of a commit. + * + * Return true if we printed any log info messages + */ +static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log_info *log) +{ + int showed_log; + struct commit_list *parents; + unsigned const char *sha1 = commit->object.sha1; + + if (!opt->diff) + return 0; + + /* Root commit? */ + parents = commit->parents; + if (!parents) { + if (opt->show_root_diff) { + diff_root_tree_sha1(sha1, "", &opt->diffopt); + log_tree_diff_flush(opt); + } + return !opt->loginfo; + } + + /* More than one parent? */ + if (parents && parents->next) { + if (opt->ignore_merges) + return 0; + else if (opt->combine_merges) + return do_diff_combined(opt, commit); + + /* If we show individual diffs, show the parent info */ + log->parent = parents->item; + } + + showed_log = 0; + for (;;) { + struct commit *parent = parents->item; + + diff_tree_sha1(parent->object.sha1, sha1, "", &opt->diffopt); + log_tree_diff_flush(opt); + + showed_log |= !opt->loginfo; + + /* Set up the log info for the next parent, if any.. */ + parents = parents->next; + if (!parents) + break; + log->parent = parents->item; + opt->loginfo = log; + } + return showed_log; +} + +int log_tree_commit(struct rev_info *opt, struct commit *commit) +{ + struct log_info log; + int shown; + + log.commit = commit; + log.parent = NULL; + opt->loginfo = &log; + + shown = log_tree_diff(opt, commit, &log); + if (!shown && opt->loginfo && opt->always_show_header) { + log.parent = NULL; + show_log(opt, ""); + shown = 1; + } + opt->loginfo = NULL; + return shown; +} diff --git a/log-tree.h b/log-tree.h new file mode 100644 index 0000000000..e82b56a20d --- /dev/null +++ b/log-tree.h @@ -0,0 +1,16 @@ +#ifndef LOG_TREE_H +#define LOG_TREE_H + +#include "revision.h" + +struct log_info { + struct commit *commit, *parent; +}; + +void init_log_tree_opt(struct rev_info *); +int log_tree_diff_flush(struct rev_info *); +int log_tree_commit(struct rev_info *, struct commit *); +int log_tree_opt_parse(struct rev_info *, const char **, int); +void show_log(struct rev_info *opt, const char *sep); + +#endif diff --git a/merge-base.c b/merge-base.c new file mode 100644 index 0000000000..385f4ba386 --- /dev/null +++ b/merge-base.c @@ -0,0 +1,53 @@ +#include "cache.h" +#include "commit.h" + +static int show_all; + +static int merge_base(struct commit *rev1, struct commit *rev2) +{ + struct commit_list *result = get_merge_bases(rev1, rev2, 0); + + if (!result) + return 1; + + while (result) { + printf("%s\n", sha1_to_hex(result->item->object.sha1)); + if (!show_all) + return 0; + result = result->next; + } + + return 0; +} + +static const char merge_base_usage[] = +"git-merge-base [--all] <commit-id> <commit-id>"; + +int main(int argc, char **argv) +{ + struct commit *rev1, *rev2; + unsigned char rev1key[20], rev2key[20]; + + setup_git_directory(); + git_config(git_default_config); + + while (1 < argc && argv[1][0] == '-') { + char *arg = argv[1]; + if (!strcmp(arg, "-a") || !strcmp(arg, "--all")) + show_all = 1; + else + usage(merge_base_usage); + argc--; argv++; + } + if (argc != 3) + usage(merge_base_usage); + if (get_sha1(argv[1], rev1key)) + die("Not a valid object name %s", argv[1]); + if (get_sha1(argv[2], rev2key)) + die("Not a valid object name %s", argv[2]); + rev1 = lookup_commit_reference(rev1key); + rev2 = lookup_commit_reference(rev2key); + if (!rev1 || !rev2) + return 1; + return merge_base(rev1, rev2); +} diff --git a/merge-file.c b/merge-file.c new file mode 100644 index 0000000000..69dc1ebbf7 --- /dev/null +++ b/merge-file.c @@ -0,0 +1,117 @@ +#include "cache.h" +#include "run-command.h" +#include "xdiff-interface.h" +#include "blob.h" + +static int fill_mmfile_blob(mmfile_t *f, struct blob *obj) +{ + void *buf; + unsigned long size; + char type[20]; + + buf = read_sha1_file(obj->object.sha1, type, &size); + if (!buf) + return -1; + if (strcmp(type, blob_type)) + return -1; + f->ptr = buf; + f->size = size; + return 0; +} + +static void free_mmfile(mmfile_t *f) +{ + free(f->ptr); +} + +static void *three_way_filemerge(mmfile_t *base, mmfile_t *our, mmfile_t *their, unsigned long *size) +{ + mmbuffer_t res; + xpparam_t xpp; + int merge_status; + + memset(&xpp, 0, sizeof(xpp)); + merge_status = xdl_merge(base, our, ".our", their, ".their", + &xpp, XDL_MERGE_ZEALOUS, &res); + + if (merge_status < 0) + return NULL; + + *size = res.size; + return res.ptr; +} + +static int common_outf(void *priv_, mmbuffer_t *mb, int nbuf) +{ + int i; + mmfile_t *dst = priv_; + + for (i = 0; i < nbuf; i++) { + memcpy(dst->ptr + dst->size, mb[i].ptr, mb[i].size); + dst->size += mb[i].size; + } + return 0; +} + +static int generate_common_file(mmfile_t *res, mmfile_t *f1, mmfile_t *f2) +{ + unsigned long size = f1->size < f2->size ? f1->size : f2->size; + void *ptr = xmalloc(size); + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + + xpp.flags = XDF_NEED_MINIMAL; + xecfg.ctxlen = 3; + xecfg.flags = XDL_EMIT_COMMON; + ecb.outf = common_outf; + + res->ptr = ptr; + res->size = 0; + + ecb.priv = res; + return xdl_diff(f1, f2, &xpp, &xecfg, &ecb); +} + +void *merge_file(struct blob *base, struct blob *our, struct blob *their, unsigned long *size) +{ + void *res = NULL; + mmfile_t f1, f2, common; + + /* + * Removed in either branch? + * + * NOTE! This depends on the caller having done the + * proper warning about removing a file that got + * modified in the other branch! + */ + if (!our || !their) { + char type[20]; + if (base) + return NULL; + if (!our) + our = their; + return read_sha1_file(our->object.sha1, type, size); + } + + if (fill_mmfile_blob(&f1, our) < 0) + goto out_no_mmfile; + if (fill_mmfile_blob(&f2, their) < 0) + goto out_free_f1; + + if (base) { + if (fill_mmfile_blob(&common, base) < 0) + goto out_free_f2_f1; + } else { + if (generate_common_file(&common, &f1, &f2) < 0) + goto out_free_f2_f1; + } + res = three_way_filemerge(&common, &f1, &f2, size); + free_mmfile(&common); +out_free_f2_f1: + free_mmfile(&f2); +out_free_f1: + free_mmfile(&f1); +out_no_mmfile: + return res; +} diff --git a/merge-index.c b/merge-index.c new file mode 100644 index 0000000000..a9983dd78a --- /dev/null +++ b/merge-index.c @@ -0,0 +1,139 @@ +#include "cache.h" + +static const char *pgm; +static const char *arguments[8]; +static int one_shot, quiet; +static int err; + +static void run_program(void) +{ + pid_t pid = fork(); + int status; + + if (pid < 0) + die("unable to fork"); + if (!pid) { + execlp(pgm, arguments[0], + arguments[1], + arguments[2], + arguments[3], + arguments[4], + arguments[5], + arguments[6], + arguments[7], + NULL); + die("unable to execute '%s'", pgm); + } + if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) || WEXITSTATUS(status)) { + if (one_shot) { + err++; + } else { + if (!quiet) + die("merge program failed"); + exit(1); + } + } +} + +static int merge_entry(int pos, const char *path) +{ + int found; + + if (pos >= active_nr) + die("git-merge-index: %s not in the cache", path); + arguments[0] = pgm; + arguments[1] = ""; + arguments[2] = ""; + arguments[3] = ""; + arguments[4] = path; + arguments[5] = ""; + arguments[6] = ""; + arguments[7] = ""; + found = 0; + do { + static char hexbuf[4][60]; + static char ownbuf[4][60]; + struct cache_entry *ce = active_cache[pos]; + int stage = ce_stage(ce); + + if (strcmp(ce->name, path)) + break; + found++; + strcpy(hexbuf[stage], sha1_to_hex(ce->sha1)); + sprintf(ownbuf[stage], "%o", ntohl(ce->ce_mode) & (~S_IFMT)); + arguments[stage] = hexbuf[stage]; + arguments[stage + 4] = ownbuf[stage]; + } while (++pos < active_nr); + if (!found) + die("git-merge-index: %s not in the cache", path); + run_program(); + return found; +} + +static void merge_file(const char *path) +{ + int pos = cache_name_pos(path, strlen(path)); + + /* + * If it already exists in the cache as stage0, it's + * already merged and there is nothing to do. + */ + if (pos < 0) + merge_entry(-pos-1, path); +} + +static void merge_all(void) +{ + int i; + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (!ce_stage(ce)) + continue; + i += merge_entry(i, ce->name)-1; + } +} + +int main(int argc, char **argv) +{ + int i, force_file = 0; + + /* Without this we cannot rely on waitpid() to tell + * what happened to our children. + */ + signal(SIGCHLD, SIG_DFL); + + if (argc < 3) + usage("git-merge-index [-o] [-q] <merge-program> (-a | <filename>*)"); + + setup_git_directory(); + read_cache(); + + i = 1; + if (!strcmp(argv[i], "-o")) { + one_shot = 1; + i++; + } + if (!strcmp(argv[i], "-q")) { + quiet = 1; + i++; + } + pgm = argv[i++]; + for (; i < argc; i++) { + char *arg = argv[i]; + if (!force_file && *arg == '-') { + if (!strcmp(arg, "--")) { + force_file = 1; + continue; + } + if (!strcmp(arg, "-a")) { + merge_all(); + continue; + } + die("git-merge-index: unknown option %s", arg); + } + merge_file(arg); + } + if (err && !quiet) + die("merge program failed"); + return err; +} diff --git a/merge-recursive.c b/merge-recursive.c new file mode 100644 index 0000000000..58989424d7 --- /dev/null +++ b/merge-recursive.c @@ -0,0 +1,1369 @@ +/* + * Recursive Merge algorithm stolen from git-merge-recursive.py by + * Fredrik Kuivinen. + * The thieves were Alex Riesen and Johannes Schindelin, in June/July 2006 + */ +#include "cache.h" +#include "cache-tree.h" +#include "commit.h" +#include "blob.h" +#include "tree-walk.h" +#include "diff.h" +#include "diffcore.h" +#include "run-command.h" +#include "tag.h" +#include "unpack-trees.h" +#include "path-list.h" +#include "xdiff-interface.h" + +/* + * A virtual commit has + * - (const char *)commit->util set to the name, and + * - *(int *)commit->object.sha1 set to the virtual id. + */ + +static unsigned commit_list_count(const struct commit_list *l) +{ + unsigned c = 0; + for (; l; l = l->next ) + c++; + return c; +} + +static struct commit *make_virtual_commit(struct tree *tree, const char *comment) +{ + struct commit *commit = xcalloc(1, sizeof(struct commit)); + static unsigned virtual_id = 1; + commit->tree = tree; + commit->util = (void*)comment; + *(int*)commit->object.sha1 = virtual_id++; + /* avoid warnings */ + commit->object.parsed = 1; + return commit; +} + +/* + * Since we use get_tree_entry(), which does not put the read object into + * the object pool, we cannot rely on a == b. + */ +static int sha_eq(const unsigned char *a, const unsigned char *b) +{ + if (!a && !b) + return 2; + return a && b && hashcmp(a, b) == 0; +} + +/* + * Since we want to write the index eventually, we cannot reuse the index + * for these (temporary) data. + */ +struct stage_data +{ + struct + { + unsigned mode; + unsigned char sha[20]; + } stages[4]; + unsigned processed:1; +}; + +struct output_buffer +{ + struct output_buffer *next; + char *str; +}; + +static struct path_list current_file_set = {NULL, 0, 0, 1}; +static struct path_list current_directory_set = {NULL, 0, 0, 1}; + +static int call_depth = 0; +static int verbosity = 2; +static int buffer_output = 1; +static int do_progress = 1; +static unsigned last_percent; +static unsigned merged_cnt; +static unsigned total_cnt; +static volatile sig_atomic_t progress_update; +static struct output_buffer *output_list, *output_end; + +static int show (int v) +{ + return (!call_depth && verbosity >= v) || verbosity >= 5; +} + +static void output(int v, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + if (buffer_output && show(v)) { + struct output_buffer *b = xmalloc(sizeof(*b)); + nfvasprintf(&b->str, fmt, args); + b->next = NULL; + if (output_end) + output_end->next = b; + else + output_list = b; + output_end = b; + } else if (show(v)) { + int i; + for (i = call_depth; i--;) + fputs(" ", stdout); + vfprintf(stdout, fmt, args); + fputc('\n', stdout); + } + va_end(args); +} + +static void flush_output() +{ + struct output_buffer *b, *n; + for (b = output_list; b; b = n) { + int i; + for (i = call_depth; i--;) + fputs(" ", stdout); + fputs(b->str, stdout); + fputc('\n', stdout); + n = b->next; + free(b->str); + free(b); + } + output_list = NULL; + output_end = NULL; +} + +static void output_commit_title(struct commit *commit) +{ + int i; + flush_output(); + for (i = call_depth; i--;) + fputs(" ", stdout); + if (commit->util) + printf("virtual %s\n", (char *)commit->util); + else { + printf("%s ", find_unique_abbrev(commit->object.sha1, DEFAULT_ABBREV)); + if (parse_commit(commit) != 0) + printf("(bad commit)\n"); + else { + const char *s; + int len; + for (s = commit->buffer; *s; s++) + if (*s == '\n' && s[1] == '\n') { + s += 2; + break; + } + for (len = 0; s[len] && '\n' != s[len]; len++) + ; /* do nothing */ + printf("%.*s\n", len, s); + } + } +} + +static void progress_interval(int signum) +{ + progress_update = 1; +} + +static void setup_progress_signal(void) +{ + struct sigaction sa; + struct itimerval v; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = progress_interval; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction(SIGALRM, &sa, NULL); + + v.it_interval.tv_sec = 1; + v.it_interval.tv_usec = 0; + v.it_value = v.it_interval; + setitimer(ITIMER_REAL, &v, NULL); +} + +static void display_progress() +{ + unsigned percent = total_cnt ? merged_cnt * 100 / total_cnt : 0; + if (progress_update || percent != last_percent) { + fprintf(stderr, "%4u%% (%u/%u) done\r", + percent, merged_cnt, total_cnt); + progress_update = 0; + last_percent = percent; + } +} + +static struct cache_entry *make_cache_entry(unsigned int mode, + const unsigned char *sha1, const char *path, int stage, int refresh) +{ + int size, len; + struct cache_entry *ce; + + if (!verify_path(path)) + return NULL; + + len = strlen(path); + size = cache_entry_size(len); + ce = xcalloc(1, size); + + hashcpy(ce->sha1, sha1); + memcpy(ce->name, path, len); + ce->ce_flags = create_ce_flags(len, stage); + ce->ce_mode = create_ce_mode(mode); + + if (refresh) + return refresh_cache_entry(ce, 0); + + return ce; +} + +static int add_cacheinfo(unsigned int mode, const unsigned char *sha1, + const char *path, int stage, int refresh, int options) +{ + struct cache_entry *ce; + ce = make_cache_entry(mode, sha1 ? sha1 : null_sha1, path, stage, refresh); + if (!ce) + return error("cache_addinfo failed: %s", strerror(cache_errno)); + return add_cache_entry(ce, options); +} + +/* + * This is a global variable which is used in a number of places but + * only written to in the 'merge' function. + * + * index_only == 1 => Don't leave any non-stage 0 entries in the cache and + * don't update the working directory. + * 0 => Leave unmerged entries in the cache and update + * the working directory. + */ +static int index_only = 0; + +static int git_merge_trees(int index_only, + struct tree *common, + struct tree *head, + struct tree *merge) +{ + int rc; + struct object_list *trees = NULL; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + if (index_only) + opts.index_only = 1; + else + opts.update = 1; + opts.merge = 1; + opts.head_idx = 2; + opts.fn = threeway_merge; + + object_list_append(&common->object, &trees); + object_list_append(&head->object, &trees); + object_list_append(&merge->object, &trees); + + rc = unpack_trees(trees, &opts); + cache_tree_free(&active_cache_tree); + return rc; +} + +static int unmerged_index(void) +{ + int i; + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (ce_stage(ce)) + return 1; + } + return 0; +} + +static struct tree *git_write_tree(void) +{ + struct tree *result = NULL; + + if (unmerged_index()) + return NULL; + + if (!active_cache_tree) + active_cache_tree = cache_tree(); + + if (!cache_tree_fully_valid(active_cache_tree) && + cache_tree_update(active_cache_tree, + active_cache, active_nr, 0, 0) < 0) + die("error building trees"); + + result = lookup_tree(active_cache_tree->sha1); + + return result; +} + +static int save_files_dirs(const unsigned char *sha1, + const char *base, int baselen, const char *path, + unsigned int mode, int stage) +{ + int len = strlen(path); + char *newpath = xmalloc(baselen + len + 1); + memcpy(newpath, base, baselen); + memcpy(newpath + baselen, path, len); + newpath[baselen + len] = '\0'; + + if (S_ISDIR(mode)) + path_list_insert(newpath, ¤t_directory_set); + else + path_list_insert(newpath, ¤t_file_set); + free(newpath); + + return READ_TREE_RECURSIVE; +} + +static int get_files_dirs(struct tree *tree) +{ + int n; + if (read_tree_recursive(tree, "", 0, 0, NULL, save_files_dirs) != 0) + return 0; + n = current_file_set.nr + current_directory_set.nr; + return n; +} + +/* + * Returns a index_entry instance which doesn't have to correspond to + * a real cache entry in Git's index. + */ +static struct stage_data *insert_stage_data(const char *path, + struct tree *o, struct tree *a, struct tree *b, + struct path_list *entries) +{ + struct path_list_item *item; + struct stage_data *e = xcalloc(1, sizeof(struct stage_data)); + get_tree_entry(o->object.sha1, path, + e->stages[1].sha, &e->stages[1].mode); + get_tree_entry(a->object.sha1, path, + e->stages[2].sha, &e->stages[2].mode); + get_tree_entry(b->object.sha1, path, + e->stages[3].sha, &e->stages[3].mode); + item = path_list_insert(path, entries); + item->util = e; + return e; +} + +/* + * Create a dictionary mapping file names to stage_data objects. The + * dictionary contains one entry for every path with a non-zero stage entry. + */ +static struct path_list *get_unmerged(void) +{ + struct path_list *unmerged = xcalloc(1, sizeof(struct path_list)); + int i; + + unmerged->strdup_paths = 1; + total_cnt += active_nr; + + for (i = 0; i < active_nr; i++, merged_cnt++) { + struct path_list_item *item; + struct stage_data *e; + struct cache_entry *ce = active_cache[i]; + if (do_progress) + display_progress(); + if (!ce_stage(ce)) + continue; + + item = path_list_lookup(ce->name, unmerged); + if (!item) { + item = path_list_insert(ce->name, unmerged); + item->util = xcalloc(1, sizeof(struct stage_data)); + } + e = item->util; + e->stages[ce_stage(ce)].mode = ntohl(ce->ce_mode); + hashcpy(e->stages[ce_stage(ce)].sha, ce->sha1); + } + + return unmerged; +} + +struct rename +{ + struct diff_filepair *pair; + struct stage_data *src_entry; + struct stage_data *dst_entry; + unsigned processed:1; +}; + +/* + * Get information of all renames which occurred between 'o_tree' and + * 'tree'. We need the three trees in the merge ('o_tree', 'a_tree' and + * 'b_tree') to be able to associate the correct cache entries with + * the rename information. 'tree' is always equal to either a_tree or b_tree. + */ +static struct path_list *get_renames(struct tree *tree, + struct tree *o_tree, + struct tree *a_tree, + struct tree *b_tree, + struct path_list *entries) +{ + int i; + struct path_list *renames; + struct diff_options opts; + + renames = xcalloc(1, sizeof(struct path_list)); + diff_setup(&opts); + opts.recursive = 1; + opts.detect_rename = DIFF_DETECT_RENAME; + opts.output_format = DIFF_FORMAT_NO_OUTPUT; + if (diff_setup_done(&opts) < 0) + die("diff setup failed"); + diff_tree_sha1(o_tree->object.sha1, tree->object.sha1, "", &opts); + diffcore_std(&opts); + for (i = 0; i < diff_queued_diff.nr; ++i) { + struct path_list_item *item; + struct rename *re; + struct diff_filepair *pair = diff_queued_diff.queue[i]; + if (pair->status != 'R') { + diff_free_filepair(pair); + continue; + } + re = xmalloc(sizeof(*re)); + re->processed = 0; + re->pair = pair; + item = path_list_lookup(re->pair->one->path, entries); + if (!item) + re->src_entry = insert_stage_data(re->pair->one->path, + o_tree, a_tree, b_tree, entries); + else + re->src_entry = item->util; + + item = path_list_lookup(re->pair->two->path, entries); + if (!item) + re->dst_entry = insert_stage_data(re->pair->two->path, + o_tree, a_tree, b_tree, entries); + else + re->dst_entry = item->util; + item = path_list_insert(pair->one->path, renames); + item->util = re; + } + opts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_queued_diff.nr = 0; + diff_flush(&opts); + return renames; +} + +static int update_stages(const char *path, struct diff_filespec *o, + struct diff_filespec *a, struct diff_filespec *b, + int clear) +{ + int options = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE; + if (clear) + if (remove_file_from_cache(path)) + return -1; + if (o) + if (add_cacheinfo(o->mode, o->sha1, path, 1, 0, options)) + return -1; + if (a) + if (add_cacheinfo(a->mode, a->sha1, path, 2, 0, options)) + return -1; + if (b) + if (add_cacheinfo(b->mode, b->sha1, path, 3, 0, options)) + return -1; + return 0; +} + +static int remove_path(const char *name) +{ + int ret, len; + char *slash, *dirs; + + ret = unlink(name); + if (ret) + return ret; + len = strlen(name); + dirs = xmalloc(len+1); + memcpy(dirs, name, len); + dirs[len] = '\0'; + while ((slash = strrchr(name, '/'))) { + *slash = '\0'; + len = slash - name; + if (rmdir(name) != 0) + break; + } + free(dirs); + return ret; +} + +static int remove_file(int clean, const char *path, int no_wd) +{ + int update_cache = index_only || clean; + int update_working_directory = !index_only && !no_wd; + + if (update_cache) { + if (remove_file_from_cache(path)) + return -1; + } + if (update_working_directory) { + unlink(path); + if (errno != ENOENT || errno != EISDIR) + return -1; + remove_path(path); + } + return 0; +} + +static char *unique_path(const char *path, const char *branch) +{ + char *newpath = xmalloc(strlen(path) + 1 + strlen(branch) + 8 + 1); + int suffix = 0; + struct stat st; + char *p = newpath + strlen(path); + strcpy(newpath, path); + *(p++) = '~'; + strcpy(p, branch); + for (; *p; ++p) + if ('/' == *p) + *p = '_'; + while (path_list_has_path(¤t_file_set, newpath) || + path_list_has_path(¤t_directory_set, newpath) || + lstat(newpath, &st) == 0) + sprintf(p, "_%d", suffix++); + + path_list_insert(newpath, ¤t_file_set); + return newpath; +} + +static int mkdir_p(const char *path, unsigned long mode) +{ + /* path points to cache entries, so xstrdup before messing with it */ + char *buf = xstrdup(path); + int result = safe_create_leading_directories(buf); + free(buf); + return result; +} + +static void flush_buffer(int fd, const char *buf, unsigned long size) +{ + while (size > 0) { + long ret = write_in_full(fd, buf, size); + if (ret < 0) { + /* Ignore epipe */ + if (errno == EPIPE) + break; + die("merge-recursive: %s", strerror(errno)); + } else if (!ret) { + die("merge-recursive: disk full?"); + } + size -= ret; + buf += ret; + } +} + +static void update_file_flags(const unsigned char *sha, + unsigned mode, + const char *path, + int update_cache, + int update_wd) +{ + if (index_only) + update_wd = 0; + + if (update_wd) { + char type[20]; + void *buf; + unsigned long size; + + buf = read_sha1_file(sha, type, &size); + if (!buf) + die("cannot read object %s '%s'", sha1_to_hex(sha), path); + if (strcmp(type, blob_type) != 0) + die("blob expected for %s '%s'", sha1_to_hex(sha), path); + + if (S_ISREG(mode)) { + int fd; + if (mkdir_p(path, 0777)) + die("failed to create path %s: %s", path, strerror(errno)); + unlink(path); + if (mode & 0100) + mode = 0777; + else + mode = 0666; + fd = open(path, O_WRONLY | O_TRUNC | O_CREAT, mode); + if (fd < 0) + die("failed to open %s: %s", path, strerror(errno)); + flush_buffer(fd, buf, size); + close(fd); + } else if (S_ISLNK(mode)) { + char *lnk = xmalloc(size + 1); + memcpy(lnk, buf, size); + lnk[size] = '\0'; + mkdir_p(path, 0777); + unlink(lnk); + symlink(lnk, path); + } else + die("do not know what to do with %06o %s '%s'", + mode, sha1_to_hex(sha), path); + } + if (update_cache) + add_cacheinfo(mode, sha, path, 0, update_wd, ADD_CACHE_OK_TO_ADD); +} + +static void update_file(int clean, + const unsigned char *sha, + unsigned mode, + const char *path) +{ + update_file_flags(sha, mode, path, index_only || clean, !index_only); +} + +/* Low level file merging, update and removal */ + +struct merge_file_info +{ + unsigned char sha[20]; + unsigned mode; + unsigned clean:1, + merge:1; +}; + +static void fill_mm(const unsigned char *sha1, mmfile_t *mm) +{ + unsigned long size; + char type[20]; + + if (!hashcmp(sha1, null_sha1)) { + mm->ptr = xstrdup(""); + mm->size = 0; + return; + } + + mm->ptr = read_sha1_file(sha1, type, &size); + if (!mm->ptr || strcmp(type, blob_type)) + die("unable to read blob object %s", sha1_to_hex(sha1)); + mm->size = size; +} + +static struct merge_file_info merge_file(struct diff_filespec *o, + struct diff_filespec *a, struct diff_filespec *b, + const char *branch1, const char *branch2) +{ + struct merge_file_info result; + result.merge = 0; + result.clean = 1; + + if ((S_IFMT & a->mode) != (S_IFMT & b->mode)) { + result.clean = 0; + if (S_ISREG(a->mode)) { + result.mode = a->mode; + hashcpy(result.sha, a->sha1); + } else { + result.mode = b->mode; + hashcpy(result.sha, b->sha1); + } + } else { + if (!sha_eq(a->sha1, o->sha1) && !sha_eq(b->sha1, o->sha1)) + result.merge = 1; + + result.mode = a->mode == o->mode ? b->mode: a->mode; + + if (sha_eq(a->sha1, o->sha1)) + hashcpy(result.sha, b->sha1); + else if (sha_eq(b->sha1, o->sha1)) + hashcpy(result.sha, a->sha1); + else if (S_ISREG(a->mode)) { + mmfile_t orig, src1, src2; + mmbuffer_t result_buf; + xpparam_t xpp; + char *name1, *name2; + int merge_status; + + name1 = xstrdup(mkpath("%s:%s", branch1, a->path)); + name2 = xstrdup(mkpath("%s:%s", branch2, b->path)); + + fill_mm(o->sha1, &orig); + fill_mm(a->sha1, &src1); + fill_mm(b->sha1, &src2); + + memset(&xpp, 0, sizeof(xpp)); + merge_status = xdl_merge(&orig, + &src1, name1, + &src2, name2, + &xpp, XDL_MERGE_ZEALOUS, + &result_buf); + free(name1); + free(name2); + free(orig.ptr); + free(src1.ptr); + free(src2.ptr); + + if ((merge_status < 0) || !result_buf.ptr) + die("Failed to execute internal merge"); + + if (write_sha1_file(result_buf.ptr, result_buf.size, + blob_type, result.sha)) + die("Unable to add %s to database", + a->path); + + free(result_buf.ptr); + result.clean = (merge_status == 0); + } else { + if (!(S_ISLNK(a->mode) || S_ISLNK(b->mode))) + die("cannot merge modes?"); + + hashcpy(result.sha, a->sha1); + + if (!sha_eq(a->sha1, b->sha1)) + result.clean = 0; + } + } + + return result; +} + +static void conflict_rename_rename(struct rename *ren1, + const char *branch1, + struct rename *ren2, + const char *branch2) +{ + char *del[2]; + int delp = 0; + const char *ren1_dst = ren1->pair->two->path; + const char *ren2_dst = ren2->pair->two->path; + const char *dst_name1 = ren1_dst; + const char *dst_name2 = ren2_dst; + if (path_list_has_path(¤t_directory_set, ren1_dst)) { + dst_name1 = del[delp++] = unique_path(ren1_dst, branch1); + output(1, "%s is a directory in %s added as %s instead", + ren1_dst, branch2, dst_name1); + remove_file(0, ren1_dst, 0); + } + if (path_list_has_path(¤t_directory_set, ren2_dst)) { + dst_name2 = del[delp++] = unique_path(ren2_dst, branch2); + output(1, "%s is a directory in %s added as %s instead", + ren2_dst, branch1, dst_name2); + remove_file(0, ren2_dst, 0); + } + update_stages(dst_name1, NULL, ren1->pair->two, NULL, 1); + update_stages(dst_name2, NULL, NULL, ren2->pair->two, 1); + while (delp--) + free(del[delp]); +} + +static void conflict_rename_dir(struct rename *ren1, + const char *branch1) +{ + char *new_path = unique_path(ren1->pair->two->path, branch1); + output(1, "Renamed %s to %s instead", ren1->pair->one->path, new_path); + remove_file(0, ren1->pair->two->path, 0); + update_file(0, ren1->pair->two->sha1, ren1->pair->two->mode, new_path); + free(new_path); +} + +static void conflict_rename_rename_2(struct rename *ren1, + const char *branch1, + struct rename *ren2, + const char *branch2) +{ + char *new_path1 = unique_path(ren1->pair->two->path, branch1); + char *new_path2 = unique_path(ren2->pair->two->path, branch2); + output(1, "Renamed %s to %s and %s to %s instead", + ren1->pair->one->path, new_path1, + ren2->pair->one->path, new_path2); + remove_file(0, ren1->pair->two->path, 0); + update_file(0, ren1->pair->two->sha1, ren1->pair->two->mode, new_path1); + update_file(0, ren2->pair->two->sha1, ren2->pair->two->mode, new_path2); + free(new_path2); + free(new_path1); +} + +static int process_renames(struct path_list *a_renames, + struct path_list *b_renames, + const char *a_branch, + const char *b_branch) +{ + int clean_merge = 1, i, j; + struct path_list a_by_dst = {NULL, 0, 0, 0}, b_by_dst = {NULL, 0, 0, 0}; + const struct rename *sre; + + for (i = 0; i < a_renames->nr; i++) { + sre = a_renames->items[i].util; + path_list_insert(sre->pair->two->path, &a_by_dst)->util + = sre->dst_entry; + } + for (i = 0; i < b_renames->nr; i++) { + sre = b_renames->items[i].util; + path_list_insert(sre->pair->two->path, &b_by_dst)->util + = sre->dst_entry; + } + + for (i = 0, j = 0; i < a_renames->nr || j < b_renames->nr;) { + int compare; + char *src; + struct path_list *renames1, *renames2, *renames2Dst; + struct rename *ren1 = NULL, *ren2 = NULL; + const char *branch1, *branch2; + const char *ren1_src, *ren1_dst; + + if (i >= a_renames->nr) { + compare = 1; + ren2 = b_renames->items[j++].util; + } else if (j >= b_renames->nr) { + compare = -1; + ren1 = a_renames->items[i++].util; + } else { + compare = strcmp(a_renames->items[i].path, + b_renames->items[j].path); + if (compare <= 0) + ren1 = a_renames->items[i++].util; + if (compare >= 0) + ren2 = b_renames->items[j++].util; + } + + /* TODO: refactor, so that 1/2 are not needed */ + if (ren1) { + renames1 = a_renames; + renames2 = b_renames; + renames2Dst = &b_by_dst; + branch1 = a_branch; + branch2 = b_branch; + } else { + struct rename *tmp; + renames1 = b_renames; + renames2 = a_renames; + renames2Dst = &a_by_dst; + branch1 = b_branch; + branch2 = a_branch; + tmp = ren2; + ren2 = ren1; + ren1 = tmp; + } + src = ren1->pair->one->path; + + ren1->dst_entry->processed = 1; + ren1->src_entry->processed = 1; + + if (ren1->processed) + continue; + ren1->processed = 1; + + ren1_src = ren1->pair->one->path; + ren1_dst = ren1->pair->two->path; + + if (ren2) { + const char *ren2_src = ren2->pair->one->path; + const char *ren2_dst = ren2->pair->two->path; + /* Renamed in 1 and renamed in 2 */ + if (strcmp(ren1_src, ren2_src) != 0) + die("ren1.src != ren2.src"); + ren2->dst_entry->processed = 1; + ren2->processed = 1; + if (strcmp(ren1_dst, ren2_dst) != 0) { + clean_merge = 0; + output(1, "CONFLICT (rename/rename): " + "Rename %s->%s in branch %s " + "rename %s->%s in %s", + src, ren1_dst, branch1, + src, ren2_dst, branch2); + conflict_rename_rename(ren1, branch1, ren2, branch2); + } else { + struct merge_file_info mfi; + remove_file(1, ren1_src, 1); + mfi = merge_file(ren1->pair->one, + ren1->pair->two, + ren2->pair->two, + branch1, + branch2); + if (mfi.merge || !mfi.clean) + output(1, "Renamed %s->%s", src, ren1_dst); + + if (mfi.merge) + output(2, "Auto-merged %s", ren1_dst); + + if (!mfi.clean) { + output(1, "CONFLICT (content): merge conflict in %s", + ren1_dst); + clean_merge = 0; + + if (!index_only) + update_stages(ren1_dst, + ren1->pair->one, + ren1->pair->two, + ren2->pair->two, + 1 /* clear */); + } + update_file(mfi.clean, mfi.sha, mfi.mode, ren1_dst); + } + } else { + /* Renamed in 1, maybe changed in 2 */ + struct path_list_item *item; + /* we only use sha1 and mode of these */ + struct diff_filespec src_other, dst_other; + int try_merge, stage = a_renames == renames1 ? 3: 2; + + remove_file(1, ren1_src, index_only || stage == 3); + + hashcpy(src_other.sha1, ren1->src_entry->stages[stage].sha); + src_other.mode = ren1->src_entry->stages[stage].mode; + hashcpy(dst_other.sha1, ren1->dst_entry->stages[stage].sha); + dst_other.mode = ren1->dst_entry->stages[stage].mode; + + try_merge = 0; + + if (path_list_has_path(¤t_directory_set, ren1_dst)) { + clean_merge = 0; + output(1, "CONFLICT (rename/directory): Renamed %s->%s in %s " + " directory %s added in %s", + ren1_src, ren1_dst, branch1, + ren1_dst, branch2); + conflict_rename_dir(ren1, branch1); + } else if (sha_eq(src_other.sha1, null_sha1)) { + clean_merge = 0; + output(1, "CONFLICT (rename/delete): Renamed %s->%s in %s " + "and deleted in %s", + ren1_src, ren1_dst, branch1, + branch2); + update_file(0, ren1->pair->two->sha1, ren1->pair->two->mode, ren1_dst); + } else if (!sha_eq(dst_other.sha1, null_sha1)) { + const char *new_path; + clean_merge = 0; + try_merge = 1; + output(1, "CONFLICT (rename/add): Renamed %s->%s in %s. " + "%s added in %s", + ren1_src, ren1_dst, branch1, + ren1_dst, branch2); + new_path = unique_path(ren1_dst, branch2); + output(1, "Added as %s instead", new_path); + update_file(0, dst_other.sha1, dst_other.mode, new_path); + } else if ((item = path_list_lookup(ren1_dst, renames2Dst))) { + ren2 = item->util; + clean_merge = 0; + ren2->processed = 1; + output(1, "CONFLICT (rename/rename): Renamed %s->%s in %s. " + "Renamed %s->%s in %s", + ren1_src, ren1_dst, branch1, + ren2->pair->one->path, ren2->pair->two->path, branch2); + conflict_rename_rename_2(ren1, branch1, ren2, branch2); + } else + try_merge = 1; + + if (try_merge) { + struct diff_filespec *o, *a, *b; + struct merge_file_info mfi; + src_other.path = (char *)ren1_src; + + o = ren1->pair->one; + if (a_renames == renames1) { + a = ren1->pair->two; + b = &src_other; + } else { + b = ren1->pair->two; + a = &src_other; + } + mfi = merge_file(o, a, b, + a_branch, b_branch); + + if (mfi.merge || !mfi.clean) + output(1, "Renamed %s => %s", ren1_src, ren1_dst); + if (mfi.merge) + output(2, "Auto-merged %s", ren1_dst); + if (!mfi.clean) { + output(1, "CONFLICT (rename/modify): Merge conflict in %s", + ren1_dst); + clean_merge = 0; + + if (!index_only) + update_stages(ren1_dst, + o, a, b, 1); + } + update_file(mfi.clean, mfi.sha, mfi.mode, ren1_dst); + } + } + } + path_list_clear(&a_by_dst, 0); + path_list_clear(&b_by_dst, 0); + + return clean_merge; +} + +static unsigned char *has_sha(const unsigned char *sha) +{ + return is_null_sha1(sha) ? NULL: (unsigned char *)sha; +} + +/* Per entry merge function */ +static int process_entry(const char *path, struct stage_data *entry, + const char *branch1, + const char *branch2) +{ + /* + printf("processing entry, clean cache: %s\n", index_only ? "yes": "no"); + print_index_entry("\tpath: ", entry); + */ + int clean_merge = 1; + unsigned char *o_sha = has_sha(entry->stages[1].sha); + unsigned char *a_sha = has_sha(entry->stages[2].sha); + unsigned char *b_sha = has_sha(entry->stages[3].sha); + unsigned o_mode = entry->stages[1].mode; + unsigned a_mode = entry->stages[2].mode; + unsigned b_mode = entry->stages[3].mode; + + if (o_sha && (!a_sha || !b_sha)) { + /* Case A: Deleted in one */ + if ((!a_sha && !b_sha) || + (sha_eq(a_sha, o_sha) && !b_sha) || + (!a_sha && sha_eq(b_sha, o_sha))) { + /* Deleted in both or deleted in one and + * unchanged in the other */ + if (a_sha) + output(2, "Removed %s", path); + /* do not touch working file if it did not exist */ + remove_file(1, path, !a_sha); + } else { + /* Deleted in one and changed in the other */ + clean_merge = 0; + if (!a_sha) { + output(1, "CONFLICT (delete/modify): %s deleted in %s " + "and modified in %s. Version %s of %s left in tree.", + path, branch1, + branch2, branch2, path); + update_file(0, b_sha, b_mode, path); + } else { + output(1, "CONFLICT (delete/modify): %s deleted in %s " + "and modified in %s. Version %s of %s left in tree.", + path, branch2, + branch1, branch1, path); + update_file(0, a_sha, a_mode, path); + } + } + + } else if ((!o_sha && a_sha && !b_sha) || + (!o_sha && !a_sha && b_sha)) { + /* Case B: Added in one. */ + const char *add_branch; + const char *other_branch; + unsigned mode; + const unsigned char *sha; + const char *conf; + + if (a_sha) { + add_branch = branch1; + other_branch = branch2; + mode = a_mode; + sha = a_sha; + conf = "file/directory"; + } else { + add_branch = branch2; + other_branch = branch1; + mode = b_mode; + sha = b_sha; + conf = "directory/file"; + } + if (path_list_has_path(¤t_directory_set, path)) { + const char *new_path = unique_path(path, add_branch); + clean_merge = 0; + output(1, "CONFLICT (%s): There is a directory with name %s in %s. " + "Added %s as %s", + conf, path, other_branch, path, new_path); + remove_file(0, path, 0); + update_file(0, sha, mode, new_path); + } else { + output(2, "Added %s", path); + update_file(1, sha, mode, path); + } + } else if (a_sha && b_sha) { + /* Case C: Added in both (check for same permissions) and */ + /* case D: Modified in both, but differently. */ + const char *reason = "content"; + struct merge_file_info mfi; + struct diff_filespec o, a, b; + + if (!o_sha) { + reason = "add/add"; + o_sha = (unsigned char *)null_sha1; + } + output(2, "Auto-merged %s", path); + o.path = a.path = b.path = (char *)path; + hashcpy(o.sha1, o_sha); + o.mode = o_mode; + hashcpy(a.sha1, a_sha); + a.mode = a_mode; + hashcpy(b.sha1, b_sha); + b.mode = b_mode; + + mfi = merge_file(&o, &a, &b, + branch1, branch2); + + if (mfi.clean) + update_file(1, mfi.sha, mfi.mode, path); + else { + clean_merge = 0; + output(1, "CONFLICT (%s): Merge conflict in %s", + reason, path); + + if (index_only) + update_file(0, mfi.sha, mfi.mode, path); + else + update_file_flags(mfi.sha, mfi.mode, path, + 0 /* update_cache */, 1 /* update_working_directory */); + } + } else + die("Fatal merge failure, shouldn't happen."); + + return clean_merge; +} + +static int merge_trees(struct tree *head, + struct tree *merge, + struct tree *common, + const char *branch1, + const char *branch2, + struct tree **result) +{ + int code, clean; + if (sha_eq(common->object.sha1, merge->object.sha1)) { + output(0, "Already uptodate!"); + *result = head; + return 1; + } + + code = git_merge_trees(index_only, common, head, merge); + + if (code != 0) + die("merging of trees %s and %s failed", + sha1_to_hex(head->object.sha1), + sha1_to_hex(merge->object.sha1)); + + if (unmerged_index()) { + struct path_list *entries, *re_head, *re_merge; + int i; + path_list_clear(¤t_file_set, 1); + path_list_clear(¤t_directory_set, 1); + get_files_dirs(head); + get_files_dirs(merge); + + entries = get_unmerged(); + re_head = get_renames(head, common, head, merge, entries); + re_merge = get_renames(merge, common, head, merge, entries); + clean = process_renames(re_head, re_merge, + branch1, branch2); + total_cnt += entries->nr; + for (i = 0; i < entries->nr; i++, merged_cnt++) { + const char *path = entries->items[i].path; + struct stage_data *e = entries->items[i].util; + if (!e->processed + && !process_entry(path, e, branch1, branch2)) + clean = 0; + if (do_progress) + display_progress(); + } + + path_list_clear(re_merge, 0); + path_list_clear(re_head, 0); + path_list_clear(entries, 1); + + } + else + clean = 1; + + if (index_only) + *result = git_write_tree(); + + return clean; +} + +static struct commit_list *reverse_commit_list(struct commit_list *list) +{ + struct commit_list *next = NULL, *current, *backup; + for (current = list; current; current = backup) { + backup = current->next; + current->next = next; + next = current; + } + return next; +} + +/* + * Merge the commits h1 and h2, return the resulting virtual + * commit object and a flag indicating the cleanness of the merge. + */ +static int merge(struct commit *h1, + struct commit *h2, + const char *branch1, + const char *branch2, + struct commit_list *ca, + struct commit **result) +{ + struct commit_list *iter; + struct commit *merged_common_ancestors; + struct tree *mrtree; + int clean; + + if (show(4)) { + output(4, "Merging:"); + output_commit_title(h1); + output_commit_title(h2); + } + + if (!ca) { + ca = get_merge_bases(h1, h2, 1); + ca = reverse_commit_list(ca); + } + + if (show(5)) { + output(5, "found %u common ancestor(s):", commit_list_count(ca)); + for (iter = ca; iter; iter = iter->next) + output_commit_title(iter->item); + } + + merged_common_ancestors = pop_commit(&ca); + if (merged_common_ancestors == NULL) { + /* if there is no common ancestor, make an empty tree */ + struct tree *tree = xcalloc(1, sizeof(struct tree)); + + tree->object.parsed = 1; + tree->object.type = OBJ_TREE; + pretend_sha1_file(NULL, 0, tree_type, tree->object.sha1); + merged_common_ancestors = make_virtual_commit(tree, "ancestor"); + } + + for (iter = ca; iter; iter = iter->next) { + call_depth++; + /* + * When the merge fails, the result contains files + * with conflict markers. The cleanness flag is + * ignored, it was never actually used, as result of + * merge_trees has always overwritten it: the committed + * "conflicts" were already resolved. + */ + discard_cache(); + merge(merged_common_ancestors, iter->item, + "Temporary merge branch 1", + "Temporary merge branch 2", + NULL, + &merged_common_ancestors); + call_depth--; + + if (!merged_common_ancestors) + die("merge returned no commit"); + } + + discard_cache(); + if (!call_depth) { + read_cache(); + index_only = 0; + } else + index_only = 1; + + clean = merge_trees(h1->tree, h2->tree, merged_common_ancestors->tree, + branch1, branch2, &mrtree); + + if (index_only) { + *result = make_virtual_commit(mrtree, "merged tree"); + commit_list_insert(h1, &(*result)->parents); + commit_list_insert(h2, &(*result)->parents->next); + } + if (!call_depth && do_progress) { + /* Make sure we end at 100% */ + if (!total_cnt) + total_cnt = 1; + merged_cnt = total_cnt; + progress_update = 1; + display_progress(); + fputc('\n', stderr); + } + flush_output(); + return clean; +} + +static const char *better_branch_name(const char *branch) +{ + static char githead_env[8 + 40 + 1]; + char *name; + + if (strlen(branch) != 40) + return branch; + sprintf(githead_env, "GITHEAD_%s", branch); + name = getenv(githead_env); + return name ? name : branch; +} + +static struct commit *get_ref(const char *ref) +{ + unsigned char sha1[20]; + struct object *object; + + if (get_sha1(ref, sha1)) + die("Could not resolve ref '%s'", ref); + object = deref_tag(parse_object(sha1), ref, strlen(ref)); + if (object->type == OBJ_TREE) + return make_virtual_commit((struct tree*)object, + better_branch_name(ref)); + if (object->type != OBJ_COMMIT) + return NULL; + if (parse_commit((struct commit *)object)) + die("Could not parse commit '%s'", sha1_to_hex(object->sha1)); + return (struct commit *)object; +} + +static int merge_config(const char *var, const char *value) +{ + if (!strcasecmp(var, "merge.verbosity")) { + verbosity = git_config_int(var, value); + return 0; + } + return git_default_config(var, value); +} + +int main(int argc, char *argv[]) +{ + static const char *bases[20]; + static unsigned bases_count = 0; + int i, clean; + const char *branch1, *branch2; + struct commit *result, *h1, *h2; + struct commit_list *ca = NULL; + struct lock_file *lock = xcalloc(1, sizeof(struct lock_file)); + int index_fd; + + git_config(merge_config); + if (getenv("GIT_MERGE_VERBOSITY")) + verbosity = strtol(getenv("GIT_MERGE_VERBOSITY"), NULL, 10); + + if (argc < 4) + die("Usage: %s <base>... -- <head> <remote> ...\n", argv[0]); + + for (i = 1; i < argc; ++i) { + if (!strcmp(argv[i], "--")) + break; + if (bases_count < sizeof(bases)/sizeof(*bases)) + bases[bases_count++] = argv[i]; + } + if (argc - i != 3) /* "--" "<head>" "<remote>" */ + die("Not handling anything other than two heads merge."); + if (verbosity >= 5) { + buffer_output = 0; + do_progress = 0; + } + else + do_progress = isatty(1); + + branch1 = argv[++i]; + branch2 = argv[++i]; + + h1 = get_ref(branch1); + h2 = get_ref(branch2); + + branch1 = better_branch_name(branch1); + branch2 = better_branch_name(branch2); + + if (do_progress) + setup_progress_signal(); + if (show(3)) + printf("Merging %s with %s\n", branch1, branch2); + + index_fd = hold_lock_file_for_update(lock, get_index_file(), 1); + + for (i = 0; i < bases_count; i++) { + struct commit *ancestor = get_ref(bases[i]); + ca = commit_list_insert(ancestor, &ca); + } + clean = merge(h1, h2, branch1, branch2, ca, &result); + + if (active_cache_changed && + (write_cache(index_fd, active_cache, active_nr) || + close(index_fd) || commit_lock_file(lock))) + die ("unable to write %s", get_index_file()); + + return clean ? 0: 1; +} diff --git a/merge-tree.c b/merge-tree.c new file mode 100644 index 0000000000..692ede0e3d --- /dev/null +++ b/merge-tree.c @@ -0,0 +1,355 @@ +#include "cache.h" +#include "tree-walk.h" +#include "xdiff-interface.h" +#include "blob.h" + +static const char merge_tree_usage[] = "git-merge-tree <base-tree> <branch1> <branch2>"; +static int resolve_directories = 1; + +struct merge_list { + struct merge_list *next; + struct merge_list *link; /* other stages for this object */ + + unsigned int stage : 2, + flags : 30; + unsigned int mode; + const char *path; + struct blob *blob; +}; + +static struct merge_list *merge_result, **merge_result_end = &merge_result; + +static void add_merge_entry(struct merge_list *entry) +{ + *merge_result_end = entry; + merge_result_end = &entry->next; +} + +static void merge_trees(struct tree_desc t[3], const char *base); + +static const char *explanation(struct merge_list *entry) +{ + switch (entry->stage) { + case 0: + return "merged"; + case 3: + return "added in remote"; + case 2: + if (entry->link) + return "added in both"; + return "added in local"; + } + + /* Existed in base */ + entry = entry->link; + if (!entry) + return "removed in both"; + + if (entry->link) + return "changed in both"; + + if (entry->stage == 3) + return "removed in local"; + return "removed in remote"; +} + +extern void *merge_file(struct blob *, struct blob *, struct blob *, unsigned long *); + +static void *result(struct merge_list *entry, unsigned long *size) +{ + char type[20]; + struct blob *base, *our, *their; + + if (!entry->stage) + return read_sha1_file(entry->blob->object.sha1, type, size); + base = NULL; + if (entry->stage == 1) { + base = entry->blob; + entry = entry->link; + } + our = NULL; + if (entry && entry->stage == 2) { + our = entry->blob; + entry = entry->link; + } + their = NULL; + if (entry) + their = entry->blob; + return merge_file(base, our, their, size); +} + +static void *origin(struct merge_list *entry, unsigned long *size) +{ + char type[20]; + while (entry) { + if (entry->stage == 2) + return read_sha1_file(entry->blob->object.sha1, type, size); + entry = entry->link; + } + return NULL; +} + +static int show_outf(void *priv_, mmbuffer_t *mb, int nbuf) +{ + int i; + for (i = 0; i < nbuf; i++) + printf("%.*s", (int) mb[i].size, mb[i].ptr); + return 0; +} + +static void show_diff(struct merge_list *entry) +{ + unsigned long size; + mmfile_t src, dst; + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + + xpp.flags = XDF_NEED_MINIMAL; + xecfg.ctxlen = 3; + xecfg.flags = 0; + ecb.outf = show_outf; + ecb.priv = NULL; + + src.ptr = origin(entry, &size); + if (!src.ptr) + size = 0; + src.size = size; + dst.ptr = result(entry, &size); + if (!dst.ptr) + size = 0; + dst.size = size; + xdl_diff(&src, &dst, &xpp, &xecfg, &ecb); + free(src.ptr); + free(dst.ptr); +} + +static void show_result_list(struct merge_list *entry) +{ + printf("%s\n", explanation(entry)); + do { + struct merge_list *link = entry->link; + static const char *desc[4] = { "result", "base", "our", "their" }; + printf(" %-6s %o %s %s\n", desc[entry->stage], entry->mode, sha1_to_hex(entry->blob->object.sha1), entry->path); + entry = link; + } while (entry); +} + +static void show_result(void) +{ + struct merge_list *walk; + + walk = merge_result; + while (walk) { + show_result_list(walk); + show_diff(walk); + walk = walk->next; + } +} + +/* An empty entry never compares same, not even to another empty entry */ +static int same_entry(struct name_entry *a, struct name_entry *b) +{ + return a->sha1 && + b->sha1 && + !hashcmp(a->sha1, b->sha1) && + a->mode == b->mode; +} + +static struct merge_list *create_entry(unsigned stage, unsigned mode, const unsigned char *sha1, const char *path) +{ + struct merge_list *res = xmalloc(sizeof(*res)); + + memset(res, 0, sizeof(*res)); + res->stage = stage; + res->path = path; + res->mode = mode; + res->blob = lookup_blob(sha1); + return res; +} + +static void resolve(const char *base, struct name_entry *branch1, struct name_entry *result) +{ + struct merge_list *orig, *final; + const char *path; + + /* If it's already branch1, don't bother showing it */ + if (!branch1) + return; + + path = xstrdup(mkpath("%s%s", base, result->path)); + orig = create_entry(2, branch1->mode, branch1->sha1, path); + final = create_entry(0, result->mode, result->sha1, path); + + final->link = orig; + + add_merge_entry(final); +} + +static int unresolved_directory(const char *base, struct name_entry n[3]) +{ + int baselen; + char *newbase; + struct name_entry *p; + struct tree_desc t[3]; + void *buf0, *buf1, *buf2; + + if (!resolve_directories) + return 0; + p = n; + if (!p->mode) { + p++; + if (!p->mode) + p++; + } + if (!S_ISDIR(p->mode)) + return 0; + baselen = strlen(base); + newbase = xmalloc(baselen + p->pathlen + 2); + memcpy(newbase, base, baselen); + memcpy(newbase + baselen, p->path, p->pathlen); + memcpy(newbase + baselen + p->pathlen, "/", 2); + + buf0 = fill_tree_descriptor(t+0, n[0].sha1); + buf1 = fill_tree_descriptor(t+1, n[1].sha1); + buf2 = fill_tree_descriptor(t+2, n[2].sha1); + merge_trees(t, newbase); + + free(buf0); + free(buf1); + free(buf2); + free(newbase); + return 1; +} + + +static struct merge_list *link_entry(unsigned stage, const char *base, struct name_entry *n, struct merge_list *entry) +{ + const char *path; + struct merge_list *link; + + if (!n->mode) + return entry; + if (entry) + path = entry->path; + else + path = xstrdup(mkpath("%s%s", base, n->path)); + link = create_entry(stage, n->mode, n->sha1, path); + link->link = entry; + return link; +} + +static void unresolved(const char *base, struct name_entry n[3]) +{ + struct merge_list *entry = NULL; + + if (unresolved_directory(base, n)) + return; + + /* + * Do them in reverse order so that the resulting link + * list has the stages in order - link_entry adds new + * links at the front. + */ + entry = link_entry(3, base, n + 2, entry); + entry = link_entry(2, base, n + 1, entry); + entry = link_entry(1, base, n + 0, entry); + + add_merge_entry(entry); +} + +/* + * Merge two trees together (t[1] and t[2]), using a common base (t[0]) + * as the origin. + * + * This walks the (sorted) trees in lock-step, checking every possible + * name. Note that directories automatically sort differently from other + * files (see "base_name_compare"), so you'll never see file/directory + * conflicts, because they won't ever compare the same. + * + * IOW, if a directory changes to a filename, it will automatically be + * seen as the directory going away, and the filename being created. + * + * Think of this as a three-way diff. + * + * The output will be either: + * - successful merge + * "0 mode sha1 filename" + * NOTE NOTE NOTE! FIXME! We really really need to walk the index + * in parallel with this too! + * + * - conflict: + * "1 mode sha1 filename" + * "2 mode sha1 filename" + * "3 mode sha1 filename" + * where not all of the 1/2/3 lines may exist, of course. + * + * The successful merge rules are the same as for the three-way merge + * in git-read-tree. + */ +static void threeway_callback(int n, unsigned long mask, struct name_entry *entry, const char *base) +{ + /* Same in both? */ + if (same_entry(entry+1, entry+2)) { + if (entry[0].sha1) { + resolve(base, NULL, entry+1); + return; + } + } + + if (same_entry(entry+0, entry+1)) { + if (entry[2].sha1 && !S_ISDIR(entry[2].mode)) { + resolve(base, entry+1, entry+2); + return; + } + } + + if (same_entry(entry+0, entry+2)) { + if (entry[1].sha1 && !S_ISDIR(entry[1].mode)) { + resolve(base, NULL, entry+1); + return; + } + } + + unresolved(base, entry); +} + +static void merge_trees(struct tree_desc t[3], const char *base) +{ + traverse_trees(3, t, base, threeway_callback); +} + +static void *get_tree_descriptor(struct tree_desc *desc, const char *rev) +{ + unsigned char sha1[20]; + void *buf; + + if (get_sha1(rev, sha1)) + die("unknown rev %s", rev); + buf = fill_tree_descriptor(desc, sha1); + if (!buf) + die("%s is not a tree", rev); + return buf; +} + +int main(int argc, char **argv) +{ + struct tree_desc t[3]; + void *buf1, *buf2, *buf3; + + if (argc != 4) + usage(merge_tree_usage); + + setup_git_directory(); + + buf1 = get_tree_descriptor(t+0, argv[1]); + buf2 = get_tree_descriptor(t+1, argv[2]); + buf3 = get_tree_descriptor(t+2, argv[3]); + merge_trees(t, ""); + free(buf1); + free(buf2); + free(buf3); + + show_result(); + return 0; +} diff --git a/mktag.c b/mktag.c new file mode 100644 index 0000000000..3448a5dde7 --- /dev/null +++ b/mktag.c @@ -0,0 +1,147 @@ +#include "cache.h" +#include "tag.h" + +/* + * A signature file has a very simple fixed format: four lines + * of "object <sha1>" + "type <typename>" + "tag <tagname>" + + * "tagger <committer>", followed by a blank line, a free-form tag + * message and a signature block that git itself doesn't care about, + * but that can be verified with gpg or similar. + * + * The first three lines are guaranteed to be at least 63 bytes: + * "object <sha1>\n" is 48 bytes, "type tag\n" at 9 bytes is the + * shortest possible type-line, and "tag .\n" at 6 bytes is the + * shortest single-character-tag line. + * + * We also artificially limit the size of the full object to 8kB. + * Just because I'm a lazy bastard, and if you can't fit a signature + * in that size, you're doing something wrong. + */ + +/* Some random size */ +#define MAXSIZE (8192) + +/* + * We refuse to tag something we can't verify. Just because. + */ +static int verify_object(unsigned char *sha1, const char *expected_type) +{ + int ret = -1; + char type[100]; + unsigned long size; + void *buffer = read_sha1_file(sha1, type, &size); + + if (buffer) { + if (!strcmp(type, expected_type)) + ret = check_sha1_signature(sha1, buffer, size, type); + free(buffer); + } + return ret; +} + +#ifdef NO_C99_FORMAT +#define PD_FMT "%d" +#else +#define PD_FMT "%td" +#endif + +static int verify_tag(char *buffer, unsigned long size) +{ + int typelen; + char type[20]; + unsigned char sha1[20]; + const char *object, *type_line, *tag_line, *tagger_line; + + if (size < 64) + return error("wanna fool me ? you obviously got the size wrong !"); + + buffer[size] = 0; + + /* Verify object line */ + object = buffer; + if (memcmp(object, "object ", 7)) + return error("char%d: does not start with \"object \"", 0); + + if (get_sha1_hex(object + 7, sha1)) + return error("char%d: could not get SHA1 hash", 7); + + /* Verify type line */ + type_line = object + 48; + if (memcmp(type_line - 1, "\ntype ", 6)) + return error("char%d: could not find \"\\ntype \"", 47); + + /* Verify tag-line */ + tag_line = strchr(type_line, '\n'); + if (!tag_line) + return error("char" PD_FMT ": could not find next \"\\n\"", type_line - buffer); + tag_line++; + if (memcmp(tag_line, "tag ", 4) || tag_line[4] == '\n') + return error("char" PD_FMT ": no \"tag \" found", tag_line - buffer); + + /* Get the actual type */ + typelen = tag_line - type_line - strlen("type \n"); + if (typelen >= sizeof(type)) + return error("char" PD_FMT ": type too long", type_line+5 - buffer); + + memcpy(type, type_line+5, typelen); + type[typelen] = 0; + + /* Verify that the object matches */ + if (verify_object(sha1, type)) + return error("char%d: could not verify object %s", 7, sha1_to_hex(sha1)); + + /* Verify the tag-name: we don't allow control characters or spaces in it */ + tag_line += 4; + for (;;) { + unsigned char c = *tag_line++; + if (c == '\n') + break; + if (c > ' ') + continue; + return error("char" PD_FMT ": could not verify tag name", tag_line - buffer); + } + + /* Verify the tagger line */ + tagger_line = tag_line; + + if (memcmp(tagger_line, "tagger", 6) || (tagger_line[6] == '\n')) + return error("char" PD_FMT ": could not find \"tagger\"", tagger_line - buffer); + + /* TODO: check for committer info + blank line? */ + /* Also, the minimum length is probably + "tagger .", or 63+8=71 */ + + /* The actual stuff afterwards we don't care about.. */ + return 0; +} + +#undef PD_FMT + +int main(int argc, char **argv) +{ + unsigned long size = 4096; + char *buffer = xmalloc(size); + unsigned char result_sha1[20]; + + if (argc != 1) + usage("git-mktag < signaturefile"); + + setup_git_directory(); + + if (read_pipe(0, &buffer, &size)) { + free(buffer); + die("could not read from stdin"); + } + + /* Verify it for some basic sanity: it needs to start with + "object <sha1>\ntype\ntagger " */ + if (verify_tag(buffer, size) < 0) + die("invalid tag signature file"); + + if (write_sha1_file(buffer, size, tag_type, result_sha1) < 0) + die("unable to write tag file"); + + free(buffer); + + printf("%s\n", sha1_to_hex(result_sha1)); + return 0; +} diff --git a/mktree.c b/mktree.c new file mode 100644 index 0000000000..56205d1e00 --- /dev/null +++ b/mktree.c @@ -0,0 +1,137 @@ +/* + * GIT - the stupid content tracker + * + * Copyright (c) Junio C Hamano, 2006 + */ +#include "cache.h" +#include "strbuf.h" +#include "quote.h" +#include "tree.h" + +static struct treeent { + unsigned mode; + unsigned char sha1[20]; + int len; + char name[FLEX_ARRAY]; +} **entries; +static int alloc, used; + +static void append_to_tree(unsigned mode, unsigned char *sha1, char *path) +{ + struct treeent *ent; + int len = strlen(path); + if (strchr(path, '/')) + die("path %s contains slash", path); + + if (alloc <= used) { + alloc = alloc_nr(used); + entries = xrealloc(entries, sizeof(*entries) * alloc); + } + ent = entries[used++] = xmalloc(sizeof(**entries) + len + 1); + ent->mode = mode; + ent->len = len; + hashcpy(ent->sha1, sha1); + memcpy(ent->name, path, len+1); +} + +static int ent_compare(const void *a_, const void *b_) +{ + struct treeent *a = *(struct treeent **)a_; + struct treeent *b = *(struct treeent **)b_; + return base_name_compare(a->name, a->len, a->mode, + b->name, b->len, b->mode); +} + +static void write_tree(unsigned char *sha1) +{ + char *buffer; + unsigned long size, offset; + int i; + + qsort(entries, used, sizeof(*entries), ent_compare); + for (size = i = 0; i < used; i++) + size += 32 + entries[i]->len; + buffer = xmalloc(size); + offset = 0; + + for (i = 0; i < used; i++) { + struct treeent *ent = entries[i]; + + if (offset + ent->len + 100 < size) { + size = alloc_nr(offset + ent->len + 100); + buffer = xrealloc(buffer, size); + } + offset += sprintf(buffer + offset, "%o ", ent->mode); + offset += sprintf(buffer + offset, "%s", ent->name); + buffer[offset++] = 0; + hashcpy((unsigned char*)buffer + offset, ent->sha1); + offset += 20; + } + write_sha1_file(buffer, offset, tree_type, sha1); +} + +static const char mktree_usage[] = "git-mktree [-z]"; + +int main(int ac, char **av) +{ + struct strbuf sb; + unsigned char sha1[20]; + int line_termination = '\n'; + + setup_git_directory(); + + while ((1 < ac) && av[1][0] == '-') { + char *arg = av[1]; + if (!strcmp("-z", arg)) + line_termination = 0; + else + usage(mktree_usage); + ac--; + av++; + } + + strbuf_init(&sb); + while (1) { + int len; + char *ptr, *ntr; + unsigned mode; + char type[20]; + char *path; + + read_line(&sb, stdin, line_termination); + if (sb.eof) + break; + len = sb.len; + ptr = sb.buf; + /* Input is non-recursive ls-tree output format + * mode SP type SP sha1 TAB name + */ + mode = strtoul(ptr, &ntr, 8); + if (ptr == ntr || !ntr || *ntr != ' ') + die("input format error: %s", sb.buf); + ptr = ntr + 1; /* type */ + ntr = strchr(ptr, ' '); + if (!ntr || sb.buf + len <= ntr + 41 || + ntr[41] != '\t' || + get_sha1_hex(ntr + 1, sha1)) + die("input format error: %s", sb.buf); + if (sha1_object_info(sha1, type, NULL)) + die("object %s unavailable", sha1_to_hex(sha1)); + *ntr++ = 0; /* now at the beginning of SHA1 */ + if (strcmp(ptr, type)) + die("object type %s mismatch (%s)", ptr, type); + ntr += 41; /* at the beginning of name */ + if (line_termination && ntr[0] == '"') + path = unquote_c_style(ntr, NULL); + else + path = ntr; + + append_to_tree(mode, sha1, path); + + if (path != ntr) + free(path); + } + write_tree(sha1); + puts(sha1_to_hex(sha1)); + exit(0); +} diff --git a/mozilla-sha1/sha1.c b/mozilla-sha1/sha1.c new file mode 100644 index 0000000000..847531d19f --- /dev/null +++ b/mozilla-sha1/sha1.c @@ -0,0 +1,152 @@ +/* + * The contents of this file are subject to the Mozilla Public + * License Version 1.1 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * The Original Code is SHA 180-1 Reference Implementation (Compact version) + * + * The Initial Developer of the Original Code is Paul Kocher of + * Cryptography Research. Portions created by Paul Kocher are + * Copyright (C) 1995-9 by Cryptography Research, Inc. All + * Rights Reserved. + * + * Contributor(s): + * + * Paul Kocher + * + * Alternatively, the contents of this file may be used under the + * terms of the GNU General Public License Version 2 or later (the + * "GPL"), in which case the provisions of the GPL are applicable + * instead of those above. If you wish to allow use of your + * version of this file only under the terms of the GPL and not to + * allow others to use your version of this file under the MPL, + * indicate your decision by deleting the provisions above and + * replace them with the notice and other provisions required by + * the GPL. If you do not delete the provisions above, a recipient + * may use your version of this file under either the MPL or the + * GPL. + */ + +#include "sha1.h" + +static void shaHashBlock(SHA_CTX *ctx); + +void SHA1_Init(SHA_CTX *ctx) { + int i; + + ctx->lenW = 0; + ctx->sizeHi = ctx->sizeLo = 0; + + /* Initialize H with the magic constants (see FIPS180 for constants) + */ + ctx->H[0] = 0x67452301; + ctx->H[1] = 0xefcdab89; + ctx->H[2] = 0x98badcfe; + ctx->H[3] = 0x10325476; + ctx->H[4] = 0xc3d2e1f0; + + for (i = 0; i < 80; i++) + ctx->W[i] = 0; +} + + +void SHA1_Update(SHA_CTX *ctx, const void *_dataIn, int len) { + const unsigned char *dataIn = _dataIn; + int i; + + /* Read the data into W and process blocks as they get full + */ + for (i = 0; i < len; i++) { + ctx->W[ctx->lenW / 4] <<= 8; + ctx->W[ctx->lenW / 4] |= (unsigned int)dataIn[i]; + if ((++ctx->lenW) % 64 == 0) { + shaHashBlock(ctx); + ctx->lenW = 0; + } + ctx->sizeLo += 8; + ctx->sizeHi += (ctx->sizeLo < 8); + } +} + + +void SHA1_Final(unsigned char hashout[20], SHA_CTX *ctx) { + unsigned char pad0x80 = 0x80; + unsigned char pad0x00 = 0x00; + unsigned char padlen[8]; + int i; + + /* Pad with a binary 1 (e.g. 0x80), then zeroes, then length + */ + padlen[0] = (unsigned char)((ctx->sizeHi >> 24) & 255); + padlen[1] = (unsigned char)((ctx->sizeHi >> 16) & 255); + padlen[2] = (unsigned char)((ctx->sizeHi >> 8) & 255); + padlen[3] = (unsigned char)((ctx->sizeHi >> 0) & 255); + padlen[4] = (unsigned char)((ctx->sizeLo >> 24) & 255); + padlen[5] = (unsigned char)((ctx->sizeLo >> 16) & 255); + padlen[6] = (unsigned char)((ctx->sizeLo >> 8) & 255); + padlen[7] = (unsigned char)((ctx->sizeLo >> 0) & 255); + SHA1_Update(ctx, &pad0x80, 1); + while (ctx->lenW != 56) + SHA1_Update(ctx, &pad0x00, 1); + SHA1_Update(ctx, padlen, 8); + + /* Output hash + */ + for (i = 0; i < 20; i++) { + hashout[i] = (unsigned char)(ctx->H[i / 4] >> 24); + ctx->H[i / 4] <<= 8; + } + + /* + * Re-initialize the context (also zeroizes contents) + */ + SHA1_Init(ctx); +} + + +#define SHA_ROT(X,n) (((X) << (n)) | ((X) >> (32-(n)))) + +static void shaHashBlock(SHA_CTX *ctx) { + int t; + unsigned int A,B,C,D,E,TEMP; + + for (t = 16; t <= 79; t++) + ctx->W[t] = + SHA_ROT(ctx->W[t-3] ^ ctx->W[t-8] ^ ctx->W[t-14] ^ ctx->W[t-16], 1); + + A = ctx->H[0]; + B = ctx->H[1]; + C = ctx->H[2]; + D = ctx->H[3]; + E = ctx->H[4]; + + for (t = 0; t <= 19; t++) { + TEMP = SHA_ROT(A,5) + (((C^D)&B)^D) + E + ctx->W[t] + 0x5a827999; + E = D; D = C; C = SHA_ROT(B, 30); B = A; A = TEMP; + } + for (t = 20; t <= 39; t++) { + TEMP = SHA_ROT(A,5) + (B^C^D) + E + ctx->W[t] + 0x6ed9eba1; + E = D; D = C; C = SHA_ROT(B, 30); B = A; A = TEMP; + } + for (t = 40; t <= 59; t++) { + TEMP = SHA_ROT(A,5) + ((B&C)|(D&(B|C))) + E + ctx->W[t] + 0x8f1bbcdc; + E = D; D = C; C = SHA_ROT(B, 30); B = A; A = TEMP; + } + for (t = 60; t <= 79; t++) { + TEMP = SHA_ROT(A,5) + (B^C^D) + E + ctx->W[t] + 0xca62c1d6; + E = D; D = C; C = SHA_ROT(B, 30); B = A; A = TEMP; + } + + ctx->H[0] += A; + ctx->H[1] += B; + ctx->H[2] += C; + ctx->H[3] += D; + ctx->H[4] += E; +} + diff --git a/mozilla-sha1/sha1.h b/mozilla-sha1/sha1.h new file mode 100644 index 0000000000..5d82afa3bd --- /dev/null +++ b/mozilla-sha1/sha1.h @@ -0,0 +1,45 @@ +/* + * The contents of this file are subject to the Mozilla Public + * License Version 1.1 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * The Original Code is SHA 180-1 Header File + * + * The Initial Developer of the Original Code is Paul Kocher of + * Cryptography Research. Portions created by Paul Kocher are + * Copyright (C) 1995-9 by Cryptography Research, Inc. All + * Rights Reserved. + * + * Contributor(s): + * + * Paul Kocher + * + * Alternatively, the contents of this file may be used under the + * terms of the GNU General Public License Version 2 or later (the + * "GPL"), in which case the provisions of the GPL are applicable + * instead of those above. If you wish to allow use of your + * version of this file only under the terms of the GPL and not to + * allow others to use your version of this file under the MPL, + * indicate your decision by deleting the provisions above and + * replace them with the notice and other provisions required by + * the GPL. If you do not delete the provisions above, a recipient + * may use your version of this file under either the MPL or the + * GPL. + */ + +typedef struct { + unsigned int H[5]; + unsigned int W[80]; + int lenW; + unsigned int sizeHi,sizeLo; +} SHA_CTX; + +void SHA1_Init(SHA_CTX *ctx); +void SHA1_Update(SHA_CTX *ctx, const void *dataIn, int len); +void SHA1_Final(unsigned char hashout[20], SHA_CTX *ctx); diff --git a/object-refs.c b/object-refs.c new file mode 100644 index 0000000000..98ea10005a --- /dev/null +++ b/object-refs.c @@ -0,0 +1,144 @@ +#include "cache.h" +#include "object.h" + +int track_object_refs = 0; + +static unsigned int refs_hash_size, nr_object_refs; +static struct object_refs **refs_hash; + +static unsigned int hash_obj(struct object *obj, unsigned int n) +{ + unsigned int hash = *(unsigned int *)obj->sha1; + return hash % n; +} + +static void insert_ref_hash(struct object_refs *ref, struct object_refs **hash, unsigned int size) +{ + int j = hash_obj(ref->base, size); + + while (hash[j]) { + j++; + if (j >= size) + j = 0; + } + hash[j] = ref; +} + +static void grow_refs_hash(void) +{ + int i; + int new_hash_size = (refs_hash_size + 1000) * 3 / 2; + struct object_refs **new_hash; + + new_hash = xcalloc(new_hash_size, sizeof(struct object_refs *)); + for (i = 0; i < refs_hash_size; i++) { + struct object_refs *ref = refs_hash[i]; + if (!ref) + continue; + insert_ref_hash(ref, new_hash, new_hash_size); + } + free(refs_hash); + refs_hash = new_hash; + refs_hash_size = new_hash_size; +} + +static void add_object_refs(struct object *obj, struct object_refs *ref) +{ + int nr = nr_object_refs + 1; + + if (nr > refs_hash_size * 2 / 3) + grow_refs_hash(); + ref->base = obj; + insert_ref_hash(ref, refs_hash, refs_hash_size); + nr_object_refs = nr; +} + +struct object_refs *lookup_object_refs(struct object *obj) +{ + struct object_refs *ref; + int j; + + /* nothing to lookup */ + if (!refs_hash_size) + return NULL; + j = hash_obj(obj, refs_hash_size); + while ((ref = refs_hash[j]) != NULL) { + if (ref->base == obj) + break; + j++; + if (j >= refs_hash_size) + j = 0; + } + return ref; +} + +struct object_refs *alloc_object_refs(unsigned count) +{ + struct object_refs *refs; + size_t size = sizeof(*refs) + count*sizeof(struct object *); + + refs = xcalloc(1, size); + refs->count = count; + return refs; +} + +static int compare_object_pointers(const void *a, const void *b) +{ + const struct object * const *pa = a; + const struct object * const *pb = b; + if (*pa == *pb) + return 0; + else if (*pa < *pb) + return -1; + else + return 1; +} + +void set_object_refs(struct object *obj, struct object_refs *refs) +{ + unsigned int i, j; + + /* Do not install empty list of references */ + if (refs->count < 1) { + free(refs); + return; + } + + /* Sort the list and filter out duplicates */ + qsort(refs->ref, refs->count, sizeof(refs->ref[0]), + compare_object_pointers); + for (i = j = 1; i < refs->count; i++) { + if (refs->ref[i] != refs->ref[i - 1]) + refs->ref[j++] = refs->ref[i]; + } + if (j < refs->count) { + /* Duplicates were found - reallocate list */ + size_t size = sizeof(*refs) + j*sizeof(struct object *); + refs->count = j; + refs = xrealloc(refs, size); + } + + for (i = 0; i < refs->count; i++) + refs->ref[i]->used = 1; + add_object_refs(obj, refs); +} + +void mark_reachable(struct object *obj, unsigned int mask) +{ + const struct object_refs *refs; + + if (!track_object_refs) + die("cannot do reachability with object refs turned off"); + /* If we've been here already, don't bother */ + if (obj->flags & mask) + return; + obj->flags |= mask; + refs = lookup_object_refs(obj); + if (refs) { + unsigned i; + for (i = 0; i < refs->count; i++) + mark_reachable(refs->ref[i], mask); + } +} + + diff --git a/object.c b/object.c new file mode 100644 index 0000000000..de244e2063 --- /dev/null +++ b/object.c @@ -0,0 +1,252 @@ +#include "cache.h" +#include "object.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "tag.h" + +static struct object **obj_hash; +static int nr_objs, obj_hash_size; + +unsigned int get_max_object_index(void) +{ + return obj_hash_size; +} + +struct object *get_indexed_object(unsigned int idx) +{ + return obj_hash[idx]; +} + +const char *type_names[] = { + "none", "commit", "tree", "blob", "tag", + "bad type 5", "bad type 6", "delta", "bad", +}; + +static unsigned int hash_obj(struct object *obj, unsigned int n) +{ + unsigned int hash = *(unsigned int *)obj->sha1; + return hash % n; +} + +static void insert_obj_hash(struct object *obj, struct object **hash, unsigned int size) +{ + int j = hash_obj(obj, size); + + while (hash[j]) { + j++; + if (j >= size) + j = 0; + } + hash[j] = obj; +} + +static int hashtable_index(const unsigned char *sha1) +{ + unsigned int i; + memcpy(&i, sha1, sizeof(unsigned int)); + return (int)(i % obj_hash_size); +} + +struct object *lookup_object(const unsigned char *sha1) +{ + int i; + struct object *obj; + + if (!obj_hash) + return NULL; + + i = hashtable_index(sha1); + while ((obj = obj_hash[i]) != NULL) { + if (!hashcmp(sha1, obj->sha1)) + break; + i++; + if (i == obj_hash_size) + i = 0; + } + return obj; +} + +static void grow_object_hash(void) +{ + int i; + int new_hash_size = obj_hash_size < 32 ? 32 : 2 * obj_hash_size; + struct object **new_hash; + + new_hash = xcalloc(new_hash_size, sizeof(struct object *)); + for (i = 0; i < obj_hash_size; i++) { + struct object *obj = obj_hash[i]; + if (!obj) + continue; + insert_obj_hash(obj, new_hash, new_hash_size); + } + free(obj_hash); + obj_hash = new_hash; + obj_hash_size = new_hash_size; +} + +void created_object(const unsigned char *sha1, struct object *obj) +{ + obj->parsed = 0; + obj->used = 0; + obj->type = OBJ_NONE; + obj->flags = 0; + hashcpy(obj->sha1, sha1); + + if (obj_hash_size - 1 <= nr_objs * 2) + grow_object_hash(); + + insert_obj_hash(obj, obj_hash, obj_hash_size); + nr_objs++; +} + +struct object *lookup_object_type(const unsigned char *sha1, const char *type) +{ + if (!type) { + return lookup_unknown_object(sha1); + } else if (!strcmp(type, blob_type)) { + return &lookup_blob(sha1)->object; + } else if (!strcmp(type, tree_type)) { + return &lookup_tree(sha1)->object; + } else if (!strcmp(type, commit_type)) { + return &lookup_commit(sha1)->object; + } else if (!strcmp(type, tag_type)) { + return &lookup_tag(sha1)->object; + } else { + error("Unknown type %s", type); + return NULL; + } +} + +union any_object { + struct object object; + struct commit commit; + struct tree tree; + struct blob blob; + struct tag tag; +}; + +struct object *lookup_unknown_object(const unsigned char *sha1) +{ + struct object *obj = lookup_object(sha1); + if (!obj) { + union any_object *ret = xcalloc(1, sizeof(*ret)); + created_object(sha1, &ret->object); + ret->object.type = OBJ_NONE; + return &ret->object; + } + return obj; +} + +struct object *parse_object_buffer(const unsigned char *sha1, const char *type, unsigned long size, void *buffer, int *eaten_p) +{ + struct object *obj; + int eaten = 0; + + if (!strcmp(type, blob_type)) { + struct blob *blob = lookup_blob(sha1); + parse_blob_buffer(blob, buffer, size); + obj = &blob->object; + } else if (!strcmp(type, tree_type)) { + struct tree *tree = lookup_tree(sha1); + obj = &tree->object; + if (!tree->object.parsed) { + parse_tree_buffer(tree, buffer, size); + eaten = 1; + } + } else if (!strcmp(type, commit_type)) { + struct commit *commit = lookup_commit(sha1); + parse_commit_buffer(commit, buffer, size); + if (!commit->buffer) { + commit->buffer = buffer; + eaten = 1; + } + obj = &commit->object; + } else if (!strcmp(type, tag_type)) { + struct tag *tag = lookup_tag(sha1); + parse_tag_buffer(tag, buffer, size); + obj = &tag->object; + } else { + obj = NULL; + } + *eaten_p = eaten; + return obj; +} + +struct object *parse_object(const unsigned char *sha1) +{ + unsigned long size; + char type[20]; + int eaten; + void *buffer = read_sha1_file(sha1, type, &size); + + if (buffer) { + struct object *obj; + if (check_sha1_signature(sha1, buffer, size, type) < 0) + printf("sha1 mismatch %s\n", sha1_to_hex(sha1)); + + obj = parse_object_buffer(sha1, type, size, buffer, &eaten); + if (!eaten) + free(buffer); + return obj; + } + return NULL; +} + +struct object_list *object_list_insert(struct object *item, + struct object_list **list_p) +{ + struct object_list *new_list = xmalloc(sizeof(struct object_list)); + new_list->item = item; + new_list->next = *list_p; + *list_p = new_list; + return new_list; +} + +void object_list_append(struct object *item, + struct object_list **list_p) +{ + while (*list_p) { + list_p = &((*list_p)->next); + } + *list_p = xmalloc(sizeof(struct object_list)); + (*list_p)->next = NULL; + (*list_p)->item = item; +} + +unsigned object_list_length(struct object_list *list) +{ + unsigned ret = 0; + while (list) { + list = list->next; + ret++; + } + return ret; +} + +int object_list_contains(struct object_list *list, struct object *obj) +{ + while (list) { + if (list->item == obj) + return 1; + list = list->next; + } + return 0; +} + +void add_object_array(struct object *obj, const char *name, struct object_array *array) +{ + unsigned nr = array->nr; + unsigned alloc = array->alloc; + struct object_array_entry *objects = array->objects; + + if (nr >= alloc) { + alloc = (alloc + 32) * 2; + objects = xrealloc(objects, alloc * sizeof(*objects)); + array->alloc = alloc; + array->objects = objects; + } + objects[nr].item = obj; + objects[nr].name = name; + array->nr = ++nr; +} diff --git a/object.h b/object.h new file mode 100644 index 0000000000..caee733cde --- /dev/null +++ b/object.h @@ -0,0 +1,89 @@ +#ifndef OBJECT_H +#define OBJECT_H + +struct object_list { + struct object *item; + struct object_list *next; +}; + +struct object_refs { + unsigned count; + struct object *base; + struct object *ref[FLEX_ARRAY]; /* more */ +}; + +struct object_array { + unsigned int nr; + unsigned int alloc; + struct object_array_entry { + struct object *item; + const char *name; + } *objects; +}; + +#define TYPE_BITS 3 +#define FLAG_BITS 27 + +/* + * The object type is stored in 3 bits. + */ +struct object { + unsigned parsed : 1; + unsigned used : 1; + unsigned type : TYPE_BITS; + unsigned flags : FLAG_BITS; + unsigned char sha1[20]; +}; + +extern int track_object_refs; +extern const char *type_names[9]; + +extern unsigned int get_max_object_index(void); +extern struct object *get_indexed_object(unsigned int); + +static inline const char *typename(unsigned int type) +{ + return type_names[type > OBJ_BAD ? OBJ_BAD : type]; +} + +extern struct object_refs *lookup_object_refs(struct object *); + +/** Internal only **/ +struct object *lookup_object(const unsigned char *sha1); + +/** Returns the object, having looked it up as being the given type. **/ +struct object *lookup_object_type(const unsigned char *sha1, const char *type); + +void created_object(const unsigned char *sha1, struct object *obj); + +/** Returns the object, having parsed it to find out what it is. **/ +struct object *parse_object(const unsigned char *sha1); + +/* Given the result of read_sha1_file(), returns the object after + * parsing it. eaten_p indicates if the object has a borrowed copy + * of buffer and the caller should not free() it. + */ +struct object *parse_object_buffer(const unsigned char *sha1, const char *type, unsigned long size, void *buffer, int *eaten_p); + +/** Returns the object, with potentially excess memory allocated. **/ +struct object *lookup_unknown_object(const unsigned char *sha1); + +struct object_refs *alloc_object_refs(unsigned count); +void set_object_refs(struct object *obj, struct object_refs *refs); + +void mark_reachable(struct object *obj, unsigned int mask); + +struct object_list *object_list_insert(struct object *item, + struct object_list **list_p); + +void object_list_append(struct object *item, + struct object_list **list_p); + +unsigned object_list_length(struct object_list *list); + +int object_list_contains(struct object_list *list, struct object *obj); + +/* Object array handling .. */ +void add_object_array(struct object *obj, const char *name, struct object_array *array); + +#endif /* OBJECT_H */ diff --git a/pack-check.c b/pack-check.c new file mode 100644 index 0000000000..08a9fd8dc0 --- /dev/null +++ b/pack-check.c @@ -0,0 +1,158 @@ +#include "cache.h" +#include "pack.h" + +static int verify_packfile(struct packed_git *p, + struct pack_window **w_curs) +{ + unsigned long index_size = p->index_size; + void *index_base = p->index_base; + SHA_CTX ctx; + unsigned char sha1[20]; + unsigned long offset = 0, pack_sig = p->pack_size - 20; + int nr_objects, err, i; + + /* Note that the pack header checks are actually performed by + * use_pack when it first opens the pack file. If anything + * goes wrong during those checks then the call will die out + * immediately. + */ + + SHA1_Init(&ctx); + while (offset < pack_sig) { + unsigned int remaining; + unsigned char *in = use_pack(p, w_curs, offset, &remaining); + offset += remaining; + if (offset > pack_sig) + remaining -= offset - pack_sig; + SHA1_Update(&ctx, in, remaining); + } + SHA1_Final(sha1, &ctx); + if (hashcmp(sha1, use_pack(p, w_curs, pack_sig, NULL))) + return error("Packfile %s SHA1 mismatch with itself", + p->pack_name); + if (hashcmp(sha1, (unsigned char *)index_base + index_size - 40)) + return error("Packfile %s SHA1 mismatch with idx", + p->pack_name); + unuse_pack(w_curs); + + /* Make sure everything reachable from idx is valid. Since we + * have verified that nr_objects matches between idx and pack, + * we do not do scan-streaming check on the pack file. + */ + nr_objects = num_packed_objects(p); + for (i = err = 0; i < nr_objects; i++) { + unsigned char sha1[20]; + void *data; + char type[20]; + unsigned long size, offset; + + if (nth_packed_object_sha1(p, i, sha1)) + die("internal error pack-check nth-packed-object"); + offset = find_pack_entry_one(sha1, p); + if (!offset) + die("internal error pack-check find-pack-entry-one"); + data = unpack_entry(p, offset, type, &size); + if (!data) { + err = error("cannot unpack %s from %s", + sha1_to_hex(sha1), p->pack_name); + continue; + } + if (check_sha1_signature(sha1, data, size, type)) { + err = error("packed %s from %s is corrupt", + sha1_to_hex(sha1), p->pack_name); + free(data); + continue; + } + free(data); + } + + return err; +} + + +#define MAX_CHAIN 40 + +static void show_pack_info(struct packed_git *p) +{ + int nr_objects, i; + unsigned int chain_histogram[MAX_CHAIN]; + + nr_objects = num_packed_objects(p); + memset(chain_histogram, 0, sizeof(chain_histogram)); + + for (i = 0; i < nr_objects; i++) { + unsigned char sha1[20], base_sha1[20]; + char type[20]; + unsigned long size; + unsigned long store_size; + unsigned long offset; + unsigned int delta_chain_length; + + if (nth_packed_object_sha1(p, i, sha1)) + die("internal error pack-check nth-packed-object"); + offset = find_pack_entry_one(sha1, p); + if (!offset) + die("internal error pack-check find-pack-entry-one"); + + packed_object_info_detail(p, offset, type, &size, &store_size, + &delta_chain_length, + base_sha1); + printf("%s ", sha1_to_hex(sha1)); + if (!delta_chain_length) + printf("%-6s %lu %lu\n", type, size, offset); + else { + printf("%-6s %lu %lu %u %s\n", type, size, offset, + delta_chain_length, sha1_to_hex(base_sha1)); + if (delta_chain_length < MAX_CHAIN) + chain_histogram[delta_chain_length]++; + else + chain_histogram[0]++; + } + } + + for (i = 0; i < MAX_CHAIN; i++) { + if (!chain_histogram[i]) + continue; + printf("chain length %s %d: %d object%s\n", + i ? "=" : ">=", + i ? i : MAX_CHAIN, + chain_histogram[i], + 1 < chain_histogram[i] ? "s" : ""); + } +} + +int verify_pack(struct packed_git *p, int verbose) +{ + unsigned long index_size = p->index_size; + void *index_base = p->index_base; + SHA_CTX ctx; + unsigned char sha1[20]; + int ret; + + ret = 0; + /* Verify SHA1 sum of the index file */ + SHA1_Init(&ctx); + SHA1_Update(&ctx, index_base, index_size - 20); + SHA1_Final(sha1, &ctx); + if (hashcmp(sha1, (unsigned char *)index_base + index_size - 20)) + ret = error("Packfile index for %s SHA1 mismatch", + p->pack_name); + + if (!ret) { + /* Verify pack file */ + struct pack_window *w_curs = NULL; + ret = verify_packfile(p, &w_curs); + unuse_pack(&w_curs); + } + + if (verbose) { + if (ret) + printf("%s: bad\n", p->pack_name); + else { + show_pack_info(p); + printf("%s: ok\n", p->pack_name); + } + } + + return ret; +} diff --git a/pack-redundant.c b/pack-redundant.c new file mode 100644 index 0000000000..edb5524fc4 --- /dev/null +++ b/pack-redundant.c @@ -0,0 +1,683 @@ +/* +* +* Copyright 2005, Lukas Sandstrom <lukass@etek.chalmers.se> +* +* This file is licensed under the GPL v2. +* +*/ + +#include "cache.h" + +#define BLKSIZE 512 + +static const char pack_redundant_usage[] = +"git-pack-redundant [ --verbose ] [ --alt-odb ] < --all | <.pack filename> ...>"; + +static int load_all_packs, verbose, alt_odb; + +struct llist_item { + struct llist_item *next; + unsigned char *sha1; +}; +static struct llist { + struct llist_item *front; + struct llist_item *back; + size_t size; +} *all_objects; /* all objects which must be present in local packfiles */ + +static struct pack_list { + struct pack_list *next; + struct packed_git *pack; + struct llist *unique_objects; + struct llist *all_objects; +} *local_packs = NULL, *altodb_packs = NULL; + +struct pll { + struct pll *next; + struct pack_list *pl; +}; + +static struct llist_item *free_nodes; + +static inline void llist_item_put(struct llist_item *item) +{ + item->next = free_nodes; + free_nodes = item; +} + +static inline struct llist_item *llist_item_get(void) +{ + struct llist_item *new; + if ( free_nodes ) { + new = free_nodes; + free_nodes = free_nodes->next; + } else { + int i = 1; + new = xmalloc(sizeof(struct llist_item) * BLKSIZE); + for(;i < BLKSIZE; i++) { + llist_item_put(&new[i]); + } + } + return new; +} + +static void llist_free(struct llist *list) +{ + while((list->back = list->front)) { + list->front = list->front->next; + llist_item_put(list->back); + } + free(list); +} + +static inline void llist_init(struct llist **list) +{ + *list = xmalloc(sizeof(struct llist)); + (*list)->front = (*list)->back = NULL; + (*list)->size = 0; +} + +static struct llist * llist_copy(struct llist *list) +{ + struct llist *ret; + struct llist_item *new, *old, *prev; + + llist_init(&ret); + + if ((ret->size = list->size) == 0) + return ret; + + new = ret->front = llist_item_get(); + new->sha1 = list->front->sha1; + + old = list->front->next; + while (old) { + prev = new; + new = llist_item_get(); + prev->next = new; + new->sha1 = old->sha1; + old = old->next; + } + new->next = NULL; + ret->back = new; + + return ret; +} + +static inline struct llist_item * llist_insert(struct llist *list, + struct llist_item *after, + unsigned char *sha1) +{ + struct llist_item *new = llist_item_get(); + new->sha1 = sha1; + new->next = NULL; + + if (after != NULL) { + new->next = after->next; + after->next = new; + if (after == list->back) + list->back = new; + } else {/* insert in front */ + if (list->size == 0) + list->back = new; + else + new->next = list->front; + list->front = new; + } + list->size++; + return new; +} + +static inline struct llist_item *llist_insert_back(struct llist *list, unsigned char *sha1) +{ + return llist_insert(list, list->back, sha1); +} + +static inline struct llist_item *llist_insert_sorted_unique(struct llist *list, unsigned char *sha1, struct llist_item *hint) +{ + struct llist_item *prev = NULL, *l; + + l = (hint == NULL) ? list->front : hint; + while (l) { + int cmp = hashcmp(l->sha1, sha1); + if (cmp > 0) { /* we insert before this entry */ + return llist_insert(list, prev, sha1); + } + if(!cmp) { /* already exists */ + return l; + } + prev = l; + l = l->next; + } + /* insert at the end */ + return llist_insert_back(list, sha1); +} + +/* returns a pointer to an item in front of sha1 */ +static inline struct llist_item * llist_sorted_remove(struct llist *list, const unsigned char *sha1, struct llist_item *hint) +{ + struct llist_item *prev, *l; + +redo_from_start: + l = (hint == NULL) ? list->front : hint; + prev = NULL; + while (l) { + int cmp = hashcmp(l->sha1, sha1); + if (cmp > 0) /* not in list, since sorted */ + return prev; + if(!cmp) { /* found */ + if (prev == NULL) { + if (hint != NULL && hint != list->front) { + /* we don't know the previous element */ + hint = NULL; + goto redo_from_start; + } + list->front = l->next; + } else + prev->next = l->next; + if (l == list->back) + list->back = prev; + llist_item_put(l); + list->size--; + return prev; + } + prev = l; + l = l->next; + } + return prev; +} + +/* computes A\B */ +static void llist_sorted_difference_inplace(struct llist *A, + struct llist *B) +{ + struct llist_item *hint, *b; + + hint = NULL; + b = B->front; + + while (b) { + hint = llist_sorted_remove(A, b->sha1, hint); + b = b->next; + } +} + +static inline struct pack_list * pack_list_insert(struct pack_list **pl, + struct pack_list *entry) +{ + struct pack_list *p = xmalloc(sizeof(struct pack_list)); + memcpy(p, entry, sizeof(struct pack_list)); + p->next = *pl; + *pl = p; + return p; +} + +static inline size_t pack_list_size(struct pack_list *pl) +{ + size_t ret = 0; + while(pl) { + ret++; + pl = pl->next; + } + return ret; +} + +static struct pack_list * pack_list_difference(const struct pack_list *A, + const struct pack_list *B) +{ + struct pack_list *ret; + const struct pack_list *pl; + + if (A == NULL) + return NULL; + + pl = B; + while (pl != NULL) { + if (A->pack == pl->pack) + return pack_list_difference(A->next, B); + pl = pl->next; + } + ret = xmalloc(sizeof(struct pack_list)); + memcpy(ret, A, sizeof(struct pack_list)); + ret->next = pack_list_difference(A->next, B); + return ret; +} + +static void cmp_two_packs(struct pack_list *p1, struct pack_list *p2) +{ + int p1_off, p2_off; + unsigned char *p1_base, *p2_base; + struct llist_item *p1_hint = NULL, *p2_hint = NULL; + + p1_off = p2_off = 256 * 4 + 4; + p1_base = (unsigned char *) p1->pack->index_base; + p2_base = (unsigned char *) p2->pack->index_base; + + while (p1_off <= p1->pack->index_size - 3 * 20 && + p2_off <= p2->pack->index_size - 3 * 20) + { + int cmp = hashcmp(p1_base + p1_off, p2_base + p2_off); + /* cmp ~ p1 - p2 */ + if (cmp == 0) { + p1_hint = llist_sorted_remove(p1->unique_objects, + p1_base + p1_off, p1_hint); + p2_hint = llist_sorted_remove(p2->unique_objects, + p1_base + p1_off, p2_hint); + p1_off+=24; + p2_off+=24; + continue; + } + if (cmp < 0) { /* p1 has the object, p2 doesn't */ + p1_off+=24; + } else { /* p2 has the object, p1 doesn't */ + p2_off+=24; + } + } +} + +static void pll_free(struct pll *l) +{ + struct pll *old; + struct pack_list *opl; + + while (l) { + old = l; + while (l->pl) { + opl = l->pl; + l->pl = opl->next; + free(opl); + } + l = l->next; + free(old); + } +} + +/* all the permutations have to be free()d at the same time, + * since they refer to each other + */ +static struct pll * get_permutations(struct pack_list *list, int n) +{ + struct pll *subset, *ret = NULL, *new_pll = NULL, *pll; + + if (list == NULL || pack_list_size(list) < n || n == 0) + return NULL; + + if (n == 1) { + while (list) { + new_pll = xmalloc(sizeof(pll)); + new_pll->pl = NULL; + pack_list_insert(&new_pll->pl, list); + new_pll->next = ret; + ret = new_pll; + list = list->next; + } + return ret; + } + + while (list->next) { + subset = get_permutations(list->next, n - 1); + while (subset) { + new_pll = xmalloc(sizeof(pll)); + new_pll->pl = subset->pl; + pack_list_insert(&new_pll->pl, list); + new_pll->next = ret; + ret = new_pll; + subset = subset->next; + } + list = list->next; + } + return ret; +} + +static int is_superset(struct pack_list *pl, struct llist *list) +{ + struct llist *diff; + + diff = llist_copy(list); + + while (pl) { + llist_sorted_difference_inplace(diff, pl->all_objects); + if (diff->size == 0) { /* we're done */ + llist_free(diff); + return 1; + } + pl = pl->next; + } + llist_free(diff); + return 0; +} + +static size_t sizeof_union(struct packed_git *p1, struct packed_git *p2) +{ + size_t ret = 0; + int p1_off, p2_off; + unsigned char *p1_base, *p2_base; + + p1_off = p2_off = 256 * 4 + 4; + p1_base = (unsigned char *)p1->index_base; + p2_base = (unsigned char *)p2->index_base; + + while (p1_off <= p1->index_size - 3 * 20 && + p2_off <= p2->index_size - 3 * 20) + { + int cmp = hashcmp(p1_base + p1_off, p2_base + p2_off); + /* cmp ~ p1 - p2 */ + if (cmp == 0) { + ret++; + p1_off+=24; + p2_off+=24; + continue; + } + if (cmp < 0) { /* p1 has the object, p2 doesn't */ + p1_off+=24; + } else { /* p2 has the object, p1 doesn't */ + p2_off+=24; + } + } + return ret; +} + +/* another O(n^2) function ... */ +static size_t get_pack_redundancy(struct pack_list *pl) +{ + struct pack_list *subset; + size_t ret = 0; + + if (pl == NULL) + return 0; + + while ((subset = pl->next)) { + while(subset) { + ret += sizeof_union(pl->pack, subset->pack); + subset = subset->next; + } + pl = pl->next; + } + return ret; +} + +static inline size_t pack_set_bytecount(struct pack_list *pl) +{ + size_t ret = 0; + while (pl) { + ret += pl->pack->pack_size; + ret += pl->pack->index_size; + pl = pl->next; + } + return ret; +} + +static void minimize(struct pack_list **min) +{ + struct pack_list *pl, *unique = NULL, + *non_unique = NULL, *min_perm = NULL; + struct pll *perm, *perm_all, *perm_ok = NULL, *new_perm; + struct llist *missing; + size_t min_perm_size = (size_t)-1, perm_size; + int n; + + pl = local_packs; + while (pl) { + if(pl->unique_objects->size) + pack_list_insert(&unique, pl); + else + pack_list_insert(&non_unique, pl); + pl = pl->next; + } + /* find out which objects are missing from the set of unique packs */ + missing = llist_copy(all_objects); + pl = unique; + while (pl) { + llist_sorted_difference_inplace(missing, pl->all_objects); + pl = pl->next; + } + + /* return if there are no objects missing from the unique set */ + if (missing->size == 0) { + *min = unique; + return; + } + + /* find the permutations which contain all missing objects */ + for (n = 1; n <= pack_list_size(non_unique) && !perm_ok; n++) { + perm_all = perm = get_permutations(non_unique, n); + while (perm) { + if (is_superset(perm->pl, missing)) { + new_perm = xmalloc(sizeof(struct pll)); + memcpy(new_perm, perm, sizeof(struct pll)); + new_perm->next = perm_ok; + perm_ok = new_perm; + } + perm = perm->next; + } + if (perm_ok) + break; + pll_free(perm_all); + } + if (perm_ok == NULL) + die("Internal error: No complete sets found!\n"); + + /* find the permutation with the smallest size */ + perm = perm_ok; + while (perm) { + perm_size = pack_set_bytecount(perm->pl); + if (min_perm_size > perm_size) { + min_perm_size = perm_size; + min_perm = perm->pl; + } + perm = perm->next; + } + *min = min_perm; + /* add the unique packs to the list */ + pl = unique; + while(pl) { + pack_list_insert(min, pl); + pl = pl->next; + } +} + +static void load_all_objects(void) +{ + struct pack_list *pl = local_packs; + struct llist_item *hint, *l; + + llist_init(&all_objects); + + while (pl) { + hint = NULL; + l = pl->all_objects->front; + while (l) { + hint = llist_insert_sorted_unique(all_objects, + l->sha1, hint); + l = l->next; + } + pl = pl->next; + } + /* remove objects present in remote packs */ + pl = altodb_packs; + while (pl) { + llist_sorted_difference_inplace(all_objects, pl->all_objects); + pl = pl->next; + } +} + +/* this scales like O(n^2) */ +static void cmp_local_packs(void) +{ + struct pack_list *subset, *pl = local_packs; + + while ((subset = pl)) { + while((subset = subset->next)) + cmp_two_packs(pl, subset); + pl = pl->next; + } +} + +static void scan_alt_odb_packs(void) +{ + struct pack_list *local, *alt; + + alt = altodb_packs; + while (alt) { + local = local_packs; + while (local) { + llist_sorted_difference_inplace(local->unique_objects, + alt->all_objects); + local = local->next; + } + llist_sorted_difference_inplace(all_objects, alt->all_objects); + alt = alt->next; + } +} + +static struct pack_list * add_pack(struct packed_git *p) +{ + struct pack_list l; + size_t off; + unsigned char *base; + + if (!p->pack_local && !(alt_odb || verbose)) + return NULL; + + l.pack = p; + llist_init(&l.all_objects); + + off = 256 * 4 + 4; + base = (unsigned char *)p->index_base; + while (off <= p->index_size - 3 * 20) { + llist_insert_back(l.all_objects, base + off); + off += 24; + } + /* this list will be pruned in cmp_two_packs later */ + l.unique_objects = llist_copy(l.all_objects); + if (p->pack_local) + return pack_list_insert(&local_packs, &l); + else + return pack_list_insert(&altodb_packs, &l); +} + +static struct pack_list * add_pack_file(char *filename) +{ + struct packed_git *p = packed_git; + + if (strlen(filename) < 40) + die("Bad pack filename: %s\n", filename); + + while (p) { + if (strstr(p->pack_name, filename)) + return add_pack(p); + p = p->next; + } + die("Filename %s not found in packed_git\n", filename); +} + +static void load_all(void) +{ + struct packed_git *p = packed_git; + + while (p) { + add_pack(p); + p = p->next; + } +} + +int main(int argc, char **argv) +{ + int i; + struct pack_list *min, *red, *pl; + struct llist *ignore; + unsigned char *sha1; + char buf[42]; /* 40 byte sha1 + \n + \0 */ + + setup_git_directory(); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if(!strcmp(arg, "--")) { + i++; + break; + } + if(!strcmp(arg, "--all")) { + load_all_packs = 1; + continue; + } + if(!strcmp(arg, "--verbose")) { + verbose = 1; + continue; + } + if(!strcmp(arg, "--alt-odb")) { + alt_odb = 1; + continue; + } + if(*arg == '-') + usage(pack_redundant_usage); + else + break; + } + + prepare_packed_git(); + + if (load_all_packs) + load_all(); + else + while (*(argv + i) != NULL) + add_pack_file(*(argv + i++)); + + if (local_packs == NULL) + die("Zero packs found!\n"); + + load_all_objects(); + + cmp_local_packs(); + if (alt_odb) + scan_alt_odb_packs(); + + /* ignore objects given on stdin */ + llist_init(&ignore); + if (!isatty(0)) { + while (fgets(buf, sizeof(buf), stdin)) { + sha1 = xmalloc(20); + if (get_sha1_hex(buf, sha1)) + die("Bad sha1 on stdin: %s", buf); + llist_insert_sorted_unique(ignore, sha1, NULL); + } + } + llist_sorted_difference_inplace(all_objects, ignore); + pl = local_packs; + while (pl) { + llist_sorted_difference_inplace(pl->unique_objects, ignore); + pl = pl->next; + } + + minimize(&min); + + if (verbose) { + fprintf(stderr, "There are %lu packs available in alt-odbs.\n", + (unsigned long)pack_list_size(altodb_packs)); + fprintf(stderr, "The smallest (bytewise) set of packs is:\n"); + pl = min; + while (pl) { + fprintf(stderr, "\t%s\n", pl->pack->pack_name); + pl = pl->next; + } + fprintf(stderr, "containing %lu duplicate objects " + "with a total size of %lukb.\n", + (unsigned long)get_pack_redundancy(min), + (unsigned long)pack_set_bytecount(min)/1024); + fprintf(stderr, "A total of %lu unique objects were considered.\n", + (unsigned long)all_objects->size); + fprintf(stderr, "Redundant packs (with indexes):\n"); + } + pl = red = pack_list_difference(local_packs, min); + while (pl) { + printf("%s\n%s\n", + sha1_pack_index_name(pl->pack->sha1), + pl->pack->pack_name); + pl = pl->next; + } + if (verbose) + fprintf(stderr, "%luMB of redundant packs in total.\n", + (unsigned long)pack_set_bytecount(red)/(1024*1024)); + + return 0; +} diff --git a/pack.h b/pack.h new file mode 100644 index 0000000000..deb427edbe --- /dev/null +++ b/pack.h @@ -0,0 +1,52 @@ +#ifndef PACK_H +#define PACK_H + +#include "object.h" + +/* + * Packed object header + */ +#define PACK_SIGNATURE 0x5041434b /* "PACK" */ +#define PACK_VERSION 2 +#define pack_version_ok(v) ((v) == htonl(2) || (v) == htonl(3)) +struct pack_header { + uint32_t hdr_signature; + uint32_t hdr_version; + uint32_t hdr_entries; +}; + +/* + * Packed object index header + * + * struct pack_idx_header { + * uint32_t idx_signature; + * uint32_t idx_version; + * }; + * + * Note: this header isn't active yet. In future versions of git + * we may change the index file format. At that time we would start + * the first four bytes of the new index format with this signature, + * as all older git binaries would find this value illegal and abort + * reading the file. + * + * This is the case because the number of objects in a packfile + * cannot exceed 1,431,660,000 as every object would need at least + * 3 bytes of data and the overall packfile cannot exceed 4 GiB due + * to the 32 bit offsets used by the index. Clearly the signature + * exceeds this maximum. + * + * Very old git binaries will also compare the first 4 bytes to the + * next 4 bytes in the index and abort with a "non-monotonic index" + * error if the second 4 byte word is smaller than the first 4 + * byte word. This would be true in the proposed future index + * format as idx_signature would be greater than idx_version. + */ +#define PACK_IDX_SIGNATURE 0xff744f63 /* "\377tOc" */ + +extern int verify_pack(struct packed_git *, int); + +#define PH_ERROR_EOF (-1) +#define PH_ERROR_PACK_SIGNATURE (-2) +#define PH_ERROR_PROTOCOL (-3) +extern int read_pack_header(int fd, struct pack_header *); +#endif diff --git a/pager.c b/pager.c new file mode 100644 index 0000000000..5f280ab527 --- /dev/null +++ b/pager.c @@ -0,0 +1,69 @@ +#include "cache.h" + +#include <sys/select.h> + +/* + * This is split up from the rest of git so that we might do + * something different on Windows, for example. + */ + +static void run_pager(const char *pager) +{ + /* + * Work around bug in "less" by not starting it until we + * have real input + */ + fd_set in; + + FD_ZERO(&in); + FD_SET(0, &in); + select(1, &in, NULL, &in, NULL); + + execlp(pager, pager, NULL); + execl("/bin/sh", "sh", "-c", pager, NULL); +} + +void setup_pager(void) +{ + pid_t pid; + int fd[2]; + const char *pager = getenv("GIT_PAGER"); + + if (!isatty(1)) + return; + if (!pager) + pager = getenv("PAGER"); + if (!pager) + pager = "less"; + else if (!*pager || !strcmp(pager, "cat")) + return; + + pager_in_use = 1; /* means we are emitting to terminal */ + + if (pipe(fd) < 0) + return; + pid = fork(); + if (pid < 0) { + close(fd[0]); + close(fd[1]); + return; + } + + /* return in the child */ + if (!pid) { + dup2(fd[1], 1); + close(fd[0]); + close(fd[1]); + return; + } + + /* The original process turns into the PAGER */ + dup2(fd[0], 0); + close(fd[0]); + close(fd[1]); + + setenv("LESS", "FRSX", 0); + run_pager(pager); + die("unable to execute pager '%s'", pager); + exit(255); +} diff --git a/patch-delta.c b/patch-delta.c new file mode 100644 index 0000000000..ed9db81fa8 --- /dev/null +++ b/patch-delta.c @@ -0,0 +1,87 @@ +/* + * patch-delta.c: + * recreate a buffer from a source and the delta produced by diff-delta.c + * + * (C) 2005 Nicolas Pitre <nico@cam.org> + * + * This code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include "git-compat-util.h" +#include "delta.h" + +void *patch_delta(const void *src_buf, unsigned long src_size, + const void *delta_buf, unsigned long delta_size, + unsigned long *dst_size) +{ + const unsigned char *data, *top; + unsigned char *dst_buf, *out, cmd; + unsigned long size; + + if (delta_size < DELTA_SIZE_MIN) + return NULL; + + data = delta_buf; + top = (const unsigned char *) delta_buf + delta_size; + + /* make sure the orig file size matches what we expect */ + size = get_delta_hdr_size(&data, top); + if (size != src_size) + return NULL; + + /* now the result size */ + size = get_delta_hdr_size(&data, top); + dst_buf = xmalloc(size + 1); + dst_buf[size] = 0; + + out = dst_buf; + while (data < top) { + cmd = *data++; + if (cmd & 0x80) { + unsigned long cp_off = 0, cp_size = 0; + if (cmd & 0x01) cp_off = *data++; + if (cmd & 0x02) cp_off |= (*data++ << 8); + if (cmd & 0x04) cp_off |= (*data++ << 16); + if (cmd & 0x08) cp_off |= (*data++ << 24); + if (cmd & 0x10) cp_size = *data++; + if (cmd & 0x20) cp_size |= (*data++ << 8); + if (cmd & 0x40) cp_size |= (*data++ << 16); + if (cp_size == 0) cp_size = 0x10000; + if (cp_off + cp_size < cp_size || + cp_off + cp_size > src_size || + cp_size > size) + break; + memcpy(out, (char *) src_buf + cp_off, cp_size); + out += cp_size; + size -= cp_size; + } else if (cmd) { + if (cmd > size) + break; + memcpy(out, data, cmd); + out += cmd; + data += cmd; + size -= cmd; + } else { + /* + * cmd == 0 is reserved for future encoding + * extensions. In the mean time we must fail when + * encountering them (might be data corruption). + */ + error("unexpected delta opcode 0"); + goto bad; + } + } + + /* sanity check */ + if (data != top || size != 0) { + error("delta replay has gone wild"); + bad: + free(dst_buf); + return NULL; + } + + *dst_size = out - dst_buf; + return dst_buf; +} diff --git a/patch-id.c b/patch-id.c new file mode 100644 index 0000000000..086d2d9c68 --- /dev/null +++ b/patch-id.c @@ -0,0 +1,84 @@ +#include "cache.h" + +static void flush_current_id(int patchlen, unsigned char *id, SHA_CTX *c) +{ + unsigned char result[20]; + char name[50]; + + if (!patchlen) + return; + + SHA1_Final(result, c); + memcpy(name, sha1_to_hex(id), 41); + printf("%s %s\n", sha1_to_hex(result), name); + SHA1_Init(c); +} + +static int remove_space(char *line) +{ + char *src = line; + char *dst = line; + unsigned char c; + + while ((c = *src++) != '\0') { + if (!isspace(c)) + *dst++ = c; + } + return dst - line; +} + +static void generate_id_list(void) +{ + static unsigned char sha1[20]; + static char line[1000]; + SHA_CTX ctx; + int patchlen = 0; + + SHA1_Init(&ctx); + while (fgets(line, sizeof(line), stdin) != NULL) { + unsigned char n[20]; + char *p = line; + int len; + + if (!memcmp(line, "diff-tree ", 10)) + p += 10; + else if (!memcmp(line, "commit ", 7)) + p += 7; + + if (!get_sha1_hex(p, n)) { + flush_current_id(patchlen, sha1, &ctx); + hashcpy(sha1, n); + patchlen = 0; + continue; + } + + /* Ignore commit comments */ + if (!patchlen && memcmp(line, "diff ", 5)) + continue; + + /* Ignore git-diff index header */ + if (!memcmp(line, "index ", 6)) + continue; + + /* Ignore line numbers when computing the SHA1 of the patch */ + if (!memcmp(line, "@@ -", 4)) + continue; + + /* Compute the sha without whitespace */ + len = remove_space(line); + patchlen += len; + SHA1_Update(&ctx, line, len); + } + flush_current_id(patchlen, sha1, &ctx); +} + +static const char patch_id_usage[] = "git-patch-id < patch"; + +int main(int argc, char **argv) +{ + if (argc != 1) + usage(patch_id_usage); + + generate_id_list(); + return 0; +} diff --git a/path-list.c b/path-list.c new file mode 100644 index 0000000000..caaa5cc57b --- /dev/null +++ b/path-list.c @@ -0,0 +1,103 @@ +#include "cache.h" +#include "path-list.h" + +/* if there is no exact match, point to the index where the entry could be + * inserted */ +static int get_entry_index(const struct path_list *list, const char *path, + int *exact_match) +{ + int left = -1, right = list->nr; + + while (left + 1 < right) { + int middle = (left + right) / 2; + int compare = strcmp(path, list->items[middle].path); + if (compare < 0) + right = middle; + else if (compare > 0) + left = middle; + else { + *exact_match = 1; + return middle; + } + } + + *exact_match = 0; + return right; +} + +/* returns -1-index if already exists */ +static int add_entry(struct path_list *list, const char *path) +{ + int exact_match; + int index = get_entry_index(list, path, &exact_match); + + if (exact_match) + return -1 - index; + + if (list->nr + 1 >= list->alloc) { + list->alloc += 32; + list->items = xrealloc(list->items, list->alloc + * sizeof(struct path_list_item)); + } + if (index < list->nr) + memmove(list->items + index + 1, list->items + index, + (list->nr - index) + * sizeof(struct path_list_item)); + list->items[index].path = list->strdup_paths ? + xstrdup(path) : (char *)path; + list->items[index].util = NULL; + list->nr++; + + return index; +} + +struct path_list_item *path_list_insert(const char *path, struct path_list *list) +{ + int index = add_entry(list, path); + + if (index < 0) + index = -1 - index; + + return list->items + index; +} + +int path_list_has_path(const struct path_list *list, const char *path) +{ + int exact_match; + get_entry_index(list, path, &exact_match); + return exact_match; +} + +struct path_list_item *path_list_lookup(const char *path, struct path_list *list) +{ + int exact_match, i = get_entry_index(list, path, &exact_match); + if (!exact_match) + return NULL; + return list->items + i; +} + +void path_list_clear(struct path_list *list, int free_items) +{ + if (list->items) { + int i; + if (free_items) + for (i = 0; i < list->nr; i++) { + if (list->strdup_paths) + free(list->items[i].path); + free(list->items[i].util); + } + free(list->items); + } + list->items = NULL; + list->nr = list->alloc = 0; +} + +void print_path_list(const char *text, const struct path_list *p) +{ + int i; + if ( text ) + printf("%s\n", text); + for (i = 0; i < p->nr; i++) + printf("%s:%p\n", p->items[i].path, p->items[i].util); +} + diff --git a/path-list.h b/path-list.h new file mode 100644 index 0000000000..d6401eaa35 --- /dev/null +++ b/path-list.h @@ -0,0 +1,22 @@ +#ifndef _PATH_LIST_H_ +#define _PATH_LIST_H_ + +struct path_list_item { + char *path; + void *util; +}; +struct path_list +{ + struct path_list_item *items; + unsigned int nr, alloc; + unsigned int strdup_paths:1; +}; + +void print_path_list(const char *text, const struct path_list *p); + +int path_list_has_path(const struct path_list *list, const char *path); +void path_list_clear(struct path_list *list, int free_items); +struct path_list_item *path_list_insert(const char *path, struct path_list *list); +struct path_list_item *path_list_lookup(const char *path, struct path_list *list); + +#endif /* _PATH_LIST_H_ */ diff --git a/path.c b/path.c new file mode 100644 index 0000000000..c5d25a4b90 --- /dev/null +++ b/path.c @@ -0,0 +1,294 @@ +/* + * I'm tired of doing "vsnprintf()" etc just to open a + * file, so here's a "return static buffer with printf" + * interface for paths. + * + * It's obviously not thread-safe. Sue me. But it's quite + * useful for doing things like + * + * f = open(mkpath("%s/%s.git", base, name), O_RDONLY); + * + * which is what it's designed for. + */ +#include "cache.h" + +static char bad_path[] = "/bad-path/"; + +static char *get_pathname(void) +{ + static char pathname_array[4][PATH_MAX]; + static int index; + return pathname_array[3 & ++index]; +} + +static char *cleanup_path(char *path) +{ + /* Clean it up */ + if (!memcmp(path, "./", 2)) { + path += 2; + while (*path == '/') + path++; + } + return path; +} + +char *mkpath(const char *fmt, ...) +{ + va_list args; + unsigned len; + char *pathname = get_pathname(); + + va_start(args, fmt); + len = vsnprintf(pathname, PATH_MAX, fmt, args); + va_end(args); + if (len >= PATH_MAX) + return bad_path; + return cleanup_path(pathname); +} + +char *git_path(const char *fmt, ...) +{ + const char *git_dir = get_git_dir(); + char *pathname = get_pathname(); + va_list args; + unsigned len; + + len = strlen(git_dir); + if (len > PATH_MAX-100) + return bad_path; + memcpy(pathname, git_dir, len); + if (len && git_dir[len-1] != '/') + pathname[len++] = '/'; + va_start(args, fmt); + len += vsnprintf(pathname + len, PATH_MAX - len, fmt, args); + va_end(args); + if (len >= PATH_MAX) + return bad_path; + return cleanup_path(pathname); +} + + +/* git_mkstemp() - create tmp file honoring TMPDIR variable */ +int git_mkstemp(char *path, size_t len, const char *template) +{ + char *env, *pch = path; + + if ((env = getenv("TMPDIR")) == NULL) { + strcpy(pch, "/tmp/"); + len -= 5; + pch += 5; + } else { + size_t n = snprintf(pch, len, "%s/", env); + + len -= n; + pch += n; + } + + strlcpy(pch, template, len); + + return mkstemp(path); +} + + +int validate_headref(const char *path) +{ + struct stat st; + char *buf, buffer[256]; + unsigned char sha1[20]; + int len, fd; + + if (lstat(path, &st) < 0) + return -1; + + /* Make sure it is a "refs/.." symlink */ + if (S_ISLNK(st.st_mode)) { + len = readlink(path, buffer, sizeof(buffer)-1); + if (len >= 5 && !memcmp("refs/", buffer, 5)) + return 0; + return -1; + } + + /* + * Anything else, just open it and try to see if it is a symbolic ref. + */ + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + len = read_in_full(fd, buffer, sizeof(buffer)-1); + close(fd); + + /* + * Is it a symbolic ref? + */ + if (len < 4) + return -1; + if (!memcmp("ref:", buffer, 4)) { + buf = buffer + 4; + len -= 4; + while (len && isspace(*buf)) + buf++, len--; + if (len >= 5 && !memcmp("refs/", buf, 5)) + return 0; + } + + /* + * Is this a detached HEAD? + */ + if (!get_sha1_hex(buffer, sha1)) + return 0; + + return -1; +} + +static char *user_path(char *buf, char *path, int sz) +{ + struct passwd *pw; + char *slash; + int len, baselen; + + if (!path || path[0] != '~') + return NULL; + path++; + slash = strchr(path, '/'); + if (path[0] == '/' || !path[0]) { + pw = getpwuid(getuid()); + } + else { + if (slash) { + *slash = 0; + pw = getpwnam(path); + *slash = '/'; + } + else + pw = getpwnam(path); + } + if (!pw || !pw->pw_dir || sz <= strlen(pw->pw_dir)) + return NULL; + baselen = strlen(pw->pw_dir); + memcpy(buf, pw->pw_dir, baselen); + while ((1 < baselen) && (buf[baselen-1] == '/')) { + buf[baselen-1] = 0; + baselen--; + } + if (slash && slash[1]) { + len = strlen(slash); + if (sz <= baselen + len) + return NULL; + memcpy(buf + baselen, slash, len + 1); + } + return buf; +} + +/* + * First, one directory to try is determined by the following algorithm. + * + * (0) If "strict" is given, the path is used as given and no DWIM is + * done. Otherwise: + * (1) "~/path" to mean path under the running user's home directory; + * (2) "~user/path" to mean path under named user's home directory; + * (3) "relative/path" to mean cwd relative directory; or + * (4) "/absolute/path" to mean absolute directory. + * + * Unless "strict" is given, we try access() for existence of "%s.git/.git", + * "%s/.git", "%s.git", "%s" in this order. The first one that exists is + * what we try. + * + * Second, we try chdir() to that. Upon failure, we return NULL. + * + * Then, we try if the current directory is a valid git repository. + * Upon failure, we return NULL. + * + * If all goes well, we return the directory we used to chdir() (but + * before ~user is expanded), avoiding getcwd() resolving symbolic + * links. User relative paths are also returned as they are given, + * except DWIM suffixing. + */ +char *enter_repo(char *path, int strict) +{ + static char used_path[PATH_MAX]; + static char validated_path[PATH_MAX]; + + if (!path) + return NULL; + + if (!strict) { + static const char *suffix[] = { + ".git/.git", "/.git", ".git", "", NULL, + }; + int len = strlen(path); + int i; + while ((1 < len) && (path[len-1] == '/')) { + path[len-1] = 0; + len--; + } + if (PATH_MAX <= len) + return NULL; + if (path[0] == '~') { + if (!user_path(used_path, path, PATH_MAX)) + return NULL; + strcpy(validated_path, path); + path = used_path; + } + else if (PATH_MAX - 10 < len) + return NULL; + else { + path = strcpy(used_path, path); + strcpy(validated_path, path); + } + len = strlen(path); + for (i = 0; suffix[i]; i++) { + strcpy(path + len, suffix[i]); + if (!access(path, F_OK)) { + strcat(validated_path, suffix[i]); + break; + } + } + if (!suffix[i] || chdir(path)) + return NULL; + path = validated_path; + } + else if (chdir(path)) + return NULL; + + if (access("objects", X_OK) == 0 && access("refs", X_OK) == 0 && + validate_headref("HEAD") == 0) { + putenv("GIT_DIR=."); + check_repository_format(); + return path; + } + + 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 |= (shared_repository == PERM_GROUP + ? S_IRGRP + : (shared_repository == PERM_EVERYBODY + ? (S_IRGRP|S_IROTH) + : 0)); + + if (mode & S_IWUSR) + mode |= S_IWGRP; + + if (mode & S_IXUSR) + mode |= (shared_repository == PERM_GROUP + ? S_IXGRP + : (shared_repository == PERM_EVERYBODY + ? (S_IXGRP|S_IXOTH) + : 0)); + if (S_ISDIR(mode)) + mode |= S_ISGID; + if ((mode & st.st_mode) != mode && chmod(path, mode) < 0) + return -2; + return 0; +} diff --git a/peek-remote.c b/peek-remote.c new file mode 100644 index 0000000000..ef3c76ce52 --- /dev/null +++ b/peek-remote.c @@ -0,0 +1,75 @@ +#include "cache.h" +#include "refs.h" +#include "pkt-line.h" + +static const char peek_remote_usage[] = +"git-peek-remote [--upload-pack=<git-upload-pack>] [<host>:]<directory>"; +static const char *uploadpack = "git-upload-pack"; + +static int peek_remote(int fd[2], unsigned flags) +{ + struct ref *ref; + + get_remote_heads(fd[0], &ref, 0, NULL, flags); + packet_flush(fd[1]); + + while (ref) { + printf("%s %s\n", sha1_to_hex(ref->old_sha1), ref->name); + ref = ref->next; + } + return 0; +} + +int main(int argc, char **argv) +{ + int i, ret; + char *dest = NULL; + int fd[2]; + pid_t pid; + int nongit = 0; + unsigned flags = 0; + + setup_git_directory_gently(&nongit); + + for (i = 1; i < argc; i++) { + char *arg = argv[i]; + + if (*arg == '-') { + if (!strncmp("--upload-pack=", arg, 14)) { + uploadpack = arg + 14; + continue; + } + if (!strncmp("--exec=", arg, 7)) { + uploadpack = arg + 7; + continue; + } + if (!strcmp("--tags", arg)) { + flags |= REF_TAGS; + continue; + } + if (!strcmp("--heads", arg)) { + flags |= REF_HEADS; + continue; + } + if (!strcmp("--refs", arg)) { + flags |= REF_NORMAL; + continue; + } + usage(peek_remote_usage); + } + dest = arg; + break; + } + + if (!dest || i != argc - 1) + usage(peek_remote_usage); + + pid = git_connect(fd, dest, uploadpack); + if (pid < 0) + return 1; + ret = peek_remote(fd, flags); + close(fd[0]); + close(fd[1]); + ret |= finish_connect(pid); + return !!ret; +} diff --git a/perl/.gitignore b/perl/.gitignore new file mode 100644 index 0000000000..98b24772c7 --- /dev/null +++ b/perl/.gitignore @@ -0,0 +1,5 @@ +perl.mak +perl.mak.old +blib +blibdirs +pm_to_blib diff --git a/perl/Git.pm b/perl/Git.pm new file mode 100644 index 0000000000..f2c156cde9 --- /dev/null +++ b/perl/Git.pm @@ -0,0 +1,844 @@ +=head1 NAME + +Git - Perl interface to the Git version control system + +=cut + + +package Git; + +use strict; + + +BEGIN { + +our ($VERSION, @ISA, @EXPORT, @EXPORT_OK); + +# Totally unstable API. +$VERSION = '0.01'; + + +=head1 SYNOPSIS + + use Git; + + my $version = Git::command_oneline('version'); + + git_cmd_try { Git::command_noisy('update-server-info') } + '%s failed w/ code %d'; + + my $repo = Git->repository (Directory => '/srv/git/cogito.git'); + + + my @revs = $repo->command('rev-list', '--since=last monday', '--all'); + + my ($fh, $c) = $repo->command_output_pipe('rev-list', '--since=last monday', '--all'); + my $lastrev = <$fh>; chomp $lastrev; + $repo->command_close_pipe($fh, $c); + + my $lastrev = $repo->command_oneline( [ 'rev-list', '--all' ], + STDERR => 0 ); + +=cut + + +require Exporter; + +@ISA = qw(Exporter); + +@EXPORT = qw(git_cmd_try); + +# Methods which can be called as standalone functions as well: +@EXPORT_OK = qw(command command_oneline command_noisy + command_output_pipe command_input_pipe command_close_pipe + version exec_path hash_object git_cmd_try); + + +=head1 DESCRIPTION + +This module provides Perl scripts easy way to interface the Git version control +system. The modules have an easy and well-tested way to call arbitrary Git +commands; in the future, the interface will also provide specialized methods +for doing easily operations which are not totally trivial to do over +the generic command interface. + +While some commands can be executed outside of any context (e.g. 'version' +or 'init'), most operations require a repository context, which in practice +means getting an instance of the Git object using the repository() constructor. +(In the future, we will also get a new_repository() constructor.) All commands +called as methods of the object are then executed in the context of the +repository. + +Part of the "repository state" is also information about path to the attached +working copy (unless you work with a bare repository). You can also navigate +inside of the working copy using the C<wc_chdir()> method. (Note that +the repository object is self-contained and will not change working directory +of your process.) + +TODO: In the future, we might also do + + my $remoterepo = $repo->remote_repository (Name => 'cogito', Branch => 'master'); + $remoterepo ||= Git->remote_repository ('http://git.or.cz/cogito.git/'); + my @refs = $remoterepo->refs(); + +Currently, the module merely wraps calls to external Git tools. In the future, +it will provide a much faster way to interact with Git by linking directly +to libgit. This should be completely opaque to the user, though (performance +increate nonwithstanding). + +=cut + + +use Carp qw(carp croak); # but croak is bad - throw instead +use Error qw(:try); +use Cwd qw(abs_path); + +} + + +=head1 CONSTRUCTORS + +=over 4 + +=item repository ( OPTIONS ) + +=item repository ( DIRECTORY ) + +=item repository () + +Construct a new repository object. +C<OPTIONS> are passed in a hash like fashion, using key and value pairs. +Possible options are: + +B<Repository> - Path to the Git repository. + +B<WorkingCopy> - Path to the associated working copy; not strictly required +as many commands will happily crunch on a bare repository. + +B<WorkingSubdir> - Subdirectory in the working copy to work inside. +Just left undefined if you do not want to limit the scope of operations. + +B<Directory> - Path to the Git working directory in its usual setup. +The C<.git> directory is searched in the directory and all the parent +directories; if found, C<WorkingCopy> is set to the directory containing +it and C<Repository> to the C<.git> directory itself. If no C<.git> +directory was found, the C<Directory> is assumed to be a bare repository, +C<Repository> is set to point at it and C<WorkingCopy> is left undefined. +If the C<$GIT_DIR> environment variable is set, things behave as expected +as well. + +You should not use both C<Directory> and either of C<Repository> and +C<WorkingCopy> - the results of that are undefined. + +Alternatively, a directory path may be passed as a single scalar argument +to the constructor; it is equivalent to setting only the C<Directory> option +field. + +Calling the constructor with no options whatsoever is equivalent to +calling it with C<< Directory => '.' >>. In general, if you are building +a standard porcelain command, simply doing C<< Git->repository() >> should +do the right thing and setup the object to reflect exactly where the user +is right now. + +=cut + +sub repository { + my $class = shift; + my @args = @_; + my %opts = (); + my $self; + + if (defined $args[0]) { + if ($#args % 2 != 1) { + # Not a hash. + $#args == 0 or throw Error::Simple("bad usage"); + %opts = ( Directory => $args[0] ); + } else { + %opts = @args; + } + } + + if (not defined $opts{Repository} and not defined $opts{WorkingCopy}) { + $opts{Directory} ||= '.'; + } + + if ($opts{Directory}) { + -d $opts{Directory} or throw Error::Simple("Directory not found: $!"); + + my $search = Git->repository(WorkingCopy => $opts{Directory}); + my $dir; + try { + $dir = $search->command_oneline(['rev-parse', '--git-dir'], + STDERR => 0); + } catch Git::Error::Command with { + $dir = undef; + }; + + if ($dir) { + $dir =~ m#^/# or $dir = $opts{Directory} . '/' . $dir; + $opts{Repository} = $dir; + + # If --git-dir went ok, this shouldn't die either. + my $prefix = $search->command_oneline('rev-parse', '--show-prefix'); + $dir = abs_path($opts{Directory}) . '/'; + if ($prefix) { + if (substr($dir, -length($prefix)) ne $prefix) { + throw Error::Simple("rev-parse confused me - $dir does not have trailing $prefix"); + } + substr($dir, -length($prefix)) = ''; + } + $opts{WorkingCopy} = $dir; + $opts{WorkingSubdir} = $prefix; + + } else { + # A bare repository? Let's see... + $dir = $opts{Directory}; + + unless (-d "$dir/refs" and -d "$dir/objects" and -e "$dir/HEAD") { + # Mimick git-rev-parse --git-dir error message: + throw Error::Simple('fatal: Not a git repository'); + } + my $search = Git->repository(Repository => $dir); + try { + $search->command('symbolic-ref', 'HEAD'); + } catch Git::Error::Command with { + # Mimick git-rev-parse --git-dir error message: + throw Error::Simple('fatal: Not a git repository'); + } + + $opts{Repository} = abs_path($dir); + } + + delete $opts{Directory}; + } + + $self = { opts => \%opts }; + bless $self, $class; +} + + +=back + +=head1 METHODS + +=over 4 + +=item command ( COMMAND [, ARGUMENTS... ] ) + +=item command ( [ COMMAND, ARGUMENTS... ], { Opt => Val ... } ) + +Execute the given Git C<COMMAND> (specify it without the 'git-' +prefix), optionally with the specified extra C<ARGUMENTS>. + +The second more elaborate form can be used if you want to further adjust +the command execution. Currently, only one option is supported: + +B<STDERR> - How to deal with the command's error output. By default (C<undef>) +it is delivered to the caller's C<STDERR>. A false value (0 or '') will cause +it to be thrown away. If you want to process it, you can get it in a filehandle +you specify, but you must be extremely careful; if the error output is not +very short and you want to read it in the same process as where you called +C<command()>, you are set up for a nice deadlock! + +The method can be called without any instance or on a specified Git repository +(in that case the command will be run in the repository context). + +In scalar context, it returns all the command output in a single string +(verbatim). + +In array context, it returns an array containing lines printed to the +command's stdout (without trailing newlines). + +In both cases, the command's stdin and stderr are the same as the caller's. + +=cut + +sub command { + my ($fh, $ctx) = command_output_pipe(@_); + + if (not defined wantarray) { + # Nothing to pepper the possible exception with. + _cmd_close($fh, $ctx); + + } elsif (not wantarray) { + local $/; + my $text = <$fh>; + try { + _cmd_close($fh, $ctx); + } catch Git::Error::Command with { + # Pepper with the output: + my $E = shift; + $E->{'-outputref'} = \$text; + throw $E; + }; + return $text; + + } else { + my @lines = <$fh>; + defined and chomp for @lines; + try { + _cmd_close($fh, $ctx); + } catch Git::Error::Command with { + my $E = shift; + $E->{'-outputref'} = \@lines; + throw $E; + }; + return @lines; + } +} + + +=item command_oneline ( COMMAND [, ARGUMENTS... ] ) + +=item command_oneline ( [ COMMAND, ARGUMENTS... ], { Opt => Val ... } ) + +Execute the given C<COMMAND> in the same way as command() +does but always return a scalar string containing the first line +of the command's standard output. + +=cut + +sub command_oneline { + my ($fh, $ctx) = command_output_pipe(@_); + + my $line = <$fh>; + defined $line and chomp $line; + try { + _cmd_close($fh, $ctx); + } catch Git::Error::Command with { + # Pepper with the output: + my $E = shift; + $E->{'-outputref'} = \$line; + throw $E; + }; + return $line; +} + + +=item command_output_pipe ( COMMAND [, ARGUMENTS... ] ) + +=item command_output_pipe ( [ COMMAND, ARGUMENTS... ], { Opt => Val ... } ) + +Execute the given C<COMMAND> in the same way as command() +does but return a pipe filehandle from which the command output can be +read. + +The function can return C<($pipe, $ctx)> in array context. +See C<command_close_pipe()> for details. + +=cut + +sub command_output_pipe { + _command_common_pipe('-|', @_); +} + + +=item command_input_pipe ( COMMAND [, ARGUMENTS... ] ) + +=item command_input_pipe ( [ COMMAND, ARGUMENTS... ], { Opt => Val ... } ) + +Execute the given C<COMMAND> in the same way as command_output_pipe() +does but return an input pipe filehandle instead; the command output +is not captured. + +The function can return C<($pipe, $ctx)> in array context. +See C<command_close_pipe()> for details. + +=cut + +sub command_input_pipe { + _command_common_pipe('|-', @_); +} + + +=item command_close_pipe ( PIPE [, CTX ] ) + +Close the C<PIPE> as returned from C<command_*_pipe()>, checking +whether the command finished successfully. The optional C<CTX> argument +is required if you want to see the command name in the error message, +and it is the second value returned by C<command_*_pipe()> when +called in array context. The call idiom is: + + my ($fh, $ctx) = $r->command_output_pipe('status'); + while (<$fh>) { ... } + $r->command_close_pipe($fh, $ctx); + +Note that you should not rely on whatever actually is in C<CTX>; +currently it is simply the command name but in future the context might +have more complicated structure. + +=cut + +sub command_close_pipe { + my ($self, $fh, $ctx) = _maybe_self(@_); + $ctx ||= '<unknown>'; + _cmd_close($fh, $ctx); +} + + +=item command_noisy ( COMMAND [, ARGUMENTS... ] ) + +Execute the given C<COMMAND> in the same way as command() does but do not +capture the command output - the standard output is not redirected and goes +to the standard output of the caller application. + +While the method is called command_noisy(), you might want to as well use +it for the most silent Git commands which you know will never pollute your +stdout but you want to avoid the overhead of the pipe setup when calling them. + +The function returns only after the command has finished running. + +=cut + +sub command_noisy { + my ($self, $cmd, @args) = _maybe_self(@_); + _check_valid_cmd($cmd); + + my $pid = fork; + if (not defined $pid) { + throw Error::Simple("fork failed: $!"); + } elsif ($pid == 0) { + _cmd_exec($self, $cmd, @args); + } + if (waitpid($pid, 0) > 0 and $?>>8 != 0) { + throw Git::Error::Command(join(' ', $cmd, @args), $? >> 8); + } +} + + +=item version () + +Return the Git version in use. + +=cut + +sub version { + my $verstr = command_oneline('--version'); + $verstr =~ s/^git version //; + $verstr; +} + + +=item exec_path () + +Return path to the Git sub-command executables (the same as +C<git --exec-path>). Useful mostly only internally. + +=cut + +sub exec_path { command_oneline('--exec-path') } + + +=item repo_path () + +Return path to the git repository. Must be called on a repository instance. + +=cut + +sub repo_path { $_[0]->{opts}->{Repository} } + + +=item wc_path () + +Return path to the working copy. Must be called on a repository instance. + +=cut + +sub wc_path { $_[0]->{opts}->{WorkingCopy} } + + +=item wc_subdir () + +Return path to the subdirectory inside of a working copy. Must be called +on a repository instance. + +=cut + +sub wc_subdir { $_[0]->{opts}->{WorkingSubdir} ||= '' } + + +=item wc_chdir ( SUBDIR ) + +Change the working copy subdirectory to work within. The C<SUBDIR> is +relative to the working copy root directory (not the current subdirectory). +Must be called on a repository instance attached to a working copy +and the directory must exist. + +=cut + +sub wc_chdir { + my ($self, $subdir) = @_; + $self->wc_path() + or throw Error::Simple("bare repository"); + + -d $self->wc_path().'/'.$subdir + or throw Error::Simple("subdir not found: $!"); + # Of course we will not "hold" the subdirectory so anyone + # can delete it now and we will never know. But at least we tried. + + $self->{opts}->{WorkingSubdir} = $subdir; +} + + +=item config ( VARIABLE ) + +Retrieve the configuration C<VARIABLE> in the same manner as C<config> +does. In scalar context requires the variable to be set only one time +(exception is thrown otherwise), in array context returns allows the +variable to be set multiple times and returns all the values. + +Must be called on a repository instance. + +This currently wraps command('config') so it is not so fast. + +=cut + +sub config { + my ($self, $var) = @_; + $self->repo_path() + or throw Error::Simple("not a repository"); + + try { + if (wantarray) { + return $self->command('config', '--get-all', $var); + } else { + return $self->command_oneline('config', '--get', $var); + } + } catch Git::Error::Command with { + my $E = shift; + if ($E->value() == 1) { + # Key not found. + return undef; + } else { + throw $E; + } + }; +} + + +=item ident ( TYPE | IDENTSTR ) + +=item ident_person ( TYPE | IDENTSTR | IDENTARRAY ) + +This suite of functions retrieves and parses ident information, as stored +in the commit and tag objects or produced by C<var GIT_type_IDENT> (thus +C<TYPE> can be either I<author> or I<committer>; case is insignificant). + +The C<ident> method retrieves the ident information from C<git-var> +and either returns it as a scalar string or as an array with the fields parsed. +Alternatively, it can take a prepared ident string (e.g. from the commit +object) and just parse it. + +C<ident_person> returns the person part of the ident - name and email; +it can take the same arguments as C<ident> or the array returned by C<ident>. + +The synopsis is like: + + my ($name, $email, $time_tz) = ident('author'); + "$name <$email>" eq ident_person('author'); + "$name <$email>" eq ident_person($name); + $time_tz =~ /^\d+ [+-]\d{4}$/; + +Both methods must be called on a repository instance. + +=cut + +sub ident { + my ($self, $type) = @_; + my $identstr; + if (lc $type eq lc 'committer' or lc $type eq lc 'author') { + $identstr = $self->command_oneline('var', 'GIT_'.uc($type).'_IDENT'); + } else { + $identstr = $type; + } + if (wantarray) { + return $identstr =~ /^(.*) <(.*)> (\d+ [+-]\d{4})$/; + } else { + return $identstr; + } +} + +sub ident_person { + my ($self, @ident) = @_; + $#ident == 0 and @ident = $self->ident($ident[0]); + return "$ident[0] <$ident[1]>"; +} + + +=item hash_object ( TYPE, FILENAME ) + +Compute the SHA1 object id of the given C<FILENAME> (or data waiting in +C<FILEHANDLE>) considering it is of the C<TYPE> object type (C<blob>, +C<commit>, C<tree>). + +The method can be called without any instance or on a specified Git repository, +it makes zero difference. + +The function returns the SHA1 hash. + +=cut + +# TODO: Support for passing FILEHANDLE instead of FILENAME +sub hash_object { + my ($self, $type, $file) = _maybe_self(@_); + command_oneline('hash-object', '-t', $type, $file); +} + + + +=back + +=head1 ERROR HANDLING + +All functions are supposed to throw Perl exceptions in case of errors. +See the L<Error> module on how to catch those. Most exceptions are mere +L<Error::Simple> instances. + +However, the C<command()>, C<command_oneline()> and C<command_noisy()> +functions suite can throw C<Git::Error::Command> exceptions as well: those are +thrown when the external command returns an error code and contain the error +code as well as access to the captured command's output. The exception class +provides the usual C<stringify> and C<value> (command's exit code) methods and +in addition also a C<cmd_output> method that returns either an array or a +string with the captured command output (depending on the original function +call context; C<command_noisy()> returns C<undef>) and $<cmdline> which +returns the command and its arguments (but without proper quoting). + +Note that the C<command_*_pipe()> functions cannot throw this exception since +it has no idea whether the command failed or not. You will only find out +at the time you C<close> the pipe; if you want to have that automated, +use C<command_close_pipe()>, which can throw the exception. + +=cut + +{ + package Git::Error::Command; + + @Git::Error::Command::ISA = qw(Error); + + sub new { + my $self = shift; + my $cmdline = '' . shift; + my $value = 0 + shift; + my $outputref = shift; + my(@args) = (); + + local $Error::Depth = $Error::Depth + 1; + + push(@args, '-cmdline', $cmdline); + push(@args, '-value', $value); + push(@args, '-outputref', $outputref); + + $self->SUPER::new(-text => 'command returned error', @args); + } + + sub stringify { + my $self = shift; + my $text = $self->SUPER::stringify; + $self->cmdline() . ': ' . $text . ': ' . $self->value() . "\n"; + } + + sub cmdline { + my $self = shift; + $self->{'-cmdline'}; + } + + sub cmd_output { + my $self = shift; + my $ref = $self->{'-outputref'}; + defined $ref or undef; + if (ref $ref eq 'ARRAY') { + return @$ref; + } else { # SCALAR + return $$ref; + } + } +} + +=over 4 + +=item git_cmd_try { CODE } ERRMSG + +This magical statement will automatically catch any C<Git::Error::Command> +exceptions thrown by C<CODE> and make your program die with C<ERRMSG> +on its lips; the message will have %s substituted for the command line +and %d for the exit status. This statement is useful mostly for producing +more user-friendly error messages. + +In case of no exception caught the statement returns C<CODE>'s return value. + +Note that this is the only auto-exported function. + +=cut + +sub git_cmd_try(&$) { + my ($code, $errmsg) = @_; + my @result; + my $err; + my $array = wantarray; + try { + if ($array) { + @result = &$code; + } else { + $result[0] = &$code; + } + } catch Git::Error::Command with { + my $E = shift; + $err = $errmsg; + $err =~ s/\%s/$E->cmdline()/ge; + $err =~ s/\%d/$E->value()/ge; + # We can't croak here since Error.pm would mangle + # that to Error::Simple. + }; + $err and croak $err; + return $array ? @result : $result[0]; +} + + +=back + +=head1 COPYRIGHT + +Copyright 2006 by Petr Baudis E<lt>pasky@suse.czE<gt>. + +This module is free software; it may be used, copied, modified +and distributed under the terms of the GNU General Public Licence, +either version 2, or (at your option) any later version. + +=cut + + +# Take raw method argument list and return ($obj, @args) in case +# the method was called upon an instance and (undef, @args) if +# it was called directly. +sub _maybe_self { + # This breaks inheritance. Oh well. + ref $_[0] eq 'Git' ? @_ : (undef, @_); +} + +# Check if the command id is something reasonable. +sub _check_valid_cmd { + my ($cmd) = @_; + $cmd =~ /^[a-z0-9A-Z_-]+$/ or throw Error::Simple("bad command: $cmd"); +} + +# Common backend for the pipe creators. +sub _command_common_pipe { + my $direction = shift; + my ($self, @p) = _maybe_self(@_); + my (%opts, $cmd, @args); + if (ref $p[0]) { + ($cmd, @args) = @{shift @p}; + %opts = ref $p[0] ? %{$p[0]} : @p; + } else { + ($cmd, @args) = @p; + } + _check_valid_cmd($cmd); + + my $fh; + if ($^O eq 'MSWin32') { + # ActiveState Perl + #defined $opts{STDERR} and + # warn 'ignoring STDERR option - running w/ ActiveState'; + $direction eq '-|' or + die 'input pipe for ActiveState not implemented'; + # the strange construction with *ACPIPE is just to + # explain the tie below that we want to bind to + # a handle class, not scalar. It is not known if + # it is something specific to ActiveState Perl or + # just a Perl quirk. + tie (*ACPIPE, 'Git::activestate_pipe', $cmd, @args); + $fh = *ACPIPE; + + } else { + my $pid = open($fh, $direction); + if (not defined $pid) { + throw Error::Simple("open failed: $!"); + } elsif ($pid == 0) { + if (defined $opts{STDERR}) { + close STDERR; + } + if ($opts{STDERR}) { + open (STDERR, '>&', $opts{STDERR}) + or die "dup failed: $!"; + } + _cmd_exec($self, $cmd, @args); + } + } + return wantarray ? ($fh, join(' ', $cmd, @args)) : $fh; +} + +# When already in the subprocess, set up the appropriate state +# for the given repository and execute the git command. +sub _cmd_exec { + my ($self, @args) = @_; + if ($self) { + $self->repo_path() and $ENV{'GIT_DIR'} = $self->repo_path(); + $self->wc_path() and chdir($self->wc_path()); + $self->wc_subdir() and chdir($self->wc_subdir()); + } + _execv_git_cmd(@args); + die "exec failed: $!"; +} + +# Execute the given Git command ($_[0]) with arguments ($_[1..]) +# by searching for it at proper places. +sub _execv_git_cmd { exec('git', @_); } + +# Close pipe to a subprocess. +sub _cmd_close { + my ($fh, $ctx) = @_; + if (not close $fh) { + if ($!) { + # It's just close, no point in fatalities + carp "error closing pipe: $!"; + } elsif ($? >> 8) { + # The caller should pepper this. + throw Git::Error::Command($ctx, $? >> 8); + } + # else we might e.g. closed a live stream; the command + # dying of SIGPIPE would drive us here. + } +} + + +sub DESTROY { } + + +# Pipe implementation for ActiveState Perl. + +package Git::activestate_pipe; +use strict; + +sub TIEHANDLE { + my ($class, @params) = @_; + # FIXME: This is probably horrible idea and the thing will explode + # at the moment you give it arguments that require some quoting, + # but I have no ActiveState clue... --pasky + # Let's just hope ActiveState Perl does at least the quoting + # correctly. + my @data = qx{git @params}; + bless { i => 0, data => \@data }, $class; +} + +sub READLINE { + my $self = shift; + if ($self->{i} >= scalar @{$self->{data}}) { + return undef; + } + return $self->{'data'}->[ $self->{i}++ ]; +} + +sub CLOSE { + my $self = shift; + delete $self->{data}; + delete $self->{i}; +} + +sub EOF { + my $self = shift; + return ($self->{i} >= scalar @{$self->{data}}); +} + + +1; # Famous last words diff --git a/perl/Makefile b/perl/Makefile new file mode 100644 index 0000000000..099beda873 --- /dev/null +++ b/perl/Makefile @@ -0,0 +1,39 @@ +# +# Makefile for perl support modules and routine +# +makfile:=perl.mak + +PERL_PATH_SQ = $(subst ','\'',$(PERL_PATH)) +prefix_SQ = $(subst ','\'',$(prefix)) + +all install instlibdir: $(makfile) + $(MAKE) -f $(makfile) $@ + +clean: + test -f $(makfile) && $(MAKE) -f $(makfile) $@ || exit 0 + $(RM) ppport.h + $(RM) $(makfile) + $(RM) $(makfile).old + +ifdef NO_PERL_MAKEMAKER +instdir_SQ = $(subst ','\'',$(prefix)/lib) +$(makfile): ../GIT-CFLAGS Makefile + echo all: > $@ + echo ' :' >> $@ + echo install: >> $@ + echo ' mkdir -p $(instdir_SQ)' >> $@ + echo ' $(RM) $(instdir_SQ)/Git.pm; cp Git.pm $(instdir_SQ)' >> $@ + echo ' $(RM) $(instdir_SQ)/Error.pm; \ + cp private-Error.pm $(instdir_SQ)/Error.pm' >> $@ + echo instlibdir: >> $@ + echo ' echo $(instdir_SQ)' >> $@ +else +$(makfile): Makefile.PL ../GIT-CFLAGS + '$(PERL_PATH_SQ)' $< PREFIX='$(prefix_SQ)' +endif + +# this is just added comfort for calling make directly in perl dir +# (even though GIT-CFLAGS aren't used yet. If ever) +../GIT-CFLAGS: + $(MAKE) -C .. GIT-CFLAGS + diff --git a/perl/Makefile.PL b/perl/Makefile.PL new file mode 100644 index 0000000000..9b117fd0d7 --- /dev/null +++ b/perl/Makefile.PL @@ -0,0 +1,33 @@ +use ExtUtils::MakeMaker; + +sub MY::postamble { + return <<'MAKE_FRAG'; +instlibdir: + @echo '$(INSTALLSITELIB)' + +MAKE_FRAG +} + +my %pm = ('Git.pm' => '$(INST_LIBDIR)/Git.pm'); + +# We come with our own bundled Error.pm. It's not in the set of default +# Perl modules so install it if it's not available on the system yet. +eval { require Error }; +if ($@) { + $pm{'private-Error.pm'} = '$(INST_LIBDIR)/Error.pm'; +} + +my %extra; +$extra{DESTDIR} = $ENV{DESTDIR} if $ENV{DESTDIR}; + +# redirect stdout, otherwise the message "Writing perl.mak for Git" +# disrupts the output for the target 'instlibdir' +open STDOUT, ">&STDERR"; + +WriteMakefile( + NAME => 'Git', + VERSION_FROM => 'Git.pm', + PM => \%pm, + MAKEFILE => 'perl.mak', + %extra +); diff --git a/perl/private-Error.pm b/perl/private-Error.pm new file mode 100644 index 0000000000..11e9cd9a02 --- /dev/null +++ b/perl/private-Error.pm @@ -0,0 +1,827 @@ +# Error.pm +# +# Copyright (c) 1997-8 Graham Barr <gbarr@ti.com>. All rights reserved. +# This program is free software; you can redistribute it and/or +# modify it under the same terms as Perl itself. +# +# Based on my original Error.pm, and Exceptions.pm by Peter Seibel +# <peter@weblogic.com> and adapted by Jesse Glick <jglick@sig.bsh.com>. +# +# but modified ***significantly*** + +package Error; + +use strict; +use vars qw($VERSION); +use 5.004; + +$VERSION = "0.15009"; + +use overload ( + '""' => 'stringify', + '0+' => 'value', + 'bool' => sub { return 1; }, + 'fallback' => 1 +); + +$Error::Depth = 0; # Depth to pass to caller() +$Error::Debug = 0; # Generate verbose stack traces +@Error::STACK = (); # Clause stack for try +$Error::THROWN = undef; # last error thrown, a workaround until die $ref works + +my $LAST; # Last error created +my %ERROR; # Last error associated with package + +sub throw_Error_Simple +{ + my $args = shift; + return Error::Simple->new($args->{'text'}); +} + +$Error::ObjectifyCallback = \&throw_Error_Simple; + + +# Exported subs are defined in Error::subs + +sub import { + shift; + local $Exporter::ExportLevel = $Exporter::ExportLevel + 1; + Error::subs->import(@_); +} + +# I really want to use last for the name of this method, but it is a keyword +# which prevent the syntax last Error + +sub prior { + shift; # ignore + + return $LAST unless @_; + + my $pkg = shift; + return exists $ERROR{$pkg} ? $ERROR{$pkg} : undef + unless ref($pkg); + + my $obj = $pkg; + my $err = undef; + if($obj->isa('HASH')) { + $err = $obj->{'__Error__'} + if exists $obj->{'__Error__'}; + } + elsif($obj->isa('GLOB')) { + $err = ${*$obj}{'__Error__'} + if exists ${*$obj}{'__Error__'}; + } + + $err; +} + +sub flush { + shift; #ignore + + unless (@_) { + $LAST = undef; + return; + } + + my $pkg = shift; + return unless ref($pkg); + + undef $ERROR{$pkg} if defined $ERROR{$pkg}; +} + +# Return as much information as possible about where the error +# happened. The -stacktrace element only exists if $Error::DEBUG +# was set when the error was created + +sub stacktrace { + my $self = shift; + + return $self->{'-stacktrace'} + if exists $self->{'-stacktrace'}; + + my $text = exists $self->{'-text'} ? $self->{'-text'} : "Died"; + + $text .= sprintf(" at %s line %d.\n", $self->file, $self->line) + unless($text =~ /\n$/s); + + $text; +} + +# Allow error propagation, ie +# +# $ber->encode(...) or +# return Error->prior($ber)->associate($ldap); + +sub associate { + my $err = shift; + my $obj = shift; + + return unless ref($obj); + + if($obj->isa('HASH')) { + $obj->{'__Error__'} = $err; + } + elsif($obj->isa('GLOB')) { + ${*$obj}{'__Error__'} = $err; + } + $obj = ref($obj); + $ERROR{ ref($obj) } = $err; + + return; +} + +sub new { + my $self = shift; + my($pkg,$file,$line) = caller($Error::Depth); + + my $err = bless { + '-package' => $pkg, + '-file' => $file, + '-line' => $line, + @_ + }, $self; + + $err->associate($err->{'-object'}) + if(exists $err->{'-object'}); + + # To always create a stacktrace would be very inefficient, so + # we only do it if $Error::Debug is set + + if($Error::Debug) { + require Carp; + local $Carp::CarpLevel = $Error::Depth; + my $text = defined($err->{'-text'}) ? $err->{'-text'} : "Error"; + my $trace = Carp::longmess($text); + # Remove try calls from the trace + $trace =~ s/(\n\s+\S+__ANON__[^\n]+)?\n\s+eval[^\n]+\n\s+Error::subs::try[^\n]+(?=\n)//sog; + $trace =~ s/(\n\s+\S+__ANON__[^\n]+)?\n\s+eval[^\n]+\n\s+Error::subs::run_clauses[^\n]+\n\s+Error::subs::try[^\n]+(?=\n)//sog; + $err->{'-stacktrace'} = $trace + } + + $@ = $LAST = $ERROR{$pkg} = $err; +} + +# Throw an error. this contains some very gory code. + +sub throw { + my $self = shift; + local $Error::Depth = $Error::Depth + 1; + + # if we are not rethrow-ing then create the object to throw + $self = $self->new(@_) unless ref($self); + + die $Error::THROWN = $self; +} + +# syntactic sugar for +# +# die with Error( ... ); + +sub with { + my $self = shift; + local $Error::Depth = $Error::Depth + 1; + + $self->new(@_); +} + +# syntactic sugar for +# +# record Error( ... ) and return; + +sub record { + my $self = shift; + local $Error::Depth = $Error::Depth + 1; + + $self->new(@_); +} + +# catch clause for +# +# try { ... } catch CLASS with { ... } + +sub catch { + my $pkg = shift; + my $code = shift; + my $clauses = shift || {}; + my $catch = $clauses->{'catch'} ||= []; + + unshift @$catch, $pkg, $code; + + $clauses; +} + +# Object query methods + +sub object { + my $self = shift; + exists $self->{'-object'} ? $self->{'-object'} : undef; +} + +sub file { + my $self = shift; + exists $self->{'-file'} ? $self->{'-file'} : undef; +} + +sub line { + my $self = shift; + exists $self->{'-line'} ? $self->{'-line'} : undef; +} + +sub text { + my $self = shift; + exists $self->{'-text'} ? $self->{'-text'} : undef; +} + +# overload methods + +sub stringify { + my $self = shift; + defined $self->{'-text'} ? $self->{'-text'} : "Died"; +} + +sub value { + my $self = shift; + exists $self->{'-value'} ? $self->{'-value'} : undef; +} + +package Error::Simple; + +@Error::Simple::ISA = qw(Error); + +sub new { + my $self = shift; + my $text = "" . shift; + my $value = shift; + my(@args) = (); + + local $Error::Depth = $Error::Depth + 1; + + @args = ( -file => $1, -line => $2) + if($text =~ s/\s+at\s+(\S+)\s+line\s+(\d+)(?:,\s*<[^>]*>\s+line\s+\d+)?\.?\n?$//s); + push(@args, '-value', 0 + $value) + if defined($value); + + $self->SUPER::new(-text => $text, @args); +} + +sub stringify { + my $self = shift; + my $text = $self->SUPER::stringify; + $text .= sprintf(" at %s line %d.\n", $self->file, $self->line) + unless($text =~ /\n$/s); + $text; +} + +########################################################################## +########################################################################## + +# Inspired by code from Jesse Glick <jglick@sig.bsh.com> and +# Peter Seibel <peter@weblogic.com> + +package Error::subs; + +use Exporter (); +use vars qw(@EXPORT_OK @ISA %EXPORT_TAGS); + +@EXPORT_OK = qw(try with finally except otherwise); +%EXPORT_TAGS = (try => \@EXPORT_OK); + +@ISA = qw(Exporter); + + +sub blessed { + my $item = shift; + local $@; # don't kill an outer $@ + ref $item and eval { $item->can('can') }; +} + + +sub run_clauses ($$$\@) { + my($clauses,$err,$wantarray,$result) = @_; + my $code = undef; + + $err = $Error::ObjectifyCallback->({'text' =>$err}) unless ref($err); + + CATCH: { + + # catch + my $catch; + if(defined($catch = $clauses->{'catch'})) { + my $i = 0; + + CATCHLOOP: + for( ; $i < @$catch ; $i += 2) { + my $pkg = $catch->[$i]; + unless(defined $pkg) { + #except + splice(@$catch,$i,2,$catch->[$i+1]->()); + $i -= 2; + next CATCHLOOP; + } + elsif(blessed($err) && $err->isa($pkg)) { + $code = $catch->[$i+1]; + while(1) { + my $more = 0; + local($Error::THROWN); + my $ok = eval { + if($wantarray) { + @{$result} = $code->($err,\$more); + } + elsif(defined($wantarray)) { + @{$result} = (); + $result->[0] = $code->($err,\$more); + } + else { + $code->($err,\$more); + } + 1; + }; + if( $ok ) { + next CATCHLOOP if $more; + undef $err; + } + else { + $err = defined($Error::THROWN) + ? $Error::THROWN : $@; + $err = $Error::ObjectifyCallback->({'text' =>$err}) + unless ref($err); + } + last CATCH; + }; + } + } + } + + # otherwise + my $owise; + if(defined($owise = $clauses->{'otherwise'})) { + my $code = $clauses->{'otherwise'}; + my $more = 0; + my $ok = eval { + if($wantarray) { + @{$result} = $code->($err,\$more); + } + elsif(defined($wantarray)) { + @{$result} = (); + $result->[0] = $code->($err,\$more); + } + else { + $code->($err,\$more); + } + 1; + }; + if( $ok ) { + undef $err; + } + else { + $err = defined($Error::THROWN) + ? $Error::THROWN : $@; + + $err = $Error::ObjectifyCallback->({'text' =>$err}) + unless ref($err); + } + } + } + $err; +} + +sub try (&;$) { + my $try = shift; + my $clauses = @_ ? shift : {}; + my $ok = 0; + my $err = undef; + my @result = (); + + unshift @Error::STACK, $clauses; + + my $wantarray = wantarray(); + + do { + local $Error::THROWN = undef; + local $@ = undef; + + $ok = eval { + if($wantarray) { + @result = $try->(); + } + elsif(defined $wantarray) { + $result[0] = $try->(); + } + else { + $try->(); + } + 1; + }; + + $err = defined($Error::THROWN) ? $Error::THROWN : $@ + unless $ok; + }; + + shift @Error::STACK; + + $err = run_clauses($clauses,$err,wantarray,@result) + unless($ok); + + $clauses->{'finally'}->() + if(defined($clauses->{'finally'})); + + if (defined($err)) + { + if (blessed($err) && $err->can('throw')) + { + throw $err; + } + else + { + die $err; + } + } + + wantarray ? @result : $result[0]; +} + +# Each clause adds a sub to the list of clauses. The finally clause is +# always the last, and the otherwise clause is always added just before +# the finally clause. +# +# All clauses, except the finally clause, add a sub which takes one argument +# this argument will be the error being thrown. The sub will return a code ref +# if that clause can handle that error, otherwise undef is returned. +# +# The otherwise clause adds a sub which unconditionally returns the users +# code reference, this is why it is forced to be last. +# +# The catch clause is defined in Error.pm, as the syntax causes it to +# be called as a method + +sub with (&;$) { + @_ +} + +sub finally (&) { + my $code = shift; + my $clauses = { 'finally' => $code }; + $clauses; +} + +# The except clause is a block which returns a hashref or a list of +# key-value pairs, where the keys are the classes and the values are subs. + +sub except (&;$) { + my $code = shift; + my $clauses = shift || {}; + my $catch = $clauses->{'catch'} ||= []; + + my $sub = sub { + my $ref; + my(@array) = $code->($_[0]); + if(@array == 1 && ref($array[0])) { + $ref = $array[0]; + $ref = [ %$ref ] + if(UNIVERSAL::isa($ref,'HASH')); + } + else { + $ref = \@array; + } + @$ref + }; + + unshift @{$catch}, undef, $sub; + + $clauses; +} + +sub otherwise (&;$) { + my $code = shift; + my $clauses = shift || {}; + + if(exists $clauses->{'otherwise'}) { + require Carp; + Carp::croak("Multiple otherwise clauses"); + } + + $clauses->{'otherwise'} = $code; + + $clauses; +} + +1; +__END__ + +=head1 NAME + +Error - Error/exception handling in an OO-ish way + +=head1 SYNOPSIS + + use Error qw(:try); + + throw Error::Simple( "A simple error"); + + sub xyz { + ... + record Error::Simple("A simple error") + and return; + } + + unlink($file) or throw Error::Simple("$file: $!",$!); + + try { + do_some_stuff(); + die "error!" if $condition; + throw Error::Simple -text => "Oops!" if $other_condition; + } + catch Error::IO with { + my $E = shift; + print STDERR "File ", $E->{'-file'}, " had a problem\n"; + } + except { + my $E = shift; + my $general_handler=sub {send_message $E->{-description}}; + return { + UserException1 => $general_handler, + UserException2 => $general_handler + }; + } + otherwise { + print STDERR "Well I don't know what to say\n"; + } + finally { + close_the_garage_door_already(); # Should be reliable + }; # Don't forget the trailing ; or you might be surprised + +=head1 DESCRIPTION + +The C<Error> package provides two interfaces. Firstly C<Error> provides +a procedural interface to exception handling. Secondly C<Error> is a +base class for errors/exceptions that can either be thrown, for +subsequent catch, or can simply be recorded. + +Errors in the class C<Error> should not be thrown directly, but the +user should throw errors from a sub-class of C<Error>. + +=head1 PROCEDURAL INTERFACE + +C<Error> exports subroutines to perform exception handling. These will +be exported if the C<:try> tag is used in the C<use> line. + +=over 4 + +=item try BLOCK CLAUSES + +C<try> is the main subroutine called by the user. All other subroutines +exported are clauses to the try subroutine. + +The BLOCK will be evaluated and, if no error is throw, try will return +the result of the block. + +C<CLAUSES> are the subroutines below, which describe what to do in the +event of an error being thrown within BLOCK. + +=item catch CLASS with BLOCK + +This clauses will cause all errors that satisfy C<$err-E<gt>isa(CLASS)> +to be caught and handled by evaluating C<BLOCK>. + +C<BLOCK> will be passed two arguments. The first will be the error +being thrown. The second is a reference to a scalar variable. If this +variable is set by the catch block then, on return from the catch +block, try will continue processing as if the catch block was never +found. + +To propagate the error the catch block may call C<$err-E<gt>throw> + +If the scalar reference by the second argument is not set, and the +error is not thrown. Then the current try block will return with the +result from the catch block. + +=item except BLOCK + +When C<try> is looking for a handler, if an except clause is found +C<BLOCK> is evaluated. The return value from this block should be a +HASHREF or a list of key-value pairs, where the keys are class names +and the values are CODE references for the handler of errors of that +type. + +=item otherwise BLOCK + +Catch any error by executing the code in C<BLOCK> + +When evaluated C<BLOCK> will be passed one argument, which will be the +error being processed. + +Only one otherwise block may be specified per try block + +=item finally BLOCK + +Execute the code in C<BLOCK> either after the code in the try block has +successfully completed, or if the try block throws an error then +C<BLOCK> will be executed after the handler has completed. + +If the handler throws an error then the error will be caught, the +finally block will be executed and the error will be re-thrown. + +Only one finally block may be specified per try block + +=back + +=head1 CLASS INTERFACE + +=head2 CONSTRUCTORS + +The C<Error> object is implemented as a HASH. This HASH is initialized +with the arguments that are passed to it's constructor. The elements +that are used by, or are retrievable by the C<Error> class are listed +below, other classes may add to these. + + -file + -line + -text + -value + -object + +If C<-file> or C<-line> are not specified in the constructor arguments +then these will be initialized with the file name and line number where +the constructor was called from. + +If the error is associated with an object then the object should be +passed as the C<-object> argument. This will allow the C<Error> package +to associate the error with the object. + +The C<Error> package remembers the last error created, and also the +last error associated with a package. This could either be the last +error created by a sub in that package, or the last error which passed +an object blessed into that package as the C<-object> argument. + +=over 4 + +=item throw ( [ ARGS ] ) + +Create a new C<Error> object and throw an error, which will be caught +by a surrounding C<try> block, if there is one. Otherwise it will cause +the program to exit. + +C<throw> may also be called on an existing error to re-throw it. + +=item with ( [ ARGS ] ) + +Create a new C<Error> object and returns it. This is defined for +syntactic sugar, eg + + die with Some::Error ( ... ); + +=item record ( [ ARGS ] ) + +Create a new C<Error> object and returns it. This is defined for +syntactic sugar, eg + + record Some::Error ( ... ) + and return; + +=back + +=head2 STATIC METHODS + +=over 4 + +=item prior ( [ PACKAGE ] ) + +Return the last error created, or the last error associated with +C<PACKAGE> + +=item flush ( [ PACKAGE ] ) + +Flush the last error created, or the last error associated with +C<PACKAGE>.It is necessary to clear the error stack before exiting the +package or uncaught errors generated using C<record> will be reported. + + $Error->flush; + +=cut + +=back + +=head2 OBJECT METHODS + +=over 4 + +=item stacktrace + +If the variable C<$Error::Debug> was non-zero when the error was +created, then C<stacktrace> returns a string created by calling +C<Carp::longmess>. If the variable was zero the C<stacktrace> returns +the text of the error appended with the filename and line number of +where the error was created, providing the text does not end with a +newline. + +=item object + +The object this error was associated with + +=item file + +The file where the constructor of this error was called from + +=item line + +The line where the constructor of this error was called from + +=item text + +The text of the error + +=back + +=head2 OVERLOAD METHODS + +=over 4 + +=item stringify + +A method that converts the object into a string. This method may simply +return the same as the C<text> method, or it may append more +information. For example the file name and line number. + +By default this method returns the C<-text> argument that was passed to +the constructor, or the string C<"Died"> if none was given. + +=item value + +A method that will return a value that can be associated with the +error. For example if an error was created due to the failure of a +system call, then this may return the numeric value of C<$!> at the +time. + +By default this method returns the C<-value> argument that was passed +to the constructor. + +=back + +=head1 PRE-DEFINED ERROR CLASSES + +=over 4 + +=item Error::Simple + +This class can be used to hold simple error strings and values. It's +constructor takes two arguments. The first is a text value, the second +is a numeric value. These values are what will be returned by the +overload methods. + +If the text value ends with C<at file line 1> as $@ strings do, then +this infomation will be used to set the C<-file> and C<-line> arguments +of the error object. + +This class is used internally if an eval'd block die's with an error +that is a plain string. (Unless C<$Error::ObjectifyCallback> is modified) + +=back + +=head1 $Error::ObjectifyCallback + +This variable holds a reference to a subroutine that converts errors that +are plain strings to objects. It is used by Error.pm to convert textual +errors to objects, and can be overridden by the user. + +It accepts a single argument which is a hash reference to named parameters. +Currently the only named parameter passed is C<'text'> which is the text +of the error, but others may be available in the future. + +For example the following code will cause Error.pm to throw objects of the +class MyError::Bar by default: + + sub throw_MyError_Bar + { + my $args = shift; + my $err = MyError::Bar->new(); + $err->{'MyBarText'} = $args->{'text'}; + return $err; + } + + { + local $Error::ObjectifyCallback = \&throw_MyError_Bar; + + # Error handling here. + } + +=head1 KNOWN BUGS + +None, but that does not mean there are not any. + +=head1 AUTHORS + +Graham Barr <gbarr@pobox.com> + +The code that inspired me to write this was originally written by +Peter Seibel <peter@weblogic.com> and adapted by Jesse Glick +<jglick@sig.bsh.com>. + +=head1 MAINTAINER + +Shlomi Fish <shlomif@iglu.org.il> + +=head1 PAST MAINTAINERS + +Arun Kumar U <u_arunkumar@yahoo.com> + +=cut diff --git a/pkt-line.c b/pkt-line.c new file mode 100644 index 0000000000..b4cb7e2756 --- /dev/null +++ b/pkt-line.c @@ -0,0 +1,114 @@ +#include "cache.h" +#include "pkt-line.h" + +/* + * Write a packetized stream, where each line is preceded by + * its length (including the header) as a 4-byte hex number. + * A length of 'zero' means end of stream (and a length of 1-3 + * would be an error). + * + * This is all pretty stupid, but we use this packetized line + * format to make a streaming format possible without ever + * over-running the read buffers. That way we'll never read + * into what might be the pack data (which should go to another + * process entirely). + * + * The writing side could use stdio, but since the reading + * side can't, we stay with pure read/write interfaces. + */ +ssize_t safe_write(int fd, const void *buf, ssize_t n) +{ + ssize_t nn = n; + while (n) { + int ret = xwrite(fd, buf, n); + if (ret > 0) { + buf = (char *) buf + ret; + n -= ret; + continue; + } + if (!ret) + die("write error (disk full?)"); + die("write error (%s)", strerror(errno)); + } + return nn; +} + +/* + * If we buffered things up above (we don't, but we should), + * we'd flush it here + */ +void packet_flush(int fd) +{ + safe_write(fd, "0000", 4); +} + +#define hex(a) (hexchar[(a) & 15]) +void packet_write(int fd, const char *fmt, ...) +{ + static char buffer[1000]; + static char hexchar[] = "0123456789abcdef"; + va_list args; + unsigned n; + + va_start(args, fmt); + n = vsnprintf(buffer + 4, sizeof(buffer) - 4, fmt, args); + va_end(args); + if (n >= sizeof(buffer)-4) + die("protocol error: impossibly long line"); + n += 4; + buffer[0] = hex(n >> 12); + buffer[1] = hex(n >> 8); + buffer[2] = hex(n >> 4); + buffer[3] = hex(n); + safe_write(fd, buffer, n); +} + +static void safe_read(int fd, void *buffer, unsigned size) +{ + int n = 0; + + while (n < size) { + int ret = xread(fd, (char *) buffer + n, size - n); + if (ret < 0) + die("read error (%s)", strerror(errno)); + if (!ret) + die("The remote end hung up unexpectedly"); + n += ret; + } +} + +int packet_read_line(int fd, char *buffer, unsigned size) +{ + int n; + unsigned len; + char linelen[4]; + + safe_read(fd, linelen, 4); + + len = 0; + for (n = 0; n < 4; n++) { + unsigned char c = linelen[n]; + len <<= 4; + if (c >= '0' && c <= '9') { + len += c - '0'; + continue; + } + if (c >= 'a' && c <= 'f') { + len += c - 'a' + 10; + continue; + } + if (c >= 'A' && c <= 'F') { + len += c - 'A' + 10; + continue; + } + die("protocol error: bad line length character"); + } + if (!len) + return 0; + len -= 4; + if (len >= size) + die("protocol error: bad line length %d", len); + safe_read(fd, buffer, len); + buffer[len] = 0; + return len; +} diff --git a/pkt-line.h b/pkt-line.h new file mode 100644 index 0000000000..9df653f6f5 --- /dev/null +++ b/pkt-line.h @@ -0,0 +1,15 @@ +#ifndef PKTLINE_H +#define PKTLINE_H + +#include "git-compat-util.h" + +/* + * Silly packetized line writing interface + */ +void packet_flush(int fd); +void packet_write(int fd, const char *fmt, ...) __attribute__((format (printf, 2, 3))); + +int packet_read_line(int fd, char *buffer, unsigned size); +ssize_t safe_write(int, const void *, ssize_t); + +#endif diff --git a/ppc/sha1.c b/ppc/sha1.c new file mode 100644 index 0000000000..0820398b00 --- /dev/null +++ b/ppc/sha1.c @@ -0,0 +1,72 @@ +/* + * SHA-1 implementation. + * + * Copyright (C) 2005 Paul Mackerras <paulus@samba.org> + * + * This version assumes we are running on a big-endian machine. + * It calls an external sha1_core() to process blocks of 64 bytes. + */ +#include <stdio.h> +#include <string.h> +#include "sha1.h" + +extern void sha1_core(uint32_t *hash, const unsigned char *p, + unsigned int nblocks); + +int SHA1_Init(SHA_CTX *c) +{ + c->hash[0] = 0x67452301; + c->hash[1] = 0xEFCDAB89; + c->hash[2] = 0x98BADCFE; + c->hash[3] = 0x10325476; + c->hash[4] = 0xC3D2E1F0; + c->len = 0; + c->cnt = 0; + return 0; +} + +int SHA1_Update(SHA_CTX *c, const void *ptr, unsigned long n) +{ + unsigned long nb; + const unsigned char *p = ptr; + + c->len += (uint64_t) n << 3; + while (n != 0) { + if (c->cnt || n < 64) { + nb = 64 - c->cnt; + if (nb > n) + nb = n; + memcpy(&c->buf.b[c->cnt], p, nb); + if ((c->cnt += nb) == 64) { + sha1_core(c->hash, c->buf.b, 1); + c->cnt = 0; + } + } else { + nb = n >> 6; + sha1_core(c->hash, p, nb); + nb <<= 6; + } + n -= nb; + p += nb; + } + return 0; +} + +int SHA1_Final(unsigned char *hash, SHA_CTX *c) +{ + unsigned int cnt = c->cnt; + + c->buf.b[cnt++] = 0x80; + if (cnt > 56) { + if (cnt < 64) + memset(&c->buf.b[cnt], 0, 64 - cnt); + sha1_core(c->hash, c->buf.b, 1); + cnt = 0; + } + if (cnt < 56) + memset(&c->buf.b[cnt], 0, 56 - cnt); + c->buf.l[7] = c->len; + sha1_core(c->hash, c->buf.b, 1); + memcpy(hash, c->hash, 20); + return 0; +} diff --git a/ppc/sha1.h b/ppc/sha1.h new file mode 100644 index 0000000000..c3c51aa4d4 --- /dev/null +++ b/ppc/sha1.h @@ -0,0 +1,20 @@ +/* + * SHA-1 implementation. + * + * Copyright (C) 2005 Paul Mackerras <paulus@samba.org> + */ +#include <stdint.h> + +typedef struct sha_context { + uint32_t hash[5]; + uint32_t cnt; + uint64_t len; + union { + unsigned char b[64]; + uint64_t l[8]; + } buf; +} SHA_CTX; + +int SHA1_Init(SHA_CTX *c); +int SHA1_Update(SHA_CTX *c, const void *p, unsigned long n); +int SHA1_Final(unsigned char *hash, SHA_CTX *c); diff --git a/ppc/sha1ppc.S b/ppc/sha1ppc.S new file mode 100644 index 0000000000..f132696ee7 --- /dev/null +++ b/ppc/sha1ppc.S @@ -0,0 +1,224 @@ +/* + * SHA-1 implementation for PowerPC. + * + * Copyright (C) 2005 Paul Mackerras <paulus@samba.org> + */ + +/* + * PowerPC calling convention: + * %r0 - volatile temp + * %r1 - stack pointer. + * %r2 - reserved + * %r3-%r12 - Incoming arguments & return values; volatile. + * %r13-%r31 - Callee-save registers + * %lr - Return address, volatile + * %ctr - volatile + * + * Register usage in this routine: + * %r0 - temp + * %r3 - argument (pointer to 5 words of SHA state) + * %r4 - argument (pointer to data to hash) + * %r5 - Constant K in SHA round (initially number of blocks to hash) + * %r6-%r10 - Working copies of SHA variables A..E (actually E..A order) + * %r11-%r26 - Data being hashed W[]. + * %r27-%r31 - Previous copies of A..E, for final add back. + * %ctr - loop count + */ + + +/* + * We roll the registers for A, B, C, D, E around on each + * iteration; E on iteration t is D on iteration t+1, and so on. + * We use registers 6 - 10 for this. (Registers 27 - 31 hold + * the previous values.) + */ +#define RA(t) (((t)+4)%5+6) +#define RB(t) (((t)+3)%5+6) +#define RC(t) (((t)+2)%5+6) +#define RD(t) (((t)+1)%5+6) +#define RE(t) (((t)+0)%5+6) + +/* We use registers 11 - 26 for the W values */ +#define W(t) ((t)%16+11) + +/* Register 5 is used for the constant k */ + +/* + * The basic SHA-1 round function is: + * E += ROTL(A,5) + F(B,C,D) + W[i] + K; B = ROTL(B,30) + * Then the variables are renamed: (A,B,C,D,E) = (E,A,B,C,D). + * + * Every 20 rounds, the function F() and the constant K changes: + * - 20 rounds of f0(b,c,d) = "bit wise b ? c : d" = (^b & d) + (b & c) + * - 20 rounds of f1(b,c,d) = b^c^d = (b^d)^c + * - 20 rounds of f2(b,c,d) = majority(b,c,d) = (b&d) + ((b^d)&c) + * - 20 more rounds of f1(b,c,d) + * + * These are all scheduled for near-optimal performance on a G4. + * The G4 is a 3-issue out-of-order machine with 3 ALUs, but it can only + * *consider* starting the oldest 3 instructions per cycle. So to get + * maximum performance out of it, you have to treat it as an in-order + * machine. Which means interleaving the computation round t with the + * computation of W[t+4]. + * + * The first 16 rounds use W values loaded directly from memory, while the + * remaining 64 use values computed from those first 16. We preload + * 4 values before starting, so there are three kinds of rounds: + * - The first 12 (all f0) also load the W values from memory. + * - The next 64 compute W(i+4) in parallel. 8*f0, 20*f1, 20*f2, 16*f1. + * - The last 4 (all f1) do not do anything with W. + * + * Therefore, we have 6 different round functions: + * STEPD0_LOAD(t,s) - Perform round t and load W(s). s < 16 + * STEPD0_UPDATE(t,s) - Perform round t and compute W(s). s >= 16. + * STEPD1_UPDATE(t,s) + * STEPD2_UPDATE(t,s) + * STEPD1(t) - Perform round t with no load or update. + * + * The G5 is more fully out-of-order, and can find the parallelism + * by itself. The big limit is that it has a 2-cycle ALU latency, so + * even though it's 2-way, the code has to be scheduled as if it's + * 4-way, which can be a limit. To help it, we try to schedule the + * read of RA(t) as late as possible so it doesn't stall waiting for + * the previous round's RE(t-1), and we try to rotate RB(t) as early + * as possible while reading RC(t) (= RB(t-1)) as late as possible. + */ + +/* the initial loads. */ +#define LOADW(s) \ + lwz W(s),(s)*4(%r4) + +/* + * Perform a step with F0, and load W(s). Uses W(s) as a temporary + * before loading it. + * This is actually 10 instructions, which is an awkward fit. + * It can execute grouped as listed, or delayed one instruction. + * (If delayed two instructions, there is a stall before the start of the + * second line.) Thus, two iterations take 7 cycles, 3.5 cycles per round. + */ +#define STEPD0_LOAD(t,s) \ +add RE(t),RE(t),W(t); andc %r0,RD(t),RB(t); and W(s),RC(t),RB(t); \ +add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; rotlwi RB(t),RB(t),30; \ +add RE(t),RE(t),W(s); add %r0,%r0,%r5; lwz W(s),(s)*4(%r4); \ +add RE(t),RE(t),%r0 + +/* + * This is likewise awkward, 13 instructions. However, it can also + * execute starting with 2 out of 3 possible moduli, so it does 2 rounds + * in 9 cycles, 4.5 cycles/round. + */ +#define STEPD0_UPDATE(t,s,loadk...) \ +add RE(t),RE(t),W(t); andc %r0,RD(t),RB(t); xor W(s),W((s)-16),W((s)-3); \ +add RE(t),RE(t),%r0; and %r0,RC(t),RB(t); xor W(s),W(s),W((s)-8); \ +add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; xor W(s),W(s),W((s)-14); \ +add RE(t),RE(t),%r5; loadk; rotlwi RB(t),RB(t),30; rotlwi W(s),W(s),1; \ +add RE(t),RE(t),%r0 + +/* Nicely optimal. Conveniently, also the most common. */ +#define STEPD1_UPDATE(t,s,loadk...) \ +add RE(t),RE(t),W(t); xor %r0,RD(t),RB(t); xor W(s),W((s)-16),W((s)-3); \ +add RE(t),RE(t),%r5; loadk; xor %r0,%r0,RC(t); xor W(s),W(s),W((s)-8); \ +add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; xor W(s),W(s),W((s)-14); \ +add RE(t),RE(t),%r0; rotlwi RB(t),RB(t),30; rotlwi W(s),W(s),1 + +/* + * The naked version, no UPDATE, for the last 4 rounds. 3 cycles per. + * We could use W(s) as a temp register, but we don't need it. + */ +#define STEPD1(t) \ + add RE(t),RE(t),W(t); xor %r0,RD(t),RB(t); \ +rotlwi RB(t),RB(t),30; add RE(t),RE(t),%r5; xor %r0,%r0,RC(t); \ +add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; /* spare slot */ \ +add RE(t),RE(t),%r0 + +/* + * 14 instructions, 5 cycles per. The majority function is a bit + * awkward to compute. This can execute with a 1-instruction delay, + * but it causes a 2-instruction delay, which triggers a stall. + */ +#define STEPD2_UPDATE(t,s,loadk...) \ +add RE(t),RE(t),W(t); and %r0,RD(t),RB(t); xor W(s),W((s)-16),W((s)-3); \ +add RE(t),RE(t),%r0; xor %r0,RD(t),RB(t); xor W(s),W(s),W((s)-8); \ +add RE(t),RE(t),%r5; loadk; and %r0,%r0,RC(t); xor W(s),W(s),W((s)-14); \ +add RE(t),RE(t),%r0; rotlwi %r0,RA(t),5; rotlwi W(s),W(s),1; \ +add RE(t),RE(t),%r0; rotlwi RB(t),RB(t),30 + +#define STEP0_LOAD4(t,s) \ + STEPD0_LOAD(t,s); \ + STEPD0_LOAD((t+1),(s)+1); \ + STEPD0_LOAD((t)+2,(s)+2); \ + STEPD0_LOAD((t)+3,(s)+3) + +#define STEPUP4(fn, t, s, loadk...) \ + STEP##fn##_UPDATE(t,s,); \ + STEP##fn##_UPDATE((t)+1,(s)+1,); \ + STEP##fn##_UPDATE((t)+2,(s)+2,); \ + STEP##fn##_UPDATE((t)+3,(s)+3,loadk) + +#define STEPUP20(fn, t, s, loadk...) \ + STEPUP4(fn, t, s,); \ + STEPUP4(fn, (t)+4, (s)+4,); \ + STEPUP4(fn, (t)+8, (s)+8,); \ + STEPUP4(fn, (t)+12, (s)+12,); \ + STEPUP4(fn, (t)+16, (s)+16, loadk) + + .globl sha1_core +sha1_core: + stwu %r1,-80(%r1) + stmw %r13,4(%r1) + + /* Load up A - E */ + lmw %r27,0(%r3) + + mtctr %r5 + +1: + LOADW(0) + lis %r5,0x5a82 + mr RE(0),%r31 + LOADW(1) + mr RD(0),%r30 + mr RC(0),%r29 + LOADW(2) + ori %r5,%r5,0x7999 /* K0-19 */ + mr RB(0),%r28 + LOADW(3) + mr RA(0),%r27 + + STEP0_LOAD4(0, 4) + STEP0_LOAD4(4, 8) + STEP0_LOAD4(8, 12) + STEPUP4(D0, 12, 16,) + STEPUP4(D0, 16, 20, lis %r5,0x6ed9) + + ori %r5,%r5,0xeba1 /* K20-39 */ + STEPUP20(D1, 20, 24, lis %r5,0x8f1b) + + ori %r5,%r5,0xbcdc /* K40-59 */ + STEPUP20(D2, 40, 44, lis %r5,0xca62) + + ori %r5,%r5,0xc1d6 /* K60-79 */ + STEPUP4(D1, 60, 64,) + STEPUP4(D1, 64, 68,) + STEPUP4(D1, 68, 72,) + STEPUP4(D1, 72, 76,) + addi %r4,%r4,64 + STEPD1(76) + STEPD1(77) + STEPD1(78) + STEPD1(79) + + /* Add results to original values */ + add %r31,%r31,RE(0) + add %r30,%r30,RD(0) + add %r29,%r29,RC(0) + add %r28,%r28,RB(0) + add %r27,%r27,RA(0) + + bdnz 1b + + /* Save final hash, restore registers, and return */ + stmw %r27,0(%r3) + lmw %r13,4(%r1) + addi %r1,%r1,80 + blr diff --git a/quote.c b/quote.c new file mode 100644 index 0000000000..fb9e4ca253 --- /dev/null +++ b/quote.c @@ -0,0 +1,423 @@ +#include "cache.h" +#include "quote.h" + +/* Help to copy the thing properly quoted for the shell safety. + * any single quote is replaced with '\'', any exclamation point + * is replaced with '\!', and the whole thing is enclosed in a + * + * E.g. + * original sq_quote result + * name ==> name ==> 'name' + * a b ==> a b ==> 'a b' + * a'b ==> a'\''b ==> 'a'\''b' + * a!b ==> a'\!'b ==> 'a'\!'b' + */ +#undef EMIT +#define EMIT(x) do { if (++len < n) *bp++ = (x); } while(0) + +static inline int need_bs_quote(char c) +{ + return (c == '\'' || c == '!'); +} + +size_t sq_quote_buf(char *dst, size_t n, const char *src) +{ + char c; + char *bp = dst; + size_t len = 0; + + EMIT('\''); + while ((c = *src++)) { + if (need_bs_quote(c)) { + EMIT('\''); + EMIT('\\'); + EMIT(c); + EMIT('\''); + } else { + EMIT(c); + } + } + EMIT('\''); + + if ( n ) + *bp = 0; + + return len; +} + +void sq_quote_print(FILE *stream, const char *src) +{ + char c; + + fputc('\'', stream); + while ((c = *src++)) { + if (need_bs_quote(c)) { + fputs("'\\", stream); + fputc(c, stream); + fputc('\'', stream); + } else { + fputc(c, stream); + } + } + fputc('\'', stream); +} + +char *sq_quote(const char *src) +{ + char *buf; + size_t cnt; + + cnt = sq_quote_buf(NULL, 0, src) + 1; + buf = xmalloc(cnt); + sq_quote_buf(buf, cnt, src); + + return buf; +} + +char *sq_quote_argv(const char** argv, int count) +{ + char *buf, *to; + int i; + size_t len = 0; + + /* Count argv if needed. */ + if (count < 0) { + for (count = 0; argv[count]; count++) + ; /* just counting */ + } + + /* Special case: no argv. */ + if (!count) + return xcalloc(1,1); + + /* Get destination buffer length. */ + for (i = 0; i < count; i++) + len += sq_quote_buf(NULL, 0, argv[i]) + 1; + + /* Alloc destination buffer. */ + to = buf = xmalloc(len + 1); + + /* Copy into destination buffer. */ + for (i = 0; i < count; ++i) { + *to++ = ' '; + to += sq_quote_buf(to, len, argv[i]); + } + + return buf; +} + +/* + * Append a string to a string buffer, with or without shell quoting. + * Return true if the buffer overflowed. + */ +int add_to_string(char **ptrp, int *sizep, const char *str, int quote) +{ + char *p = *ptrp; + int size = *sizep; + int oc; + int err = 0; + + if (quote) + oc = sq_quote_buf(p, size, str); + else { + oc = strlen(str); + memcpy(p, str, (size <= oc) ? size - 1 : oc); + } + + if (size <= oc) { + err = 1; + oc = size - 1; + } + + *ptrp += oc; + **ptrp = '\0'; + *sizep -= oc; + return err; +} + +char *sq_dequote(char *arg) +{ + char *dst = arg; + char *src = arg; + char c; + + if (*src != '\'') + return NULL; + for (;;) { + c = *++src; + if (!c) + return NULL; + if (c != '\'') { + *dst++ = c; + continue; + } + /* We stepped out of sq */ + switch (*++src) { + case '\0': + *dst = 0; + return arg; + case '\\': + c = *++src; + if (need_bs_quote(c) && *++src == '\'') { + *dst++ = c; + continue; + } + /* Fallthrough */ + default: + return NULL; + } + } +} + +/* + * C-style name quoting. + * + * Does one of three things: + * + * (1) if outbuf and outfp are both NULL, inspect the input name and + * counts the number of bytes that are needed to hold c_style + * quoted version of name, counting the double quotes around + * it but not terminating NUL, and returns it. However, if name + * does not need c_style quoting, it returns 0. + * + * (2) if outbuf is not NULL, it must point at a buffer large enough + * to hold the c_style quoted version of name, enclosing double + * quotes, and terminating NUL. Fills outbuf with c_style quoted + * version of name enclosed in double-quote pair. Return value + * is undefined. + * + * (3) if outfp is not NULL, outputs c_style quoted version of name, + * but not enclosed in double-quote pair. Return value is undefined. + */ + +static int quote_c_style_counted(const char *name, int namelen, + char *outbuf, FILE *outfp, int no_dq) +{ +#undef EMIT +#define EMIT(c) \ + (outbuf ? (*outbuf++ = (c)) : outfp ? fputc(c, outfp) : (count++)) + +#define EMITQ() EMIT('\\') + + const char *sp; + int ch, count = 0, needquote = 0; + + if (!no_dq) + EMIT('"'); + for (sp = name; sp < name + namelen; sp++) { + ch = *sp; + if (!ch) + break; + if ((ch < ' ') || (ch == '"') || (ch == '\\') || + (ch >= 0177)) { + needquote = 1; + switch (ch) { + case '\a': EMITQ(); ch = 'a'; break; + case '\b': EMITQ(); ch = 'b'; break; + case '\f': EMITQ(); ch = 'f'; break; + case '\n': EMITQ(); ch = 'n'; break; + case '\r': EMITQ(); ch = 'r'; break; + case '\t': EMITQ(); ch = 't'; break; + case '\v': EMITQ(); ch = 'v'; break; + + case '\\': /* fallthru */ + case '"': EMITQ(); break; + default: + /* octal */ + EMITQ(); + EMIT(((ch >> 6) & 03) + '0'); + EMIT(((ch >> 3) & 07) + '0'); + ch = (ch & 07) + '0'; + break; + } + } + EMIT(ch); + } + if (!no_dq) + EMIT('"'); + if (outbuf) + *outbuf = 0; + + return needquote ? count : 0; +} + +int quote_c_style(const char *name, char *outbuf, FILE *outfp, int no_dq) +{ + int cnt = strlen(name); + return quote_c_style_counted(name, cnt, outbuf, outfp, no_dq); +} + +/* + * C-style name unquoting. + * + * Quoted should point at the opening double quote. Returns + * an allocated memory that holds unquoted name, which the caller + * should free when done. Updates endp pointer to point at + * one past the ending double quote if given. + */ + +char *unquote_c_style(const char *quoted, const char **endp) +{ + const char *sp; + char *name = NULL, *outp = NULL; + int count = 0, ch, ac; + +#undef EMIT +#define EMIT(c) (outp ? (*outp++ = (c)) : (count++)) + + if (*quoted++ != '"') + return NULL; + + while (1) { + /* first pass counts and allocates, second pass fills */ + for (sp = quoted; (ch = *sp++) != '"'; ) { + if (ch == '\\') { + switch (ch = *sp++) { + case 'a': ch = '\a'; break; + case 'b': ch = '\b'; break; + case 'f': ch = '\f'; break; + case 'n': ch = '\n'; break; + case 'r': ch = '\r'; break; + case 't': ch = '\t'; break; + case 'v': ch = '\v'; break; + + case '\\': case '"': + break; /* verbatim */ + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + /* octal */ + ac = ((ch - '0') << 6); + if ((ch = *sp++) < '0' || '7' < ch) + return NULL; + ac |= ((ch - '0') << 3); + if ((ch = *sp++) < '0' || '7' < ch) + return NULL; + ac |= (ch - '0'); + ch = ac; + break; + default: + return NULL; /* malformed */ + } + } + EMIT(ch); + } + + if (name) { + *outp = 0; + if (endp) + *endp = sp; + return name; + } + outp = name = xmalloc(count + 1); + } +} + +void write_name_quoted(const char *prefix, int prefix_len, + const char *name, int quote, FILE *out) +{ + int needquote; + + if (!quote) { + no_quote: + if (prefix_len) + fprintf(out, "%.*s", prefix_len, prefix); + fputs(name, out); + return; + } + + needquote = 0; + if (prefix_len) + needquote = quote_c_style_counted(prefix, prefix_len, + NULL, NULL, 0); + if (!needquote) + needquote = quote_c_style(name, NULL, NULL, 0); + if (needquote) { + fputc('"', out); + if (prefix_len) + quote_c_style_counted(prefix, prefix_len, + NULL, out, 1); + quote_c_style(name, NULL, out, 1); + fputc('"', out); + } + else + goto no_quote; +} + +/* quoting as a string literal for other languages */ + +void perl_quote_print(FILE *stream, const char *src) +{ + const char sq = '\''; + const char bq = '\\'; + char c; + + fputc(sq, stream); + while ((c = *src++)) { + if (c == sq || c == bq) + fputc(bq, stream); + fputc(c, stream); + } + fputc(sq, stream); +} + +void python_quote_print(FILE *stream, const char *src) +{ + const char sq = '\''; + const char bq = '\\'; + const char nl = '\n'; + char c; + + fputc(sq, stream); + while ((c = *src++)) { + if (c == nl) { + fputc(bq, stream); + fputc('n', stream); + continue; + } + if (c == sq || c == bq) + fputc(bq, stream); + fputc(c, stream); + } + fputc(sq, stream); +} + +void tcl_quote_print(FILE *stream, const char *src) +{ + char c; + + fputc('"', stream); + while ((c = *src++)) { + switch (c) { + case '[': case ']': + case '{': case '}': + case '$': case '\\': case '"': + fputc('\\', stream); + default: + fputc(c, stream); + break; + case '\f': + fputs("\\f", stream); + break; + case '\r': + fputs("\\r", stream); + break; + case '\n': + fputs("\\n", stream); + break; + case '\t': + fputs("\\t", stream); + break; + case '\v': + fputs("\\v", stream); + break; + } + } + fputc('"', stream); +} diff --git a/quote.h b/quote.h new file mode 100644 index 0000000000..bdc3610df5 --- /dev/null +++ b/quote.h @@ -0,0 +1,60 @@ +#ifndef QUOTE_H +#define QUOTE_H + +#include <stddef.h> +#include <stdio.h> + +/* Help to copy the thing properly quoted for the shell safety. + * any single quote is replaced with '\'', any exclamation point + * is replaced with '\!', and the whole thing is enclosed in a + * single quote pair. + * + * For example, if you are passing the result to system() as an + * argument: + * + * sprintf(cmd, "foobar %s %s", sq_quote(arg0), sq_quote(arg1)) + * + * would be appropriate. If the system() is going to call ssh to + * run the command on the other side: + * + * sprintf(cmd, "git-diff-tree %s %s", sq_quote(arg0), sq_quote(arg1)); + * sprintf(rcmd, "ssh %s %s", sq_quote(host), sq_quote(cmd)); + * + * Note that the above examples leak memory! Remember to free result from + * sq_quote() in a real application. + * + * sq_quote_buf() writes to an existing buffer of specified size; it + * will return the number of characters that would have been written + * excluding the final null regardless of the buffer size. + */ + +extern char *sq_quote(const char *src); +extern void sq_quote_print(FILE *stream, const char *src); +extern size_t sq_quote_buf(char *dst, size_t n, const char *src); +extern char *sq_quote_argv(const char** argv, int count); + +/* + * Append a string to a string buffer, with or without shell quoting. + * Return true if the buffer overflowed. + */ +extern int add_to_string(char **ptrp, int *sizep, const char *str, int quote); + +/* This unwraps what sq_quote() produces in place, but returns + * NULL if the input does not look like what sq_quote would have + * produced. + */ +extern char *sq_dequote(char *); + +extern int quote_c_style(const char *name, char *outbuf, FILE *outfp, + int nodq); +extern char *unquote_c_style(const char *quoted, const char **endp); + +extern void write_name_quoted(const char *prefix, int prefix_len, + const char *name, int quote, FILE *out); + +/* quoting as a string literal for other languages */ +extern void perl_quote_print(FILE *stream, const char *src); +extern void python_quote_print(FILE *stream, const char *src); +extern void tcl_quote_print(FILE *stream, const char *src); + +#endif diff --git a/reachable.c b/reachable.c new file mode 100644 index 0000000000..01760d7046 --- /dev/null +++ b/reachable.c @@ -0,0 +1,201 @@ +#include "cache.h" +#include "refs.h" +#include "tag.h" +#include "commit.h" +#include "blob.h" +#include "diff.h" +#include "revision.h" +#include "reachable.h" +#include "cache-tree.h" + +static void process_blob(struct blob *blob, + struct object_array *p, + struct name_path *path, + const char *name) +{ + struct object *obj = &blob->object; + + if (obj->flags & SEEN) + return; + obj->flags |= SEEN; + /* Nothing to do, really .. The blob lookup was the important part */ +} + +static void process_tree(struct tree *tree, + struct object_array *p, + struct name_path *path, + const char *name) +{ + struct object *obj = &tree->object; + struct tree_desc desc; + struct name_entry entry; + struct name_path me; + + if (obj->flags & SEEN) + return; + obj->flags |= SEEN; + if (parse_tree(tree) < 0) + die("bad tree object %s", sha1_to_hex(obj->sha1)); + name = xstrdup(name); + add_object(obj, p, path, name); + me.up = path; + me.elem = name; + me.elem_len = strlen(name); + + desc.buf = tree->buffer; + desc.size = tree->size; + + while (tree_entry(&desc, &entry)) { + if (S_ISDIR(entry.mode)) + process_tree(lookup_tree(entry.sha1), p, &me, entry.path); + else + process_blob(lookup_blob(entry.sha1), p, &me, entry.path); + } + free(tree->buffer); + tree->buffer = NULL; +} + +static void process_tag(struct tag *tag, struct object_array *p, const char *name) +{ + struct object *obj = &tag->object; + struct name_path me; + + if (obj->flags & SEEN) + return; + obj->flags |= SEEN; + + me.up = NULL; + me.elem = "tag:/"; + me.elem_len = 5; + + if (parse_tag(tag) < 0) + die("bad tag object %s", sha1_to_hex(obj->sha1)); + add_object(tag->tagged, p, NULL, name); +} + +static void walk_commit_list(struct rev_info *revs) +{ + int i; + struct commit *commit; + struct object_array objects = { 0, 0, NULL }; + + /* Walk all commits, process their trees */ + while ((commit = get_revision(revs)) != NULL) + process_tree(commit->tree, &objects, NULL, ""); + + /* Then walk all the pending objects, recursively processing them too */ + for (i = 0; i < revs->pending.nr; i++) { + struct object_array_entry *pending = revs->pending.objects + i; + struct object *obj = pending->item; + const char *name = pending->name; + if (obj->type == OBJ_TAG) { + process_tag((struct tag *) obj, &objects, name); + continue; + } + if (obj->type == OBJ_TREE) { + process_tree((struct tree *)obj, &objects, NULL, name); + continue; + } + if (obj->type == OBJ_BLOB) { + process_blob((struct blob *)obj, &objects, NULL, name); + continue; + } + die("unknown pending object %s (%s)", sha1_to_hex(obj->sha1), name); + } +} + +static int add_one_reflog_ent(unsigned char *osha1, unsigned char *nsha1, + const char *email, unsigned long timestamp, int tz, + const char *message, void *cb_data) +{ + struct object *object; + struct rev_info *revs = (struct rev_info *)cb_data; + + object = parse_object(osha1); + if (object) + add_pending_object(revs, object, ""); + object = parse_object(nsha1); + if (object) + add_pending_object(revs, object, ""); + return 0; +} + +static int add_one_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + struct object *object = parse_object(sha1); + struct rev_info *revs = (struct rev_info *)cb_data; + + if (!object) + die("bad object ref: %s:%s", path, sha1_to_hex(sha1)); + add_pending_object(revs, object, ""); + + return 0; +} + +static int add_one_reflog(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + for_each_reflog_ent(path, add_one_reflog_ent, cb_data); + return 0; +} + +static void add_one_tree(const unsigned char *sha1, struct rev_info *revs) +{ + struct tree *tree = lookup_tree(sha1); + add_pending_object(revs, &tree->object, ""); +} + +static void add_cache_tree(struct cache_tree *it, struct rev_info *revs) +{ + int i; + + if (it->entry_count >= 0) + add_one_tree(it->sha1, revs); + for (i = 0; i < it->subtree_nr; i++) + add_cache_tree(it->down[i]->cache_tree, revs); +} + +static void add_cache_refs(struct rev_info *revs) +{ + int i; + + read_cache(); + for (i = 0; i < active_nr; i++) { + lookup_blob(active_cache[i]->sha1); + /* + * We could add the blobs to the pending list, but quite + * frankly, we don't care. Once we've looked them up, and + * added them as objects, we've really done everything + * there is to do for a blob + */ + } + if (active_cache_tree) + add_cache_tree(active_cache_tree, revs); +} + +void mark_reachable_objects(struct rev_info *revs, int mark_reflog) +{ + /* + * Set up revision parsing, and mark us as being interested + * in all object types, not just commits. + */ + revs->tag_objects = 1; + revs->blob_objects = 1; + revs->tree_objects = 1; + + /* Add all refs from the index file */ + add_cache_refs(revs); + + /* Add all external refs */ + for_each_ref(add_one_ref, revs); + + /* Add all reflog info */ + if (mark_reflog) + for_each_reflog(add_one_reflog, revs); + + /* + * Set up the revision walk - this will move all commits + * from the pending list to the commit walking list. + */ + prepare_revision_walk(revs); + walk_commit_list(revs); +} diff --git a/reachable.h b/reachable.h new file mode 100644 index 0000000000..40751810b6 --- /dev/null +++ b/reachable.h @@ -0,0 +1,6 @@ +#ifndef REACHEABLE_H +#define REACHEABLE_H + +extern void mark_reachable_objects(struct rev_info *revs, int mark_reflog); + +#endif diff --git a/read-cache.c b/read-cache.c new file mode 100644 index 0000000000..c54a611877 --- /dev/null +++ b/read-cache.c @@ -0,0 +1,1020 @@ +/* + * GIT - The information manager from hell + * + * 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; +static time_t index_file_timestamp; +unsigned int active_nr, active_alloc, active_cache_changed; + +struct cache_tree *active_cache_tree; + +int cache_errno; + +static void *cache_mmap; +static size_t cache_mmap_size; + +/* + * This only updates the "non-critical" parts of the directory + * cache, ie the parts that aren't tracked by GIT, and only used + * to validate the cache. + */ +void fill_stat_cache_info(struct cache_entry *ce, struct stat *st) +{ + ce->ce_ctime.sec = htonl(st->st_ctime); + ce->ce_mtime.sec = htonl(st->st_mtime); +#ifdef USE_NSEC + ce->ce_ctime.nsec = htonl(st->st_ctim.tv_nsec); + ce->ce_mtime.nsec = htonl(st->st_mtim.tv_nsec); +#endif + ce->ce_dev = htonl(st->st_dev); + ce->ce_ino = htonl(st->st_ino); + ce->ce_uid = htonl(st->st_uid); + ce->ce_gid = htonl(st->st_gid); + ce->ce_size = htonl(st->st_size); + + if (assume_unchanged) + ce->ce_flags |= htons(CE_VALID); +} + +static int ce_compare_data(struct cache_entry *ce, struct stat *st) +{ + int match = -1; + int fd = open(ce->name, O_RDONLY); + + if (fd >= 0) { + unsigned char sha1[20]; + if (!index_fd(sha1, fd, st, 0, NULL)) + match = hashcmp(sha1, ce->sha1); + /* index_fd() closed the file descriptor already */ + } + return match; +} + +static int ce_compare_link(struct cache_entry *ce, unsigned long expected_size) +{ + int match = -1; + char *target; + void *buffer; + unsigned long size; + char type[10]; + int len; + + target = xmalloc(expected_size); + len = readlink(ce->name, target, expected_size); + if (len != expected_size) { + free(target); + return -1; + } + buffer = read_sha1_file(ce->sha1, type, &size); + if (!buffer) { + free(target); + return -1; + } + if (size == expected_size) + match = memcmp(buffer, target, size); + free(buffer); + free(target); + return match; +} + +static int ce_modified_check_fs(struct cache_entry *ce, struct stat *st) +{ + switch (st->st_mode & S_IFMT) { + case S_IFREG: + if (ce_compare_data(ce, st)) + return DATA_CHANGED; + break; + case S_IFLNK: + if (ce_compare_link(ce, st->st_size)) + return DATA_CHANGED; + break; + default: + return TYPE_CHANGED; + } + return 0; +} + +static int ce_match_stat_basic(struct cache_entry *ce, struct stat *st) +{ + unsigned int changed = 0; + + switch (ntohl(ce->ce_mode) & S_IFMT) { + case S_IFREG: + changed |= !S_ISREG(st->st_mode) ? TYPE_CHANGED : 0; + /* We consider only the owner x bit to be relevant for + * "mode changes" + */ + if (trust_executable_bit && + (0100 & (ntohl(ce->ce_mode) ^ st->st_mode))) + changed |= MODE_CHANGED; + break; + case S_IFLNK: + changed |= !S_ISLNK(st->st_mode) ? TYPE_CHANGED : 0; + break; + default: + die("internal error: ce_mode is %o", ntohl(ce->ce_mode)); + } + if (ce->ce_mtime.sec != htonl(st->st_mtime)) + changed |= MTIME_CHANGED; + if (ce->ce_ctime.sec != htonl(st->st_ctime)) + changed |= CTIME_CHANGED; + +#ifdef USE_NSEC + /* + * nsec seems unreliable - not all filesystems support it, so + * as long as it is in the inode cache you get right nsec + * but after it gets flushed, you get zero nsec. + */ + if (ce->ce_mtime.nsec != htonl(st->st_mtim.tv_nsec)) + changed |= MTIME_CHANGED; + if (ce->ce_ctime.nsec != htonl(st->st_ctim.tv_nsec)) + changed |= CTIME_CHANGED; +#endif + + if (ce->ce_uid != htonl(st->st_uid) || + ce->ce_gid != htonl(st->st_gid)) + changed |= OWNER_CHANGED; + if (ce->ce_ino != htonl(st->st_ino)) + changed |= INODE_CHANGED; + +#ifdef USE_STDEV + /* + * st_dev breaks on network filesystems where different + * clients will have different views of what "device" + * the filesystem is on + */ + if (ce->ce_dev != htonl(st->st_dev)) + changed |= INODE_CHANGED; +#endif + + if (ce->ce_size != htonl(st->st_size)) + changed |= DATA_CHANGED; + + return changed; +} + +int ce_match_stat(struct cache_entry *ce, struct stat *st, int options) +{ + unsigned int changed; + int ignore_valid = options & 01; + int assume_racy_is_modified = options & 02; + + /* + * If it's marked as always valid in the index, it's + * valid whatever the checked-out copy says. + */ + if (!ignore_valid && (ce->ce_flags & htons(CE_VALID))) + return 0; + + changed = ce_match_stat_basic(ce, st); + + /* + * Within 1 second of this sequence: + * echo xyzzy >file && git-update-index --add file + * running this command: + * echo frotz >file + * would give a falsely clean cache entry. The mtime and + * length match the cache, and other stat fields do not change. + * + * We could detect this at update-index time (the cache entry + * being registered/updated records the same time as "now") + * and delay the return from git-update-index, but that would + * effectively mean we can make at most one commit per second, + * which is not acceptable. Instead, we check cache entries + * whose mtime are the same as the index file timestamp more + * carefully than others. + */ + if (!changed && + index_file_timestamp && + index_file_timestamp <= ntohl(ce->ce_mtime.sec)) { + if (assume_racy_is_modified) + changed |= DATA_CHANGED; + else + changed |= ce_modified_check_fs(ce, st); + } + + return changed; +} + +int ce_modified(struct cache_entry *ce, struct stat *st, int really) +{ + int changed, changed_fs; + changed = ce_match_stat(ce, st, really); + if (!changed) + return 0; + /* + * If the mode or type has changed, there's no point in trying + * to refresh the entry - it's not going to match + */ + if (changed & (MODE_CHANGED | TYPE_CHANGED)) + return changed; + + /* Immediately after read-tree or update-index --cacheinfo, + * the length field is zero. For other cases the ce_size + * should match the SHA1 recorded in the index entry. + */ + if ((changed & DATA_CHANGED) && ce->ce_size != htonl(0)) + return changed; + + changed_fs = ce_modified_check_fs(ce, st); + if (changed_fs) + return changed | changed_fs; + return 0; +} + +int base_name_compare(const char *name1, int len1, int mode1, + const char *name2, int len2, int mode2) +{ + unsigned char c1, c2; + int len = len1 < len2 ? len1 : len2; + int cmp; + + cmp = memcmp(name1, name2, len); + if (cmp) + return cmp; + c1 = name1[len]; + c2 = name2[len]; + if (!c1 && S_ISDIR(mode1)) + c1 = '/'; + if (!c2 && S_ISDIR(mode2)) + c2 = '/'; + return (c1 < c2) ? -1 : (c1 > c2) ? 1 : 0; +} + +int cache_name_compare(const char *name1, int flags1, const char *name2, int flags2) +{ + int len1 = flags1 & CE_NAMEMASK; + int len2 = flags2 & CE_NAMEMASK; + int len = len1 < len2 ? len1 : len2; + int cmp; + + cmp = memcmp(name1, name2, len); + if (cmp) + return cmp; + if (len1 < len2) + return -1; + if (len1 > len2) + return 1; + + /* Compare stages */ + flags1 &= CE_STAGEMASK; + flags2 &= CE_STAGEMASK; + + if (flags1 < flags2) + return -1; + if (flags1 > flags2) + return 1; + return 0; +} + +int cache_name_pos(const char *name, int namelen) +{ + int first, last; + + first = 0; + last = active_nr; + while (last > first) { + int next = (last + first) >> 1; + struct cache_entry *ce = active_cache[next]; + int cmp = cache_name_compare(name, namelen, ce->name, ntohs(ce->ce_flags)); + if (!cmp) + return next; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + return -first-1; +} + +/* Remove entry, return true if there are more entries to go.. */ +int remove_cache_entry_at(int pos) +{ + active_cache_changed = 1; + active_nr--; + if (pos >= active_nr) + return 0; + memmove(active_cache + pos, active_cache + pos + 1, (active_nr - pos) * sizeof(struct cache_entry *)); + return 1; +} + +int remove_file_from_cache(const char *path) +{ + int pos = cache_name_pos(path, strlen(path)); + if (pos < 0) + pos = -pos-1; + while (pos < active_nr && !strcmp(active_cache[pos]->name, path)) + remove_cache_entry_at(pos); + return 0; +} + +int add_file_to_index(const char *path, int verbose) +{ + int size, namelen; + struct stat st; + struct cache_entry *ce; + + if (lstat(path, &st)) + die("%s: unable to stat (%s)", path, strerror(errno)); + + if (!S_ISREG(st.st_mode) && !S_ISLNK(st.st_mode)) + die("%s: can only add regular files or symbolic links", path); + + namelen = strlen(path); + size = cache_entry_size(namelen); + ce = xcalloc(1, size); + memcpy(ce->name, path, namelen); + ce->ce_flags = htons(namelen); + fill_stat_cache_info(ce, &st); + + ce->ce_mode = create_ce_mode(st.st_mode); + if (!trust_executable_bit) { + /* If there is an existing entry, pick the mode bits + * from it, otherwise assume unexecutable. + */ + int pos = cache_name_pos(path, namelen); + if (pos >= 0) + ce->ce_mode = active_cache[pos]->ce_mode; + else if (S_ISREG(st.st_mode)) + ce->ce_mode = create_ce_mode(S_IFREG | 0666); + } + + if (index_path(ce->sha1, path, &st, 1)) + die("unable to index file %s", path); + if (add_cache_entry(ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE)) + 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; +} + +int ce_same_name(struct cache_entry *a, struct cache_entry *b) +{ + int len = ce_namelen(a); + return ce_namelen(b) == len && !memcmp(a->name, b->name, len); +} + +int ce_path_match(const struct cache_entry *ce, const char **pathspec) +{ + const char *match, *name; + int len; + + if (!pathspec) + return 1; + + len = ce_namelen(ce); + name = ce->name; + while ((match = *pathspec++) != NULL) { + int matchlen = strlen(match); + if (matchlen > len) + continue; + if (memcmp(name, match, matchlen)) + continue; + if (matchlen && name[matchlen-1] == '/') + return 1; + if (name[matchlen] == '/' || !name[matchlen]) + return 1; + if (!matchlen) + return 1; + } + return 0; +} + +/* + * We fundamentally don't like some paths: we don't want + * dot or dot-dot anywhere, and for obvious reasons don't + * want to recurse into ".git" either. + * + * Also, we don't want double slashes or slashes at the + * end that can make pathnames ambiguous. + */ +static int verify_dotfile(const char *rest) +{ + /* + * The first character was '.', but that + * has already been discarded, we now test + * the rest. + */ + switch (*rest) { + /* "." is not allowed */ + case '\0': case '/': + return 0; + + /* + * ".git" followed by NUL or slash is bad. This + * shares the path end test with the ".." case. + */ + case 'g': + if (rest[1] != 'i') + break; + if (rest[2] != 't') + break; + rest += 2; + /* fallthrough */ + case '.': + if (rest[1] == '\0' || rest[1] == '/') + return 0; + } + return 1; +} + +int verify_path(const char *path) +{ + char c; + + goto inside; + for (;;) { + if (!c) + return 1; + if (c == '/') { +inside: + c = *path++; + switch (c) { + default: + continue; + case '/': case '\0': + break; + case '.': + if (verify_dotfile(path)) + continue; + } + return 0; + } + c = *path++; + } +} + +/* + * Do we have another file that has the beginning components being a + * proper superset of the name we're trying to add? + */ +static int has_file_name(const struct cache_entry *ce, int pos, int ok_to_replace) +{ + int retval = 0; + int len = ce_namelen(ce); + int stage = ce_stage(ce); + const char *name = ce->name; + + while (pos < active_nr) { + struct cache_entry *p = active_cache[pos++]; + + if (len >= ce_namelen(p)) + break; + if (memcmp(name, p->name, len)) + break; + if (ce_stage(p) != stage) + continue; + if (p->name[len] != '/') + continue; + retval = -1; + if (!ok_to_replace) + break; + remove_cache_entry_at(--pos); + } + return retval; +} + +/* + * Do we have another file with a pathname that is a proper + * subset of the name we're trying to add? + */ +static int has_dir_name(const struct cache_entry *ce, int pos, int ok_to_replace) +{ + int retval = 0; + int stage = ce_stage(ce); + const char *name = ce->name; + const char *slash = name + ce_namelen(ce); + + for (;;) { + int len; + + for (;;) { + if (*--slash == '/') + break; + if (slash <= ce->name) + return retval; + } + len = slash - name; + + pos = cache_name_pos(name, ntohs(create_ce_flags(len, stage))); + if (pos >= 0) { + retval = -1; + if (!ok_to_replace) + break; + remove_cache_entry_at(pos); + continue; + } + + /* + * Trivial optimization: if we find an entry that + * already matches the sub-directory, then we know + * we're ok, and we can exit. + */ + pos = -pos-1; + while (pos < active_nr) { + struct cache_entry *p = active_cache[pos]; + if ((ce_namelen(p) <= len) || + (p->name[len] != '/') || + memcmp(p->name, name, len)) + break; /* not our subdirectory */ + if (ce_stage(p) == stage) + /* p is at the same stage as our entry, and + * is a subdirectory of what we are looking + * at, so we cannot have conflicts at our + * level or anything shorter. + */ + return retval; + pos++; + } + } + return retval; +} + +/* We may be in a situation where we already have path/file and path + * is being added, or we already have path and path/file is being + * added. Either one would result in a nonsense tree that has path + * twice when git-write-tree tries to write it out. Prevent it. + * + * If ok-to-replace is specified, we remove the conflicting entries + * from the cache so the caller should recompute the insert position. + * When this happens, we return non-zero. + */ +static int check_file_directory_conflict(const struct cache_entry *ce, int pos, int ok_to_replace) +{ + /* + * We check if the path is a sub-path of a subsequent pathname + * first, since removing those will not change the position + * in the array + */ + int retval = has_file_name(ce, pos, ok_to_replace); + /* + * Then check if the path might have a clashing sub-directory + * before it. + */ + return retval + has_dir_name(ce, pos, ok_to_replace); +} + +int add_cache_entry(struct cache_entry *ce, int option) +{ + int pos; + int ok_to_add = option & ADD_CACHE_OK_TO_ADD; + int ok_to_replace = option & ADD_CACHE_OK_TO_REPLACE; + int skip_df_check = option & ADD_CACHE_SKIP_DFCHECK; + + pos = cache_name_pos(ce->name, ntohs(ce->ce_flags)); + + /* existing match? Just replace it. */ + if (pos >= 0) { + active_cache_changed = 1; + active_cache[pos] = ce; + return 0; + } + pos = -pos-1; + + /* + * Inserting a merged entry ("stage 0") into the index + * will always replace all non-merged entries.. + */ + if (pos < active_nr && ce_stage(ce) == 0) { + while (ce_same_name(active_cache[pos], ce)) { + ok_to_add = 1; + if (!remove_cache_entry_at(pos)) + break; + } + } + + if (!ok_to_add) + return -1; + if (!verify_path(ce->name)) + return -1; + + if (!skip_df_check && + check_file_directory_conflict(ce, pos, ok_to_replace)) { + if (!ok_to_replace) + return error("'%s' appears as both a file and as a directory", ce->name); + pos = cache_name_pos(ce->name, ntohs(ce->ce_flags)); + pos = -pos-1; + } + + /* Make sure the array is big enough .. */ + if (active_nr == active_alloc) { + active_alloc = alloc_nr(active_alloc); + active_cache = xrealloc(active_cache, active_alloc * sizeof(struct cache_entry *)); + } + + /* Add it in.. */ + active_nr++; + if (active_nr > pos) + memmove(active_cache + pos + 1, active_cache + pos, (active_nr - pos - 1) * sizeof(ce)); + active_cache[pos] = ce; + active_cache_changed = 1; + return 0; +} + +/* + * "refresh" does not calculate a new sha1 file or bring the + * cache up-to-date for mode/content changes. But what it + * _does_ do is to "re-match" the stat information of a file + * with the cache, so that you can refresh the cache for a + * file that hasn't been changed but where the stat entry is + * out of date. + * + * For example, you'd want to do this after doing a "git-read-tree", + * to link up the stat cache details with the proper files. + */ +struct cache_entry *refresh_cache_entry(struct cache_entry *ce, int really) +{ + struct stat st; + struct cache_entry *updated; + int changed, size; + + if (lstat(ce->name, &st) < 0) { + cache_errno = errno; + return NULL; + } + + changed = ce_match_stat(ce, &st, really); + if (!changed) { + if (really && assume_unchanged && + !(ce->ce_flags & htons(CE_VALID))) + ; /* mark this one VALID again */ + else + return ce; + } + + if (ce_modified(ce, &st, really)) { + cache_errno = EINVAL; + return NULL; + } + + size = ce_size(ce); + updated = xmalloc(size); + memcpy(updated, ce, size); + fill_stat_cache_info(updated, &st); + + /* In this case, if really is not set, we should leave + * CE_VALID bit alone. Otherwise, paths marked with + * --no-assume-unchanged (i.e. things to be edited) will + * reacquire CE_VALID bit automatically, which is not + * really what we want. + */ + if (!really && assume_unchanged && !(ce->ce_flags & htons(CE_VALID))) + updated->ce_flags &= ~htons(CE_VALID); + + return updated; +} + +int refresh_cache(unsigned int flags) +{ + int i; + int has_errors = 0; + int really = (flags & REFRESH_REALLY) != 0; + int allow_unmerged = (flags & REFRESH_UNMERGED) != 0; + int quiet = (flags & REFRESH_QUIET) != 0; + int not_new = (flags & REFRESH_IGNORE_MISSING) != 0; + + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce, *new; + ce = active_cache[i]; + if (ce_stage(ce)) { + while ((i < active_nr) && + ! strcmp(active_cache[i]->name, ce->name)) + i++; + i--; + if (allow_unmerged) + continue; + printf("%s: needs merge\n", ce->name); + has_errors = 1; + continue; + } + + new = refresh_cache_entry(ce, really); + if (new == ce) + continue; + if (!new) { + if (not_new && cache_errno == ENOENT) + continue; + if (really && cache_errno == EINVAL) { + /* If we are doing --really-refresh that + * means the index is not valid anymore. + */ + ce->ce_flags &= ~htons(CE_VALID); + active_cache_changed = 1; + } + if (quiet) + continue; + printf("%s: needs update\n", ce->name); + has_errors = 1; + continue; + } + active_cache_changed = 1; + /* You can NOT just free active_cache[i] here, since it + * might not be necessarily malloc()ed but can also come + * from mmap(). */ + active_cache[i] = new; + } + return has_errors; +} + +static int verify_hdr(struct cache_header *hdr, unsigned long size) +{ + SHA_CTX c; + unsigned char sha1[20]; + + if (hdr->hdr_signature != htonl(CACHE_SIGNATURE)) + return error("bad signature"); + if (hdr->hdr_version != htonl(2)) + return error("bad index version"); + SHA1_Init(&c); + SHA1_Update(&c, hdr, size - 20); + SHA1_Final(sha1, &c); + if (hashcmp(sha1, (unsigned char *)hdr + size - 20)) + return error("bad index file sha1 signature"); + 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) +{ + return read_cache_from(get_index_file()); +} + +/* remember to discard_cache() before reading a different cache! */ +int read_cache_from(const char *path) +{ + int fd, i; + struct stat st; + unsigned long offset; + struct cache_header *hdr; + + errno = EBUSY; + if (cache_mmap) + return active_nr; + + errno = ENOENT; + index_file_timestamp = 0; + fd = open(path, O_RDONLY); + if (fd < 0) { + if (errno == ENOENT) + return 0; + die("index file open failed (%s)", strerror(errno)); + } + + if (!fstat(fd, &st)) { + cache_mmap_size = st.st_size; + errno = EINVAL; + if (cache_mmap_size >= sizeof(struct cache_header) + 20) + cache_mmap = xmmap(NULL, cache_mmap_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); + else + die("index file smaller than expected"); + } else + die("cannot stat the open index (%s)", strerror(errno)); + close(fd); + + hdr = cache_mmap; + if (verify_hdr(hdr, cache_mmap_size) < 0) + goto unmap; + + active_nr = ntohl(hdr->hdr_entries); + active_alloc = alloc_nr(active_nr); + active_cache = xcalloc(active_alloc, sizeof(struct cache_entry *)); + + offset = sizeof(*hdr); + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = (struct cache_entry *) ((char *) cache_mmap + offset); + offset = offset + ce_size(ce); + active_cache[i] = ce; + } + index_file_timestamp = st.st_mtime; + while (offset <= cache_mmap_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, (char *) cache_mmap + offset + 4, 4); + extsize = ntohl(extsize); + if (read_index_extension(((const char *) cache_mmap) + offset, + (char *) cache_mmap + offset + 8, + extsize) < 0) + goto unmap; + offset += 8; + offset += extsize; + } + return active_nr; + +unmap: + munmap(cache_mmap, cache_mmap_size); + errno = EINVAL; + die("index file corrupt"); +} + +int discard_cache(void) +{ + int ret; + + active_nr = active_cache_changed = 0; + index_file_timestamp = 0; + cache_tree_free(&active_cache_tree); + if (cache_mmap == NULL) + return 0; + ret = munmap(cache_mmap, cache_mmap_size); + cache_mmap = NULL; + cache_mmap_size = 0; + + /* no need to throw away allocated active_cache */ + return ret; +} + +#define WRITE_BUFFER_SIZE 8192 +static unsigned char write_buffer[WRITE_BUFFER_SIZE]; +static unsigned long write_buffer_len; + +static int ce_write_flush(SHA_CTX *context, int fd) +{ + unsigned int buffered = write_buffer_len; + if (buffered) { + SHA1_Update(context, write_buffer, buffered); + if (write_in_full(fd, write_buffer, buffered) != buffered) + return -1; + write_buffer_len = 0; + } + return 0; +} + +static int ce_write(SHA_CTX *context, int fd, void *data, unsigned int len) +{ + while (len) { + unsigned int buffered = write_buffer_len; + unsigned int partial = WRITE_BUFFER_SIZE - buffered; + if (partial > len) + partial = len; + memcpy(write_buffer + buffered, data, partial); + buffered += partial; + if (buffered == WRITE_BUFFER_SIZE) { + write_buffer_len = buffered; + if (ce_write_flush(context, fd)) + return -1; + buffered = 0; + } + write_buffer_len = buffered; + len -= partial; + data = (char *) data + partial; + } + 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); + return ((ce_write(context, fd, &ext, 4) < 0) || + (ce_write(context, fd, &sz, 4) < 0)) ? -1 : 0; +} + +static int ce_flush(SHA_CTX *context, int fd) +{ + unsigned int left = write_buffer_len; + + if (left) { + write_buffer_len = 0; + SHA1_Update(context, write_buffer, left); + } + + /* Flush first if not enough space for SHA1 signature */ + if (left + 20 > WRITE_BUFFER_SIZE) { + if (write_in_full(fd, write_buffer, left) != left) + return -1; + left = 0; + } + + /* Append the SHA1 signature at the end */ + SHA1_Final(write_buffer + left, context); + left += 20; + return (write_in_full(fd, write_buffer, left) != left) ? -1 : 0; +} + +static void ce_smudge_racily_clean_entry(struct cache_entry *ce) +{ + /* + * The only thing we care about in this function is to smudge the + * falsely clean entry due to touch-update-touch race, so we leave + * everything else as they are. We are called for entries whose + * ce_mtime match the index file mtime. + */ + struct stat st; + + if (lstat(ce->name, &st) < 0) + return; + if (ce_match_stat_basic(ce, &st)) + return; + if (ce_modified_check_fs(ce, &st)) { + /* This is "racily clean"; smudge it. Note that this + * is a tricky code. At first glance, it may appear + * that it can break with this sequence: + * + * $ echo xyzzy >frotz + * $ git-update-index --add frotz + * $ : >frotz + * $ sleep 3 + * $ echo filfre >nitfol + * $ git-update-index --add nitfol + * + * but it does not. When the second update-index runs, + * it notices that the entry "frotz" has the same timestamp + * as index, and if we were to smudge it by resetting its + * size to zero here, then the object name recorded + * in index is the 6-byte file but the cached stat information + * becomes zero --- which would then match what we would + * obtain from the filesystem next time we stat("frotz"). + * + * However, the second update-index, before calling + * this function, notices that the cached size is 6 + * bytes and what is on the filesystem is an empty + * file, and never calls us, so the cached size information + * for "frotz" stays 6 which does not match the filesystem. + */ + ce->ce_size = htonl(0); + } +} + +int write_cache(int newfd, struct cache_entry **cache, int entries) +{ + SHA_CTX c; + struct cache_header hdr; + int i, removed; + + for (i = removed = 0; i < entries; i++) + if (!cache[i]->ce_mode) + removed++; + + hdr.hdr_signature = htonl(CACHE_SIGNATURE); + hdr.hdr_version = htonl(2); + hdr.hdr_entries = htonl(entries - removed); + + SHA1_Init(&c); + if (ce_write(&c, newfd, &hdr, sizeof(hdr)) < 0) + return -1; + + for (i = 0; i < entries; i++) { + struct cache_entry *ce = cache[i]; + if (!ce->ce_mode) + continue; + if (index_file_timestamp && + index_file_timestamp <= ntohl(ce->ce_mtime.sec)) + ce_smudge_racily_clean_entry(ce); + 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)) + free(data); + else { + free(data); + return -1; + } + } + return ce_flush(&c, newfd); +} diff --git a/receive-pack.c b/receive-pack.c new file mode 100644 index 0000000000..7311c822dd --- /dev/null +++ b/receive-pack.c @@ -0,0 +1,458 @@ +#include "cache.h" +#include "pack.h" +#include "refs.h" +#include "pkt-line.h" +#include "run-command.h" +#include "exec_cmd.h" +#include "commit.h" +#include "object.h" + +static const char receive_pack_usage[] = "git-receive-pack <git-dir>"; + +static int deny_non_fast_forwards = 0; +static int receive_unpack_limit = -1; +static int transfer_unpack_limit = -1; +static int unpack_limit = 100; +static int report_status; + +static char capabilities[] = " report-status delete-refs "; +static int capabilities_sent; + +static int receive_pack_config(const char *var, const char *value) +{ + if (strcmp(var, "receive.denynonfastforwards") == 0) { + deny_non_fast_forwards = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "receive.unpacklimit") == 0) { + receive_unpack_limit = git_config_int(var, value); + return 0; + } + + if (strcmp(var, "transfer.unpacklimit") == 0) { + transfer_unpack_limit = git_config_int(var, value); + return 0; + } + + return git_default_config(var, value); +} + +static int show_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + if (capabilities_sent) + packet_write(1, "%s %s\n", sha1_to_hex(sha1), path); + else + packet_write(1, "%s %s%c%s\n", + sha1_to_hex(sha1), path, 0, capabilities); + capabilities_sent = 1; + return 0; +} + +static void write_head_info(void) +{ + for_each_ref(show_ref, NULL); + if (!capabilities_sent) + show_ref("capabilities^{}", null_sha1, 0, NULL); + +} + +struct command { + struct command *next; + const char *error_string; + unsigned char old_sha1[20]; + unsigned char new_sha1[20]; + char ref_name[FLEX_ARRAY]; /* more */ +}; + +static struct command *commands; + +static char update_hook[] = "hooks/update"; + +static int run_update_hook(const char *refname, + char *old_hex, char *new_hex) +{ + int code; + + if (access(update_hook, X_OK) < 0) + return 0; + code = run_command_opt(RUN_COMMAND_NO_STDIN + | RUN_COMMAND_STDOUT_TO_STDERR, + update_hook, refname, old_hex, new_hex, NULL); + switch (code) { + case 0: + return 0; + case -ERR_RUN_COMMAND_FORK: + return error("hook fork failed"); + case -ERR_RUN_COMMAND_EXEC: + return error("hook execute failed"); + case -ERR_RUN_COMMAND_WAITPID: + return error("waitpid failed"); + case -ERR_RUN_COMMAND_WAITPID_WRONG_PID: + return error("waitpid is confused"); + case -ERR_RUN_COMMAND_WAITPID_SIGNAL: + return error("%s died of signal", update_hook); + case -ERR_RUN_COMMAND_WAITPID_NOEXIT: + return error("%s died strangely", update_hook); + default: + error("%s exited with error code %d", update_hook, -code); + return -code; + } +} + +static int update(struct command *cmd) +{ + const char *name = cmd->ref_name; + unsigned char *old_sha1 = cmd->old_sha1; + unsigned char *new_sha1 = cmd->new_sha1; + char new_hex[41], old_hex[41]; + struct ref_lock *lock; + + cmd->error_string = NULL; + if (!strncmp(name, "refs/", 5) && check_ref_format(name + 5)) { + cmd->error_string = "funny refname"; + return error("refusing to create funny ref '%s' locally", + name); + } + + strcpy(new_hex, sha1_to_hex(new_sha1)); + strcpy(old_hex, sha1_to_hex(old_sha1)); + + if (!is_null_sha1(new_sha1) && !has_sha1_file(new_sha1)) { + cmd->error_string = "bad pack"; + return error("unpack should have generated %s, " + "but I can't find it!", new_hex); + } + if (deny_non_fast_forwards && !is_null_sha1(new_sha1) && + !is_null_sha1(old_sha1) && + !strncmp(name, "refs/heads/", 11)) { + struct commit *old_commit, *new_commit; + struct commit_list *bases, *ent; + + old_commit = (struct commit *)parse_object(old_sha1); + new_commit = (struct commit *)parse_object(new_sha1); + bases = get_merge_bases(old_commit, new_commit, 1); + for (ent = bases; ent; ent = ent->next) + if (!hashcmp(old_sha1, ent->item->object.sha1)) + break; + free_commit_list(bases); + if (!ent) + return error("denying non-fast forward;" + " you should pull first"); + } + if (run_update_hook(name, old_hex, new_hex)) { + cmd->error_string = "hook declined"; + return error("hook declined to update %s", name); + } + + if (is_null_sha1(new_sha1)) { + if (delete_ref(name, old_sha1)) { + cmd->error_string = "failed to delete"; + return error("failed to delete %s", name); + } + fprintf(stderr, "%s: %s -> deleted\n", name, old_hex); + } + else { + lock = lock_any_ref_for_update(name, old_sha1); + if (!lock) { + cmd->error_string = "failed to lock"; + return error("failed to lock %s", name); + } + write_ref_sha1(lock, new_sha1, "push"); + fprintf(stderr, "%s: %s -> %s\n", name, old_hex, new_hex); + } + return 0; +} + +static char update_post_hook[] = "hooks/post-update"; + +static void run_update_post_hook(struct command *cmd) +{ + struct command *cmd_p; + int argc; + const char **argv; + + if (access(update_post_hook, X_OK) < 0) + return; + for (argc = 1, cmd_p = cmd; cmd_p; cmd_p = cmd_p->next) { + if (cmd_p->error_string) + continue; + argc++; + } + argv = xmalloc(sizeof(*argv) * (1 + argc)); + argv[0] = update_post_hook; + + for (argc = 1, cmd_p = cmd; cmd_p; cmd_p = cmd_p->next) { + char *p; + if (cmd_p->error_string) + continue; + p = xmalloc(strlen(cmd_p->ref_name) + 1); + strcpy(p, cmd_p->ref_name); + argv[argc] = p; + argc++; + } + argv[argc] = NULL; + run_command_v_opt(argv, RUN_COMMAND_NO_STDIN + | RUN_COMMAND_STDOUT_TO_STDERR); +} + +/* + * This gets called after(if) we've successfully + * unpacked the data payload. + */ +static void execute_commands(void) +{ + struct command *cmd = commands; + + while (cmd) { + update(cmd); + cmd = cmd->next; + } + run_update_post_hook(commands); +} + +static void read_head_info(void) +{ + struct command **p = &commands; + for (;;) { + static char line[1000]; + unsigned char old_sha1[20], new_sha1[20]; + struct command *cmd; + char *refname; + int len, reflen; + + len = packet_read_line(0, line, sizeof(line)); + if (!len) + break; + if (line[len-1] == '\n') + line[--len] = 0; + if (len < 83 || + line[40] != ' ' || + line[81] != ' ' || + get_sha1_hex(line, old_sha1) || + get_sha1_hex(line + 41, new_sha1)) + die("protocol error: expected old/new/ref, got '%s'", + line); + + refname = line + 82; + reflen = strlen(refname); + if (reflen + 82 < len) { + if (strstr(refname + reflen + 1, "report-status")) + report_status = 1; + } + cmd = xmalloc(sizeof(struct command) + len - 80); + hashcpy(cmd->old_sha1, old_sha1); + hashcpy(cmd->new_sha1, new_sha1); + memcpy(cmd->ref_name, line + 82, len - 81); + cmd->error_string = "n/a (unpacker error)"; + cmd->next = NULL; + *p = cmd; + p = &cmd->next; + } +} + +static const char *parse_pack_header(struct pack_header *hdr) +{ + switch (read_pack_header(0, hdr)) { + case PH_ERROR_EOF: + return "eof before pack header was fully read"; + + case PH_ERROR_PACK_SIGNATURE: + return "protocol error (pack signature mismatch detected)"; + + case PH_ERROR_PROTOCOL: + return "protocol error (pack version unsupported)"; + + default: + return "unknown error in parse_pack_header"; + + case 0: + return NULL; + } +} + +static const char *pack_lockfile; + +static const char *unpack(void) +{ + struct pack_header hdr; + const char *hdr_err; + char hdr_arg[38]; + + hdr_err = parse_pack_header(&hdr); + if (hdr_err) + return hdr_err; + snprintf(hdr_arg, sizeof(hdr_arg), "--pack_header=%u,%u", + ntohl(hdr.hdr_version), ntohl(hdr.hdr_entries)); + + if (ntohl(hdr.hdr_entries) < unpack_limit) { + int code; + const char *unpacker[3]; + unpacker[0] = "unpack-objects"; + unpacker[1] = hdr_arg; + unpacker[2] = NULL; + code = run_command_v_opt(unpacker, RUN_GIT_CMD); + switch (code) { + case 0: + return NULL; + case -ERR_RUN_COMMAND_FORK: + return "unpack fork failed"; + case -ERR_RUN_COMMAND_EXEC: + return "unpack execute failed"; + case -ERR_RUN_COMMAND_WAITPID: + return "waitpid failed"; + case -ERR_RUN_COMMAND_WAITPID_WRONG_PID: + return "waitpid is confused"; + case -ERR_RUN_COMMAND_WAITPID_SIGNAL: + return "unpacker died of signal"; + case -ERR_RUN_COMMAND_WAITPID_NOEXIT: + return "unpacker died strangely"; + default: + return "unpacker exited with error code"; + } + } else { + const char *keeper[6]; + int fd[2], s, len, status; + pid_t pid; + char keep_arg[256]; + char packname[46]; + + s = sprintf(keep_arg, "--keep=receive-pack %i on ", getpid()); + if (gethostname(keep_arg + s, sizeof(keep_arg) - s)) + strcpy(keep_arg + s, "localhost"); + + keeper[0] = "index-pack"; + keeper[1] = "--stdin"; + keeper[2] = "--fix-thin"; + keeper[3] = hdr_arg; + keeper[4] = keep_arg; + keeper[5] = NULL; + + if (pipe(fd) < 0) + return "index-pack pipe failed"; + pid = fork(); + if (pid < 0) + return "index-pack fork failed"; + if (!pid) { + dup2(fd[1], 1); + close(fd[1]); + close(fd[0]); + execv_git_cmd(keeper); + die("execv of index-pack failed"); + } + close(fd[1]); + + /* + * The first thing we expects from index-pack's output + * is "pack\t%40s\n" or "keep\t%40s\n" (46 bytes) where + * %40s is the newly created pack SHA1 name. In the "keep" + * case, we need it to remove the corresponding .keep file + * later on. If we don't get that then tough luck with it. + */ + for (len = 0; + len < 46 && (s = xread(fd[0], packname+len, 46-len)) > 0; + len += s); + close(fd[0]); + if (len == 46 && packname[45] == '\n' && + memcmp(packname, "keep\t", 5) == 0) { + char path[PATH_MAX]; + packname[45] = 0; + snprintf(path, sizeof(path), "%s/pack/pack-%s.keep", + get_object_directory(), packname + 5); + pack_lockfile = xstrdup(path); + } + + /* Then wrap our index-pack process. */ + while (waitpid(pid, &status, 0) < 0) + if (errno != EINTR) + return "waitpid failed"; + if (WIFEXITED(status)) { + int code = WEXITSTATUS(status); + if (code) + return "index-pack exited with error code"; + reprepare_packed_git(); + return NULL; + } + return "index-pack abnormal exit"; + } +} + +static void report(const char *unpack_status) +{ + struct command *cmd; + packet_write(1, "unpack %s\n", + unpack_status ? unpack_status : "ok"); + for (cmd = commands; cmd; cmd = cmd->next) { + if (!cmd->error_string) + packet_write(1, "ok %s\n", + cmd->ref_name); + else + packet_write(1, "ng %s %s\n", + cmd->ref_name, cmd->error_string); + } + packet_flush(1); +} + +static int delete_only(struct command *cmd) +{ + while (cmd) { + if (!is_null_sha1(cmd->new_sha1)) + return 0; + cmd = cmd->next; + } + return 1; +} + +int main(int argc, char **argv) +{ + int i; + char *dir = NULL; + + argv++; + for (i = 1; i < argc; i++) { + char *arg = *argv++; + + if (*arg == '-') { + /* Do flag handling here */ + usage(receive_pack_usage); + } + if (dir) + usage(receive_pack_usage); + dir = arg; + } + if (!dir) + usage(receive_pack_usage); + + if (!enter_repo(dir, 0)) + die("'%s': unable to chdir or not a git archive", dir); + + if (is_repository_shallow()) + die("attempt to push into a shallow repository"); + + git_config(receive_pack_config); + + if (0 <= transfer_unpack_limit) + unpack_limit = transfer_unpack_limit; + else if (0 <= receive_unpack_limit) + unpack_limit = receive_unpack_limit; + + write_head_info(); + + /* EOF */ + packet_flush(1); + + read_head_info(); + if (commands) { + const char *unpack_status = NULL; + + if (!delete_only(commands)) + unpack_status = unpack(); + if (!unpack_status) + execute_commands(); + if (pack_lockfile) + unlink(pack_lockfile); + if (report_status) + report(unpack_status); + } + return 0; +} diff --git a/reflog-walk.c b/reflog-walk.c new file mode 100644 index 0000000000..c983858259 --- /dev/null +++ b/reflog-walk.c @@ -0,0 +1,273 @@ +#include "cache.h" +#include "commit.h" +#include "refs.h" +#include "diff.h" +#include "revision.h" +#include "path-list.h" +#include "reflog-walk.h" + +struct complete_reflogs { + char *ref; + struct reflog_info { + unsigned char osha1[20], nsha1[20]; + char *email; + unsigned long timestamp; + int tz; + char *message; + } *items; + int nr, alloc; +}; + +static int read_one_reflog(unsigned char *osha1, unsigned char *nsha1, + const char *email, unsigned long timestamp, int tz, + const char *message, void *cb_data) +{ + struct complete_reflogs *array = cb_data; + struct reflog_info *item; + + if (array->nr >= array->alloc) { + array->alloc = alloc_nr(array->nr + 1); + array->items = xrealloc(array->items, array->alloc * + sizeof(struct reflog_info)); + } + item = array->items + array->nr; + memcpy(item->osha1, osha1, 20); + memcpy(item->nsha1, nsha1, 20); + item->email = xstrdup(email); + item->timestamp = timestamp; + item->tz = tz; + item->message = xstrdup(message); + array->nr++; + return 0; +} + +static struct complete_reflogs *read_complete_reflog(const char *ref) +{ + struct complete_reflogs *reflogs = + xcalloc(sizeof(struct complete_reflogs), 1); + reflogs->ref = xstrdup(ref); + for_each_reflog_ent(ref, read_one_reflog, reflogs); + if (reflogs->nr == 0) { + unsigned char sha1[20]; + const char *name = resolve_ref(ref, sha1, 1, NULL); + if (name) + for_each_reflog_ent(name, read_one_reflog, reflogs); + } + if (reflogs->nr == 0) { + int len = strlen(ref); + char *refname = xmalloc(len + 12); + sprintf(refname, "refs/%s", ref); + for_each_reflog_ent(refname, read_one_reflog, reflogs); + if (reflogs->nr == 0) { + sprintf(refname, "refs/heads/%s", ref); + for_each_reflog_ent(refname, read_one_reflog, reflogs); + } + free(refname); + } + return reflogs; +} + +static int get_reflog_recno_by_time(struct complete_reflogs *array, + unsigned long timestamp) +{ + int i; + for (i = array->nr - 1; i >= 0; i--) + if (timestamp >= array->items[i].timestamp) + return i; + return -1; +} + +struct commit_info_lifo { + struct commit_info { + struct commit *commit; + void *util; + } *items; + int nr, alloc; +}; + +static struct commit_info *get_commit_info(struct commit *commit, + struct commit_info_lifo *lifo, int pop) +{ + int i; + for (i = 0; i < lifo->nr; i++) + if (lifo->items[i].commit == commit) { + struct commit_info *result = &lifo->items[i]; + if (pop) { + if (i + 1 < lifo->nr) + memmove(lifo->items + i, + lifo->items + i + 1, + (lifo->nr - i) * + sizeof(struct commit_info)); + lifo->nr--; + } + return result; + } + return NULL; +} + +static void add_commit_info(struct commit *commit, void *util, + struct commit_info_lifo *lifo) +{ + struct commit_info *info; + if (lifo->nr >= lifo->alloc) { + lifo->alloc = alloc_nr(lifo->nr + 1); + lifo->items = xrealloc(lifo->items, + lifo->alloc * sizeof(struct commit_info)); + } + info = lifo->items + lifo->nr; + info->commit = commit; + info->util = util; + lifo->nr++; +} + +struct commit_reflog { + int flag, recno; + struct complete_reflogs *reflogs; +}; + +struct reflog_walk_info { + struct commit_info_lifo reflogs; + struct path_list complete_reflogs; + struct commit_reflog *last_commit_reflog; +}; + +void init_reflog_walk(struct reflog_walk_info** info) +{ + *info = xcalloc(sizeof(struct reflog_walk_info), 1); +} + +void add_reflog_for_walk(struct reflog_walk_info *info, + struct commit *commit, const char *name) +{ + unsigned long timestamp = 0; + int recno = -1; + struct path_list_item *item; + struct complete_reflogs *reflogs; + char *branch, *at = strchr(name, '@'); + struct commit_reflog *commit_reflog; + + if (commit->object.flags & UNINTERESTING) + die ("Cannot walk reflogs for %s", name); + + branch = xstrdup(name); + if (at && at[1] == '{') { + char *ep; + branch[at - name] = '\0'; + recno = strtoul(at + 2, &ep, 10); + if (*ep != '}') { + recno = -1; + timestamp = approxidate(at + 2); + } + } else + recno = 0; + + item = path_list_lookup(branch, &info->complete_reflogs); + if (item) + reflogs = item->util; + else { + if (*branch == '\0') { + unsigned char sha1[20]; + const char *head = resolve_ref("HEAD", sha1, 0, NULL); + if (!head) + die ("No current branch"); + free(branch); + branch = xstrdup(head); + } + reflogs = read_complete_reflog(branch); + if (!reflogs || reflogs->nr == 0) { + unsigned char sha1[20]; + char *b; + if (dwim_log(branch, strlen(branch), sha1, &b) == 1) { + if (reflogs) { + free(reflogs->ref); + free(reflogs); + } + free(branch); + branch = b; + reflogs = read_complete_reflog(branch); + } + } + if (!reflogs || reflogs->nr == 0) + die("No reflogs found for '%s'", branch); + path_list_insert(branch, &info->complete_reflogs)->util + = reflogs; + } + + commit_reflog = xcalloc(sizeof(struct commit_reflog), 1); + if (recno < 0) { + commit_reflog->flag = 1; + commit_reflog->recno = get_reflog_recno_by_time(reflogs, timestamp); + if (commit_reflog->recno < 0) { + free(branch); + free(commit_reflog); + return; + } + } else + commit_reflog->recno = reflogs->nr - recno - 1; + commit_reflog->reflogs = reflogs; + + add_commit_info(commit, commit_reflog, &info->reflogs); +} + +void fake_reflog_parent(struct reflog_walk_info *info, struct commit *commit) +{ + struct commit_info *commit_info = + get_commit_info(commit, &info->reflogs, 0); + struct commit_reflog *commit_reflog; + struct reflog_info *reflog; + + info->last_commit_reflog = NULL; + if (!commit_info) + return; + + commit_reflog = commit_info->util; + if (commit_reflog->recno < 0) { + commit->parents = NULL; + return; + } + + reflog = &commit_reflog->reflogs->items[commit_reflog->recno]; + info->last_commit_reflog = commit_reflog; + commit_reflog->recno--; + commit_info->commit = (struct commit *)parse_object(reflog->osha1); + if (!commit_info->commit) { + commit->parents = NULL; + return; + } + + commit->parents = xcalloc(sizeof(struct commit_list), 1); + commit->parents->item = commit_info->commit; + commit->object.flags &= ~(ADDED | SEEN | SHOWN); +} + +void show_reflog_message(struct reflog_walk_info* info, int oneline, + int relative_date) +{ + if (info && info->last_commit_reflog) { + struct commit_reflog *commit_reflog = info->last_commit_reflog; + struct reflog_info *info; + + info = &commit_reflog->reflogs->items[commit_reflog->recno+1]; + if (oneline) { + printf("%s@{", commit_reflog->reflogs->ref); + if (commit_reflog->flag || relative_date) + printf("%s", show_date(info->timestamp, 0, 1)); + else + printf("%d", commit_reflog->reflogs->nr + - 2 - commit_reflog->recno); + printf("}: %s", info->message); + } + else { + printf("Reflog: %s@{", commit_reflog->reflogs->ref); + if (commit_reflog->flag || relative_date) + printf("%s", show_date(info->timestamp, + info->tz, + relative_date)); + else + printf("%d", commit_reflog->reflogs->nr + - 2 - commit_reflog->recno); + printf("} (%s)\nReflog message: %s", + info->email, info->message); + } + } +} diff --git a/reflog-walk.h b/reflog-walk.h new file mode 100644 index 0000000000..a4f7015d3e --- /dev/null +++ b/reflog-walk.h @@ -0,0 +1,11 @@ +#ifndef REFLOG_WALK_H +#define REFLOG_WALK_H + +extern void init_reflog_walk(struct reflog_walk_info** info); +extern void add_reflog_for_walk(struct reflog_walk_info *info, + struct commit *commit, const char *name); +extern void fake_reflog_parent(struct reflog_walk_info *info, + struct commit *commit); +extern void show_reflog_message(struct reflog_walk_info *info, int, int); + +#endif diff --git a/refs.c b/refs.c new file mode 100644 index 0000000000..9e3dfb3c97 --- /dev/null +++ b/refs.c @@ -0,0 +1,1262 @@ +#include "cache.h" +#include "refs.h" +#include "object.h" +#include "tag.h" + +/* ISSYMREF=01 and ISPACKED=02 are public interfaces */ +#define REF_KNOWS_PEELED 04 + +struct ref_list { + struct ref_list *next; + unsigned char flag; /* ISSYMREF? ISPACKED? */ + unsigned char sha1[20]; + unsigned char peeled[20]; + char name[FLEX_ARRAY]; +}; + +static const char *parse_ref_line(char *line, unsigned char *sha1) +{ + /* + * 42: the answer to everything. + * + * In this case, it happens to be the answer to + * 40 (length of sha1 hex representation) + * +1 (space in between hex and name) + * +1 (newline at the end of the line) + */ + int len = strlen(line) - 42; + + if (len <= 0) + return NULL; + if (get_sha1_hex(line, sha1) < 0) + return NULL; + if (!isspace(line[40])) + return NULL; + line += 41; + if (isspace(*line)) + return NULL; + if (line[len] != '\n') + return NULL; + line[len] = 0; + + return line; +} + +static struct ref_list *add_ref(const char *name, const unsigned char *sha1, + int flag, struct ref_list *list, + struct ref_list **new_entry) +{ + int len; + struct ref_list **p = &list, *entry; + + /* Find the place to insert the ref into.. */ + while ((entry = *p) != NULL) { + int cmp = strcmp(entry->name, name); + if (cmp > 0) + break; + + /* Same as existing entry? */ + if (!cmp) { + if (new_entry) + *new_entry = entry; + return list; + } + p = &entry->next; + } + + /* Allocate it and add it in.. */ + len = strlen(name) + 1; + entry = xmalloc(sizeof(struct ref_list) + len); + hashcpy(entry->sha1, sha1); + hashclr(entry->peeled); + memcpy(entry->name, name, len); + entry->flag = flag; + entry->next = *p; + *p = entry; + if (new_entry) + *new_entry = entry; + return list; +} + +/* + * Future: need to be in "struct repository" + * when doing a full libification. + */ +struct cached_refs { + char did_loose; + char did_packed; + struct ref_list *loose; + struct ref_list *packed; +} cached_refs; + +static void free_ref_list(struct ref_list *list) +{ + struct ref_list *next; + for ( ; list; list = next) { + next = list->next; + free(list); + } +} + +static void invalidate_cached_refs(void) +{ + struct cached_refs *ca = &cached_refs; + + if (ca->did_loose && ca->loose) + free_ref_list(ca->loose); + if (ca->did_packed && ca->packed) + free_ref_list(ca->packed); + ca->loose = ca->packed = NULL; + ca->did_loose = ca->did_packed = 0; +} + +static void read_packed_refs(FILE *f, struct cached_refs *cached_refs) +{ + struct ref_list *list = NULL; + struct ref_list *last = NULL; + char refline[PATH_MAX]; + int flag = REF_ISPACKED; + + while (fgets(refline, sizeof(refline), f)) { + unsigned char sha1[20]; + const char *name; + static const char header[] = "# pack-refs with:"; + + if (!strncmp(refline, header, sizeof(header)-1)) { + const char *traits = refline + sizeof(header) - 1; + if (strstr(traits, " peeled ")) + flag |= REF_KNOWS_PEELED; + /* perhaps other traits later as well */ + continue; + } + + name = parse_ref_line(refline, sha1); + if (name) { + list = add_ref(name, sha1, flag, list, &last); + continue; + } + if (last && + refline[0] == '^' && + strlen(refline) == 42 && + refline[41] == '\n' && + !get_sha1_hex(refline + 1, sha1)) + hashcpy(last->peeled, sha1); + } + cached_refs->packed = list; +} + +static struct ref_list *get_packed_refs(void) +{ + if (!cached_refs.did_packed) { + FILE *f = fopen(git_path("packed-refs"), "r"); + cached_refs.packed = NULL; + if (f) { + read_packed_refs(f, &cached_refs); + fclose(f); + } + cached_refs.did_packed = 1; + } + return cached_refs.packed; +} + +static struct ref_list *get_ref_dir(const char *base, struct ref_list *list) +{ + DIR *dir = opendir(git_path("%s", base)); + + if (dir) { + struct dirent *de; + int baselen = strlen(base); + char *ref = xmalloc(baselen + 257); + + memcpy(ref, base, baselen); + if (baselen && base[baselen-1] != '/') + ref[baselen++] = '/'; + + while ((de = readdir(dir)) != NULL) { + unsigned char sha1[20]; + struct stat st; + int flag; + int namelen; + + if (de->d_name[0] == '.') + continue; + namelen = strlen(de->d_name); + if (namelen > 255) + continue; + if (has_extension(de->d_name, ".lock")) + continue; + memcpy(ref + baselen, de->d_name, namelen+1); + if (stat(git_path("%s", ref), &st) < 0) + continue; + if (S_ISDIR(st.st_mode)) { + list = get_ref_dir(ref, list); + continue; + } + if (!resolve_ref(ref, sha1, 1, &flag)) { + error("%s points nowhere!", ref); + continue; + } + list = add_ref(ref, sha1, flag, list, NULL); + } + free(ref); + closedir(dir); + } + return list; +} + +static struct ref_list *get_loose_refs(void) +{ + if (!cached_refs.did_loose) { + cached_refs.loose = get_ref_dir("refs", NULL); + cached_refs.did_loose = 1; + } + return cached_refs.loose; +} + +/* We allow "recursive" symbolic refs. Only within reason, though */ +#define MAXDEPTH 5 + +const char *resolve_ref(const char *ref, unsigned char *sha1, int reading, int *flag) +{ + int depth = MAXDEPTH, len; + char buffer[256]; + static char ref_buffer[256]; + + if (flag) + *flag = 0; + + for (;;) { + const char *path = git_path("%s", ref); + struct stat st; + char *buf; + int fd; + + if (--depth < 0) + return NULL; + + /* Special case: non-existing file. + * Not having the refs/heads/new-branch is OK + * if we are writing into it, so is .git/HEAD + * that points at refs/heads/master still to be + * born. It is NOT OK if we are resolving for + * reading. + */ + if (lstat(path, &st) < 0) { + struct ref_list *list = get_packed_refs(); + while (list) { + if (!strcmp(ref, list->name)) { + hashcpy(sha1, list->sha1); + if (flag) + *flag |= REF_ISPACKED; + return ref; + } + list = list->next; + } + if (reading || errno != ENOENT) + return NULL; + hashclr(sha1); + return ref; + } + + /* Follow "normalized" - ie "refs/.." symlinks by hand */ + if (S_ISLNK(st.st_mode)) { + len = readlink(path, buffer, sizeof(buffer)-1); + if (len >= 5 && !memcmp("refs/", buffer, 5)) { + buffer[len] = 0; + strcpy(ref_buffer, buffer); + ref = ref_buffer; + if (flag) + *flag |= REF_ISSYMREF; + continue; + } + } + + /* Is it a directory? */ + if (S_ISDIR(st.st_mode)) { + errno = EISDIR; + return NULL; + } + + /* + * Anything else, just open it and try to use it as + * a ref + */ + fd = open(path, O_RDONLY); + if (fd < 0) + return NULL; + len = read_in_full(fd, buffer, sizeof(buffer)-1); + close(fd); + + /* + * Is it a symbolic ref? + */ + if (len < 4 || memcmp("ref:", buffer, 4)) + break; + buf = buffer + 4; + len -= 4; + while (len && isspace(*buf)) + buf++, len--; + while (len && isspace(buf[len-1])) + len--; + buf[len] = 0; + memcpy(ref_buffer, buf, len + 1); + ref = ref_buffer; + if (flag) + *flag |= REF_ISSYMREF; + } + if (len < 40 || get_sha1_hex(buffer, sha1)) + return NULL; + return ref; +} + +int read_ref(const char *ref, unsigned char *sha1) +{ + if (resolve_ref(ref, sha1, 1, NULL)) + return 0; + return -1; +} + +static int do_one_ref(const char *base, each_ref_fn fn, int trim, + void *cb_data, struct ref_list *entry) +{ + if (strncmp(base, entry->name, trim)) + return 0; + if (is_null_sha1(entry->sha1)) + return 0; + if (!has_sha1_file(entry->sha1)) { + error("%s does not point to a valid object!", entry->name); + return 0; + } + return fn(entry->name + trim, entry->sha1, entry->flag, cb_data); +} + +int peel_ref(const char *ref, unsigned char *sha1) +{ + int flag; + unsigned char base[20]; + struct object *o; + + if (!resolve_ref(ref, base, 1, &flag)) + return -1; + + if ((flag & REF_ISPACKED)) { + struct ref_list *list = get_packed_refs(); + + while (list) { + if (!strcmp(list->name, ref)) { + if (list->flag & REF_KNOWS_PEELED) { + hashcpy(sha1, list->peeled); + return 0; + } + /* older pack-refs did not leave peeled ones */ + break; + } + list = list->next; + } + } + + /* fallback - callers should not call this for unpacked refs */ + o = parse_object(base); + if (o->type == OBJ_TAG) { + o = deref_tag(o, ref, 0); + if (o) { + hashcpy(sha1, o->sha1); + return 0; + } + } + return -1; +} + +static int do_for_each_ref(const char *base, each_ref_fn fn, int trim, + void *cb_data) +{ + int retval; + struct ref_list *packed = get_packed_refs(); + struct ref_list *loose = get_loose_refs(); + + while (packed && loose) { + struct ref_list *entry; + int cmp = strcmp(packed->name, loose->name); + if (!cmp) { + packed = packed->next; + continue; + } + if (cmp > 0) { + entry = loose; + loose = loose->next; + } else { + entry = packed; + packed = packed->next; + } + retval = do_one_ref(base, fn, trim, cb_data, entry); + if (retval) + return retval; + } + + for (packed = packed ? packed : loose; packed; packed = packed->next) { + retval = do_one_ref(base, fn, trim, cb_data, packed); + if (retval) + return retval; + } + return 0; +} + +int head_ref(each_ref_fn fn, void *cb_data) +{ + unsigned char sha1[20]; + int flag; + + if (resolve_ref("HEAD", sha1, 1, &flag)) + return fn("HEAD", sha1, flag, cb_data); + return 0; +} + +int for_each_ref(each_ref_fn fn, void *cb_data) +{ + return do_for_each_ref("refs/", fn, 0, cb_data); +} + +int for_each_tag_ref(each_ref_fn fn, void *cb_data) +{ + return do_for_each_ref("refs/tags/", fn, 10, cb_data); +} + +int for_each_branch_ref(each_ref_fn fn, void *cb_data) +{ + return do_for_each_ref("refs/heads/", fn, 11, cb_data); +} + +int for_each_remote_ref(each_ref_fn fn, void *cb_data) +{ + return do_for_each_ref("refs/remotes/", fn, 13, cb_data); +} + +/* NEEDSWORK: This is only used by ssh-upload and it should go; the + * caller should do resolve_ref or read_ref like everybody else. Or + * maybe everybody else should use get_ref_sha1() instead of doing + * read_ref(). + */ +int get_ref_sha1(const char *ref, unsigned char *sha1) +{ + if (check_ref_format(ref)) + return -1; + return read_ref(mkpath("refs/%s", ref), sha1); +} + +/* + * Make sure "ref" is something reasonable to have under ".git/refs/"; + * We do not like it if: + * + * - any path component of it begins with ".", or + * - it has double dots "..", or + * - it has ASCII control character, "~", "^", ":" or SP, anywhere, or + * - it ends with a "/". + */ + +static inline int bad_ref_char(int ch) +{ + return (((unsigned) ch) <= ' ' || + ch == '~' || ch == '^' || ch == ':' || + /* 2.13 Pattern Matching Notation */ + ch == '?' || ch == '*' || ch == '['); +} + +int check_ref_format(const char *ref) +{ + int ch, level; + const char *cp = ref; + + level = 0; + while (1) { + while ((ch = *cp++) == '/') + ; /* tolerate duplicated slashes */ + if (!ch) + return -1; /* should not end with slashes */ + + /* we are at the beginning of the path component */ + if (ch == '.' || bad_ref_char(ch)) + return -1; + + /* scan the rest of the path component */ + while ((ch = *cp++) != 0) { + if (bad_ref_char(ch)) + return -1; + if (ch == '/') + break; + if (ch == '.' && *cp == '.') + return -1; + } + level++; + if (!ch) { + if (level < 2) + return -2; /* at least of form "heads/blah" */ + return 0; + } + } +} + +static struct ref_lock *verify_lock(struct ref_lock *lock, + const unsigned char *old_sha1, int mustexist) +{ + if (!resolve_ref(lock->ref_name, lock->old_sha1, mustexist, NULL)) { + error("Can't verify ref %s", lock->ref_name); + unlock_ref(lock); + return NULL; + } + if (hashcmp(lock->old_sha1, old_sha1)) { + error("Ref %s is at %s but expected %s", lock->ref_name, + sha1_to_hex(lock->old_sha1), sha1_to_hex(old_sha1)); + unlock_ref(lock); + return NULL; + } + return lock; +} + +static int remove_empty_dir_recursive(char *path, int len) +{ + DIR *dir = opendir(path); + struct dirent *e; + int ret = 0; + + if (!dir) + return -1; + if (path[len-1] != '/') + path[len++] = '/'; + while ((e = readdir(dir)) != NULL) { + struct stat st; + int namlen; + if ((e->d_name[0] == '.') && + ((e->d_name[1] == 0) || + ((e->d_name[1] == '.') && e->d_name[2] == 0))) + continue; /* "." and ".." */ + + namlen = strlen(e->d_name); + if ((len + namlen < PATH_MAX) && + strcpy(path + len, e->d_name) && + !lstat(path, &st) && + S_ISDIR(st.st_mode) && + !remove_empty_dir_recursive(path, len + namlen)) + continue; /* happy */ + + /* path too long, stat fails, or non-directory still exists */ + ret = -1; + break; + } + closedir(dir); + if (!ret) { + path[len] = 0; + ret = rmdir(path); + } + return ret; +} + +static int remove_empty_directories(char *file) +{ + /* we want to create a file but there is a directory there; + * if that is an empty directory (or a directory that contains + * only empty directories), remove them. + */ + char path[PATH_MAX]; + int len = strlen(file); + + if (len >= PATH_MAX) /* path too long ;-) */ + return -1; + strcpy(path, file); + return remove_empty_dir_recursive(path, len); +} + +static int is_refname_available(const char *ref, const char *oldref, + struct ref_list *list, int quiet) +{ + int namlen = strlen(ref); /* e.g. 'foo/bar' */ + while (list) { + /* list->name could be 'foo' or 'foo/bar/baz' */ + if (!oldref || strcmp(oldref, list->name)) { + int len = strlen(list->name); + int cmplen = (namlen < len) ? namlen : len; + const char *lead = (namlen < len) ? list->name : ref; + if (!strncmp(ref, list->name, cmplen) && + lead[cmplen] == '/') { + if (!quiet) + error("'%s' exists; cannot create '%s'", + list->name, ref); + return 0; + } + } + list = list->next; + } + return 1; +} + +static struct ref_lock *lock_ref_sha1_basic(const char *ref, const unsigned char *old_sha1, int *flag) +{ + char *ref_file; + const char *orig_ref = ref; + struct ref_lock *lock; + struct stat st; + int last_errno = 0; + int mustexist = (old_sha1 && !is_null_sha1(old_sha1)); + + lock = xcalloc(1, sizeof(struct ref_lock)); + lock->lock_fd = -1; + + ref = resolve_ref(ref, lock->old_sha1, mustexist, flag); + if (!ref && errno == EISDIR) { + /* we are trying to lock foo but we used to + * have foo/bar which now does not exist; + * it is normal for the empty directory 'foo' + * to remain. + */ + ref_file = git_path("%s", orig_ref); + if (remove_empty_directories(ref_file)) { + last_errno = errno; + error("there are still refs under '%s'", orig_ref); + goto error_return; + } + ref = resolve_ref(orig_ref, lock->old_sha1, mustexist, flag); + } + if (!ref) { + last_errno = errno; + error("unable to resolve reference %s: %s", + orig_ref, strerror(errno)); + goto error_return; + } + /* When the ref did not exist and we are creating it, + * make sure there is no existing ref that is packed + * whose name begins with our refname, nor a ref whose + * name is a proper prefix of our refname. + */ + if (is_null_sha1(lock->old_sha1) && + !is_refname_available(ref, NULL, get_packed_refs(), 0)) + goto error_return; + + lock->lk = xcalloc(1, sizeof(struct lock_file)); + + lock->ref_name = xstrdup(ref); + lock->orig_ref_name = xstrdup(orig_ref); + ref_file = git_path("%s", ref); + lock->force_write = lstat(ref_file, &st) && errno == ENOENT; + + if (safe_create_leading_directories(ref_file)) { + last_errno = errno; + error("unable to create directory for %s", ref_file); + goto error_return; + } + lock->lock_fd = hold_lock_file_for_update(lock->lk, ref_file, 1); + + return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock; + + error_return: + unlock_ref(lock); + errno = last_errno; + return NULL; +} + +struct ref_lock *lock_ref_sha1(const char *ref, const unsigned char *old_sha1) +{ + char refpath[PATH_MAX]; + if (check_ref_format(ref)) + return NULL; + strcpy(refpath, mkpath("refs/%s", ref)); + return lock_ref_sha1_basic(refpath, old_sha1, NULL); +} + +struct ref_lock *lock_any_ref_for_update(const char *ref, const unsigned char *old_sha1) +{ + if (check_ref_format(ref) == -1) + return NULL; + return lock_ref_sha1_basic(ref, old_sha1, NULL); +} + +static struct lock_file packlock; + +static int repack_without_ref(const char *refname) +{ + struct ref_list *list, *packed_ref_list; + int fd; + int found = 0; + + packed_ref_list = get_packed_refs(); + for (list = packed_ref_list; list; list = list->next) { + if (!strcmp(refname, list->name)) { + found = 1; + break; + } + } + if (!found) + return 0; + fd = hold_lock_file_for_update(&packlock, git_path("packed-refs"), 0); + if (fd < 0) + return error("cannot delete '%s' from packed refs", refname); + + for (list = packed_ref_list; list; list = list->next) { + char line[PATH_MAX + 100]; + int len; + + if (!strcmp(refname, list->name)) + continue; + len = snprintf(line, sizeof(line), "%s %s\n", + sha1_to_hex(list->sha1), list->name); + /* this should not happen but just being defensive */ + if (len > sizeof(line)) + die("too long a refname '%s'", list->name); + write_or_die(fd, line, len); + } + return commit_lock_file(&packlock); +} + +int delete_ref(const char *refname, unsigned char *sha1) +{ + struct ref_lock *lock; + int err, i, ret = 0, flag = 0; + + lock = lock_ref_sha1_basic(refname, sha1, &flag); + if (!lock) + return 1; + if (!(flag & REF_ISPACKED)) { + /* loose */ + i = strlen(lock->lk->filename) - 5; /* .lock */ + lock->lk->filename[i] = 0; + err = unlink(lock->lk->filename); + if (err) { + ret = 1; + error("unlink(%s) failed: %s", + lock->lk->filename, strerror(errno)); + } + lock->lk->filename[i] = '.'; + } + /* removing the loose one could have resurrected an earlier + * packed one. Also, if it was not loose we need to repack + * without it. + */ + ret |= repack_without_ref(refname); + + err = unlink(git_path("logs/%s", lock->ref_name)); + if (err && errno != ENOENT) + fprintf(stderr, "warning: unlink(%s) failed: %s", + git_path("logs/%s", lock->ref_name), strerror(errno)); + invalidate_cached_refs(); + unlock_ref(lock); + return ret; +} + +int rename_ref(const char *oldref, const char *newref, const char *logmsg) +{ + static const char renamed_ref[] = "RENAMED-REF"; + unsigned char sha1[20], orig_sha1[20]; + int flag = 0, logmoved = 0; + struct ref_lock *lock; + struct stat loginfo; + int log = !lstat(git_path("logs/%s", oldref), &loginfo); + + if (S_ISLNK(loginfo.st_mode)) + return error("reflog for %s is a symlink", oldref); + + if (!resolve_ref(oldref, orig_sha1, 1, &flag)) + return error("refname %s not found", oldref); + + if (!is_refname_available(newref, oldref, get_packed_refs(), 0)) + return 1; + + if (!is_refname_available(newref, oldref, get_loose_refs(), 0)) + return 1; + + lock = lock_ref_sha1_basic(renamed_ref, NULL, NULL); + if (!lock) + return error("unable to lock %s", renamed_ref); + lock->force_write = 1; + if (write_ref_sha1(lock, orig_sha1, logmsg)) + return error("unable to save current sha1 in %s", renamed_ref); + + if (log && rename(git_path("logs/%s", oldref), git_path("tmp-renamed-log"))) + return error("unable to move logfile logs/%s to tmp-renamed-log: %s", + oldref, strerror(errno)); + + if (delete_ref(oldref, orig_sha1)) { + error("unable to delete old %s", oldref); + goto rollback; + } + + if (resolve_ref(newref, sha1, 1, &flag) && delete_ref(newref, sha1)) { + if (errno==EISDIR) { + if (remove_empty_directories(git_path("%s", newref))) { + error("Directory not empty: %s", newref); + goto rollback; + } + } else { + error("unable to delete existing %s", newref); + goto rollback; + } + } + + if (log && safe_create_leading_directories(git_path("logs/%s", newref))) { + error("unable to create directory for %s", newref); + goto rollback; + } + + retry: + if (log && rename(git_path("tmp-renamed-log"), git_path("logs/%s", newref))) { + if (errno==EISDIR || errno==ENOTDIR) { + /* + * rename(a, b) when b is an existing + * directory ought to result in ISDIR, but + * Solaris 5.8 gives ENOTDIR. Sheesh. + */ + if (remove_empty_directories(git_path("logs/%s", newref))) { + error("Directory not empty: logs/%s", newref); + goto rollback; + } + goto retry; + } else { + error("unable to move logfile tmp-renamed-log to logs/%s: %s", + newref, strerror(errno)); + goto rollback; + } + } + logmoved = log; + + lock = lock_ref_sha1_basic(newref, NULL, NULL); + if (!lock) { + error("unable to lock %s for update", newref); + goto rollback; + } + + lock->force_write = 1; + hashcpy(lock->old_sha1, orig_sha1); + if (write_ref_sha1(lock, orig_sha1, logmsg)) { + error("unable to write current sha1 into %s", newref); + goto rollback; + } + + if (!strncmp(oldref, "refs/heads/", 11) && + !strncmp(newref, "refs/heads/", 11)) { + char oldsection[1024], newsection[1024]; + + snprintf(oldsection, 1024, "branch.%s", oldref + 11); + snprintf(newsection, 1024, "branch.%s", newref + 11); + if (git_config_rename_section(oldsection, newsection) < 0) + return 1; + } + + return 0; + + rollback: + lock = lock_ref_sha1_basic(oldref, NULL, NULL); + if (!lock) { + error("unable to lock %s for rollback", oldref); + goto rollbacklog; + } + + lock->force_write = 1; + flag = log_all_ref_updates; + log_all_ref_updates = 0; + if (write_ref_sha1(lock, orig_sha1, NULL)) + error("unable to write current sha1 into %s", oldref); + log_all_ref_updates = flag; + + rollbacklog: + if (logmoved && rename(git_path("logs/%s", newref), git_path("logs/%s", oldref))) + error("unable to restore logfile %s from %s: %s", + oldref, newref, strerror(errno)); + if (!logmoved && log && + rename(git_path("tmp-renamed-log"), git_path("logs/%s", oldref))) + error("unable to restore logfile %s from tmp-renamed-log: %s", + oldref, strerror(errno)); + + return 1; +} + +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); + } + free(lock->ref_name); + free(lock->orig_ref_name); + free(lock); +} + +static int log_ref_write(const char *ref_name, const unsigned char *old_sha1, + const unsigned char *new_sha1, const char *msg) +{ + int logfd, written, oflags = O_APPEND | O_WRONLY; + unsigned maxlen, len; + int msglen; + char *log_file, *logrec; + const char *committer; + + if (log_all_ref_updates < 0) + log_all_ref_updates = !is_bare_repository(); + + log_file = git_path("logs/%s", ref_name); + + if (log_all_ref_updates && + (!strncmp(ref_name, "refs/heads/", 11) || + !strncmp(ref_name, "refs/remotes/", 13) || + !strcmp(ref_name, "HEAD"))) { + if (safe_create_leading_directories(log_file) < 0) + return error("unable to create directory for %s", + log_file); + oflags |= O_CREAT; + } + + logfd = open(log_file, oflags, 0666); + if (logfd < 0) { + if (!(oflags & O_CREAT) && errno == ENOENT) + return 0; + + if ((oflags & O_CREAT) && errno == EISDIR) { + if (remove_empty_directories(log_file)) { + return error("There are still logs under '%s'", + log_file); + } + logfd = open(log_file, oflags, 0666); + } + + if (logfd < 0) + return error("Unable to append to %s: %s", + log_file, strerror(errno)); + } + + msglen = 0; + if (msg) { + /* clean up the message and make sure it is a single line */ + for ( ; *msg; msg++) + if (!isspace(*msg)) + break; + if (*msg) { + const char *ep = strchr(msg, '\n'); + if (ep) + msglen = ep - msg; + else + msglen = strlen(msg); + } + } + + committer = git_committer_info(-1); + maxlen = strlen(committer) + msglen + 100; + logrec = xmalloc(maxlen); + len = sprintf(logrec, "%s %s %s\n", + sha1_to_hex(old_sha1), + sha1_to_hex(new_sha1), + committer); + if (msglen) + len += sprintf(logrec + len - 1, "\t%.*s\n", msglen, msg) - 1; + written = len <= maxlen ? write_in_full(logfd, logrec, len) : -1; + free(logrec); + close(logfd); + if (written != len) + return error("Unable to append to %s", 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; + if (!lock->force_write && !hashcmp(lock->old_sha1, sha1)) { + unlock_ref(lock); + return 0; + } + if (write_in_full(lock->lock_fd, sha1_to_hex(sha1), 40) != 40 || + write_in_full(lock->lock_fd, &term, 1) != 1 + || close(lock->lock_fd) < 0) { + error("Couldn't write %s", lock->lk->filename); + unlock_ref(lock); + return -1; + } + invalidate_cached_refs(); + if (log_ref_write(lock->ref_name, lock->old_sha1, sha1, logmsg) < 0 || + (strcmp(lock->ref_name, lock->orig_ref_name) && + log_ref_write(lock->orig_ref_name, lock->old_sha1, sha1, logmsg) < 0)) { + unlock_ref(lock); + return -1; + } + if (commit_lock_file(lock->lk)) { + error("Couldn't set %s", lock->ref_name); + unlock_ref(lock); + return -1; + } + lock->lock_fd = -1; + unlock_ref(lock); + return 0; +} + +int create_symref(const char *ref_target, const char *refs_heads_master, + const char *logmsg) +{ + const char *lockpath; + char ref[1000]; + int fd, len, written; + char *git_HEAD = xstrdup(git_path("%s", ref_target)); + unsigned char old_sha1[20], new_sha1[20]; + + if (logmsg && read_ref(ref_target, old_sha1)) + hashclr(old_sha1); + + if (safe_create_leading_directories(git_HEAD) < 0) + return error("unable to create directory for %s", git_HEAD); + +#ifndef NO_SYMLINK_HEAD + if (prefer_symlink_refs) { + unlink(git_HEAD); + if (!symlink(refs_heads_master, git_HEAD)) + goto done; + fprintf(stderr, "no symlink - falling back to symbolic ref\n"); + } +#endif + + len = snprintf(ref, sizeof(ref), "ref: %s\n", refs_heads_master); + if (sizeof(ref) <= len) { + error("refname too long: %s", refs_heads_master); + goto error_free_return; + } + lockpath = mkpath("%s.lock", git_HEAD); + fd = open(lockpath, O_CREAT | O_EXCL | O_WRONLY, 0666); + if (fd < 0) { + error("Unable to open %s for writing", lockpath); + goto error_free_return; + } + written = write_in_full(fd, ref, len); + close(fd); + if (written != len) { + error("Unable to write to %s", lockpath); + goto error_unlink_return; + } + if (rename(lockpath, git_HEAD) < 0) { + error("Unable to create %s", git_HEAD); + goto error_unlink_return; + } + if (adjust_shared_perm(git_HEAD)) { + error("Unable to fix permissions on %s", lockpath); + error_unlink_return: + unlink(lockpath); + error_free_return: + free(git_HEAD); + return -1; + } + + done: + if (logmsg && !read_ref(refs_heads_master, new_sha1)) + log_ref_write(ref_target, old_sha1, new_sha1, logmsg); + + free(git_HEAD); + return 0; +} + +static char *ref_msg(const char *line, const char *endp) +{ + const char *ep; + char *msg; + + line += 82; + for (ep = line; ep < endp && *ep != '\n'; ep++) + ; + msg = xmalloc(ep - line + 1); + memcpy(msg, line, ep - line); + msg[ep - line] = 0; + return msg; +} + +int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *sha1, char **msg, unsigned long *cutoff_time, int *cutoff_tz, int *cutoff_cnt) +{ + const char *logfile, *logdata, *logend, *rec, *lastgt, *lastrec; + char *tz_c; + int logfd, tz, reccnt = 0; + struct stat st; + unsigned long date; + unsigned char logged_sha1[20]; + void *log_mapped; + + 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); + log_mapped = xmmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, logfd, 0); + logdata = log_mapped; + close(logfd); + + lastrec = NULL; + rec = logend = logdata + st.st_size; + while (logdata < rec) { + reccnt++; + 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 || cnt == 0) { + tz = strtoul(tz_c, NULL, 10); + if (msg) + *msg = ref_msg(rec, logend); + if (cutoff_time) + *cutoff_time = date; + if (cutoff_tz) + *cutoff_tz = tz; + if (cutoff_cnt) + *cutoff_cnt = reccnt - 1; + 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 (hashcmp(logged_sha1, sha1)) { + 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 (hashcmp(logged_sha1, sha1)) { + fprintf(stderr, + "warning: Log %s unexpectedly ended on %s.\n", + logfile, show_rfc2822_date(date, tz)); + } + } + munmap(log_mapped, st.st_size); + return 0; + } + lastrec = rec; + if (cnt > 0) + cnt--; + } + + 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); + if (msg) + *msg = ref_msg(logdata, logend); + munmap(log_mapped, st.st_size); + + if (cutoff_time) + *cutoff_time = date; + if (cutoff_tz) + *cutoff_tz = tz; + if (cutoff_cnt) + *cutoff_cnt = reccnt; + return 1; +} + +int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data) +{ + const char *logfile; + FILE *logfp; + char buf[1024]; + int ret = 0; + + logfile = git_path("logs/%s", ref); + logfp = fopen(logfile, "r"); + if (!logfp) + return -1; + while (fgets(buf, sizeof(buf), logfp)) { + unsigned char osha1[20], nsha1[20]; + char *email_end, *message; + unsigned long timestamp; + int len, tz; + + /* old SP new SP name <email> SP time TAB msg LF */ + len = strlen(buf); + if (len < 83 || buf[len-1] != '\n' || + get_sha1_hex(buf, osha1) || buf[40] != ' ' || + get_sha1_hex(buf + 41, nsha1) || buf[81] != ' ' || + !(email_end = strchr(buf + 82, '>')) || + email_end[1] != ' ' || + !(timestamp = strtoul(email_end + 2, &message, 10)) || + !message || message[0] != ' ' || + (message[1] != '+' && message[1] != '-') || + !isdigit(message[2]) || !isdigit(message[3]) || + !isdigit(message[4]) || !isdigit(message[5])) + continue; /* corrupt? */ + email_end[1] = '\0'; + tz = strtol(message + 1, NULL, 10); + if (message[6] != '\t') + message += 6; + else + message += 7; + ret = fn(osha1, nsha1, buf+82, timestamp, tz, message, cb_data); + if (ret) + break; + } + fclose(logfp); + return ret; +} + +static int do_for_each_reflog(const char *base, each_ref_fn fn, void *cb_data) +{ + DIR *dir = opendir(git_path("logs/%s", base)); + int retval = 0; + + if (dir) { + struct dirent *de; + int baselen = strlen(base); + char *log = xmalloc(baselen + 257); + + memcpy(log, base, baselen); + if (baselen && base[baselen-1] != '/') + log[baselen++] = '/'; + + while ((de = readdir(dir)) != NULL) { + struct stat st; + int namelen; + + if (de->d_name[0] == '.') + continue; + namelen = strlen(de->d_name); + if (namelen > 255) + continue; + if (has_extension(de->d_name, ".lock")) + continue; + memcpy(log + baselen, de->d_name, namelen+1); + if (stat(git_path("logs/%s", log), &st) < 0) + continue; + if (S_ISDIR(st.st_mode)) { + retval = do_for_each_reflog(log, fn, cb_data); + } else { + unsigned char sha1[20]; + if (!resolve_ref(log, sha1, 0, NULL)) + retval = error("bad ref for %s", log); + else + retval = fn(log, sha1, 0, cb_data); + } + if (retval) + break; + } + free(log); + closedir(dir); + } + else + return errno; + return retval; +} + +int for_each_reflog(each_ref_fn fn, void *cb_data) +{ + return do_for_each_reflog("", fn, cb_data); +} diff --git a/refs.h b/refs.h new file mode 100644 index 0000000000..acedffc0e4 --- /dev/null +++ b/refs.h @@ -0,0 +1,63 @@ +#ifndef REFS_H +#define REFS_H + +struct ref_lock { + char *ref_name; + char *orig_ref_name; + struct lock_file *lk; + unsigned char old_sha1[20]; + int lock_fd; + int force_write; +}; + +#define REF_ISSYMREF 01 +#define REF_ISPACKED 02 + +/* + * Calls the specified function for each ref file until it returns nonzero, + * and returns the value + */ +typedef int each_ref_fn(const char *refname, const unsigned char *sha1, int flags, void *cb_data); +extern int head_ref(each_ref_fn, void *); +extern int for_each_ref(each_ref_fn, void *); +extern int for_each_tag_ref(each_ref_fn, void *); +extern int for_each_branch_ref(each_ref_fn, void *); +extern int for_each_remote_ref(each_ref_fn, void *); + +extern int peel_ref(const char *, unsigned char *); + +/** Reads the refs file specified into sha1 **/ +extern int get_ref_sha1(const char *ref, unsigned char *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); + +/** Locks any ref (for 'HEAD' type refs). */ +extern struct ref_lock *lock_any_ref_for_update(const char *ref, const unsigned char *old_sha1); + +/** Release any lock taken but not written. **/ +extern void unlock_ref(struct ref_lock *lock); + +/** 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); + +/** Reads log for the value of ref during at_time. **/ +extern int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *sha1, char **msg, unsigned long *cutoff_time, int *cutoff_tz, int *cutoff_cnt); + +/* iterate over reflog entries */ +typedef int each_reflog_ent_fn(unsigned char *osha1, unsigned char *nsha1, const char *, unsigned long, int, const char *, void *); +int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data); + +/* + * Calls the specified function for each reflog file until it returns nonzero, + * and returns the value + */ +extern int for_each_reflog(each_ref_fn, void *); + +/** Returns 0 if target has the right format for a ref. **/ +extern int check_ref_format(const char *target); + +/** rename ref, return 0 on success **/ +extern int rename_ref(const char *oldref, const char *newref, const char *logmsg); + +#endif /* REFS_H */ diff --git a/revision.c b/revision.c new file mode 100644 index 0000000000..5bcd155e21 --- /dev/null +++ b/revision.c @@ -0,0 +1,1308 @@ +#include "cache.h" +#include "tag.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "diff.h" +#include "refs.h" +#include "revision.h" +#include "grep.h" +#include "reflog-walk.h" + +static char *path_name(struct name_path *path, const char *name) +{ + struct name_path *p; + char *n, *m; + int nlen = strlen(name); + int len = nlen + 1; + + for (p = path; p; p = p->up) { + if (p->elem_len) + len += p->elem_len + 1; + } + n = xmalloc(len); + m = n + len - (nlen + 1); + strcpy(m, name); + for (p = path; p; p = p->up) { + if (p->elem_len) { + m -= p->elem_len + 1; + memcpy(m, p->elem, p->elem_len); + m[p->elem_len] = '/'; + } + } + return n; +} + +void add_object(struct object *obj, + struct object_array *p, + struct name_path *path, + const char *name) +{ + add_object_array(obj, path_name(path, name), p); +} + +static void mark_blob_uninteresting(struct blob *blob) +{ + if (blob->object.flags & UNINTERESTING) + return; + blob->object.flags |= UNINTERESTING; +} + +void mark_tree_uninteresting(struct tree *tree) +{ + struct tree_desc desc; + struct name_entry entry; + struct object *obj = &tree->object; + + if (obj->flags & UNINTERESTING) + return; + obj->flags |= UNINTERESTING; + if (!has_sha1_file(obj->sha1)) + return; + if (parse_tree(tree) < 0) + die("bad tree %s", sha1_to_hex(obj->sha1)); + + 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(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) +{ + struct commit_list *parents = commit->parents; + + while (parents) { + struct commit *commit = parents->item; + if (!(commit->object.flags & UNINTERESTING)) { + commit->object.flags |= UNINTERESTING; + + /* + * Normally we haven't parsed the parent + * yet, so we won't have a parent of a parent + * here. However, it may turn out that we've + * reached this commit some other way (where it + * wasn't uninteresting), in which case we need + * to mark its parents recursively too.. + */ + if (commit->parents) + mark_parents_uninteresting(commit); + } + + /* + * A missing commit is ok iff its parent is marked + * uninteresting. + * + * We just mark such a thing parsed, so that when + * it is popped next time around, we won't be trying + * to parse it and get an error. + */ + if (!has_sha1_file(commit->object.sha1)) + commit->object.parsed = 1; + parents = parents->next; + } +} + +void add_pending_object(struct rev_info *revs, struct object *obj, const char *name) +{ + add_object_array(obj, name, &revs->pending); + if (revs->reflog_info && obj->type == OBJ_COMMIT) + add_reflog_for_walk(revs->reflog_info, + (struct commit *)obj, name); +} + +static struct object *get_reference(struct rev_info *revs, const char *name, const unsigned char *sha1, unsigned int flags) +{ + struct object *object; + + object = parse_object(sha1); + if (!object) + die("bad object %s", name); + object->flags |= flags; + return object; +} + +static struct commit *handle_commit(struct rev_info *revs, struct object *object, const char *name) +{ + unsigned long flags = object->flags; + + /* + * Tag object? Look what it points to.. + */ + while (object->type == OBJ_TAG) { + struct tag *tag = (struct tag *) object; + if (revs->tag_objects && !(flags & UNINTERESTING)) + add_pending_object(revs, object, tag->tag); + object = parse_object(tag->tagged->sha1); + if (!object) + die("bad object %s", sha1_to_hex(tag->tagged->sha1)); + } + + /* + * Commit object? Just return it, we'll do all the complex + * reachability crud. + */ + if (object->type == OBJ_COMMIT) { + struct commit *commit = (struct commit *)object; + if (parse_commit(commit) < 0) + die("unable to parse commit %s", name); + if (flags & UNINTERESTING) { + commit->object.flags |= UNINTERESTING; + mark_parents_uninteresting(commit); + revs->limited = 1; + } + return commit; + } + + /* + * Tree object? Either mark it uniniteresting, or add it + * to the list of objects to look at later.. + */ + if (object->type == OBJ_TREE) { + struct tree *tree = (struct tree *)object; + if (!revs->tree_objects) + return NULL; + if (flags & UNINTERESTING) { + mark_tree_uninteresting(tree); + return NULL; + } + add_pending_object(revs, object, ""); + return NULL; + } + + /* + * Blob object? You know the drill by now.. + */ + if (object->type == OBJ_BLOB) { + struct blob *blob = (struct blob *)object; + if (!revs->blob_objects) + return NULL; + if (flags & UNINTERESTING) { + mark_blob_uninteresting(blob); + return NULL; + } + add_pending_object(revs, object, ""); + return NULL; + } + die("%s is unknown object", name); +} + +static int everybody_uninteresting(struct commit_list *orig) +{ + struct commit_list *list = orig; + while (list) { + struct commit *commit = list->item; + list = list->next; + if (commit->object.flags & UNINTERESTING) + continue; + return 0; + } + return 1; +} + +static int tree_difference = REV_TREE_SAME; + +static void file_add_remove(struct diff_options *options, + int addremove, unsigned mode, + const unsigned char *sha1, + const char *base, const char *path) +{ + int diff = REV_TREE_DIFFERENT; + + /* + * Is it an add of a new file? It means that the old tree + * didn't have it at all, so we will turn "REV_TREE_SAME" -> + * "REV_TREE_NEW", but leave any "REV_TREE_DIFFERENT" alone + * (and if it already was "REV_TREE_NEW", we'll keep it + * "REV_TREE_NEW" of course). + */ + if (addremove == '+') { + diff = tree_difference; + if (diff != REV_TREE_SAME) + return; + diff = REV_TREE_NEW; + } + tree_difference = diff; +} + +static void file_change(struct diff_options *options, + unsigned old_mode, unsigned new_mode, + const unsigned char *old_sha1, + const unsigned char *new_sha1, + const char *base, const char *path) +{ + tree_difference = REV_TREE_DIFFERENT; +} + +int rev_compare_tree(struct rev_info *revs, struct tree *t1, struct tree *t2) +{ + if (!t1) + return REV_TREE_NEW; + if (!t2) + return REV_TREE_DIFFERENT; + tree_difference = REV_TREE_SAME; + if (diff_tree_sha1(t1->object.sha1, t2->object.sha1, "", + &revs->pruning) < 0) + return REV_TREE_DIFFERENT; + return tree_difference; +} + +int rev_same_tree_as_empty(struct rev_info *revs, struct tree *t1) +{ + int retval; + void *tree; + struct tree_desc empty, real; + + if (!t1) + return 0; + + tree = read_object_with_reference(t1->object.sha1, tree_type, &real.size, NULL); + if (!tree) + return 0; + real.buf = tree; + + empty.buf = ""; + empty.size = 0; + + tree_difference = 0; + retval = diff_tree(&empty, &real, "", &revs->pruning); + free(tree); + + return retval >= 0 && !tree_difference; +} + +static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) +{ + struct commit_list **pp, *parent; + int tree_changed = 0, tree_same = 0; + + if (!commit->tree) + return; + + if (!commit->parents) { + if (!rev_same_tree_as_empty(revs, commit->tree)) + commit->object.flags |= TREECHANGE; + return; + } + + pp = &commit->parents; + while ((parent = *pp) != NULL) { + struct commit *p = parent->item; + + parse_commit(p); + switch (rev_compare_tree(revs, p->tree, commit->tree)) { + case REV_TREE_SAME: + tree_same = 1; + 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 + * to lose the other branches of this + * merge, so we just keep going. + */ + pp = &parent->next; + continue; + } + parent->next = NULL; + commit->parents = parent; + return; + + case REV_TREE_NEW: + if (revs->remove_empty_trees && + rev_same_tree_as_empty(revs, p->tree)) { + /* We are adding all the specified + * paths from this parent, so the + * history beyond this parent is not + * interesting. Remove its parents + * (they are grandparents for us). + * IOW, we pretend this parent is a + * "root" commit. + */ + parse_commit(p); + p->parents = NULL; + } + /* fallthrough */ + case REV_TREE_DIFFERENT: + tree_changed = 1; + pp = &parent->next; + continue; + } + die("bad tree compare for commit %s", sha1_to_hex(commit->object.sha1)); + } + if (tree_changed && !tree_same) + commit->object.flags |= TREECHANGE; +} + +static void add_parents_to_list(struct rev_info *revs, struct commit *commit, struct commit_list **list) +{ + struct commit_list *parent = commit->parents; + unsigned left_flag; + + if (commit->object.flags & ADDED) + return; + commit->object.flags |= ADDED; + + /* + * If the commit is uninteresting, don't try to + * prune parents - we want the maximal uninteresting + * set. + * + * Normally we haven't parsed the parent + * yet, so we won't have a parent of a parent + * here. However, it may turn out that we've + * reached this commit some other way (where it + * wasn't uninteresting), in which case we need + * to mark its parents recursively too.. + */ + if (commit->object.flags & UNINTERESTING) { + while (parent) { + struct commit *p = parent->item; + parent = parent->next; + parse_commit(p); + p->object.flags |= UNINTERESTING; + if (p->parents) + mark_parents_uninteresting(p); + if (p->object.flags & SEEN) + continue; + p->object.flags |= SEEN; + insert_by_date(p, list); + } + return; + } + + /* + * Ok, the commit wasn't uninteresting. Try to + * simplify the commit history and find the parent + * that has no differences in the path set if one exists. + */ + if (revs->prune_fn) + revs->prune_fn(revs, commit); + + if (revs->no_walk) + return; + + left_flag = (commit->object.flags & SYMMETRIC_LEFT); + parent = commit->parents; + while (parent) { + struct commit *p = parent->item; + + parent = parent->next; + + parse_commit(p); + p->object.flags |= left_flag; + if (p->object.flags & SEEN) + continue; + p->object.flags |= SEEN; + insert_by_date(p, list); + } +} + +static void limit_list(struct rev_info *revs) +{ + struct commit_list *list = revs->commits; + struct commit_list *newlist = NULL; + struct commit_list **p = &newlist; + + while (list) { + struct commit_list *entry = list; + struct commit *commit = list->item; + struct object *obj = &commit->object; + + list = list->next; + free(entry); + + if (revs->max_age != -1 && (commit->date < revs->max_age)) + obj->flags |= UNINTERESTING; + add_parents_to_list(revs, commit, &list); + if (obj->flags & UNINTERESTING) { + mark_parents_uninteresting(commit); + if (everybody_uninteresting(list)) + break; + continue; + } + if (revs->min_age != -1 && (commit->date > revs->min_age)) + continue; + p = &commit_list_insert(commit, p)->next; + } + if (revs->boundary) { + /* mark the ones that are on the result list first */ + for (list = newlist; list; list = list->next) { + struct commit *commit = list->item; + commit->object.flags |= TMP_MARK; + } + for (list = newlist; list; list = list->next) { + struct commit *commit = list->item; + struct object *obj = &commit->object; + struct commit_list *parent; + if (obj->flags & UNINTERESTING) + continue; + for (parent = commit->parents; + parent; + parent = parent->next) { + struct commit *pcommit = parent->item; + if (!(pcommit->object.flags & UNINTERESTING)) + continue; + pcommit->object.flags |= BOUNDARY; + if (pcommit->object.flags & TMP_MARK) + continue; + pcommit->object.flags |= TMP_MARK; + p = &commit_list_insert(pcommit, p)->next; + } + } + for (list = newlist; list; list = list->next) { + struct commit *commit = list->item; + commit->object.flags &= ~TMP_MARK; + } + } + revs->commits = newlist; +} + +struct all_refs_cb { + int all_flags; + int warned_bad_reflog; + struct rev_info *all_revs; + const char *name_for_errormsg; +}; + +static int handle_one_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + struct all_refs_cb *cb = cb_data; + struct object *object = get_reference(cb->all_revs, path, sha1, + cb->all_flags); + add_pending_object(cb->all_revs, object, ""); + return 0; +} + +static void handle_all(struct rev_info *revs, unsigned flags) +{ + struct all_refs_cb cb; + cb.all_revs = revs; + cb.all_flags = flags; + for_each_ref(handle_one_ref, &cb); +} + +static void handle_one_reflog_commit(unsigned char *sha1, void *cb_data) +{ + struct all_refs_cb *cb = cb_data; + if (!is_null_sha1(sha1)) { + struct object *o = parse_object(sha1); + if (o) { + o->flags |= cb->all_flags; + add_pending_object(cb->all_revs, o, ""); + } + else if (!cb->warned_bad_reflog) { + warn("reflog of '%s' references pruned commits", + cb->name_for_errormsg); + cb->warned_bad_reflog = 1; + } + } +} + +static int handle_one_reflog_ent(unsigned char *osha1, unsigned char *nsha1, + const char *email, unsigned long timestamp, int tz, + const char *message, void *cb_data) +{ + handle_one_reflog_commit(osha1, cb_data); + handle_one_reflog_commit(nsha1, cb_data); + return 0; +} + +static int handle_one_reflog(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + struct all_refs_cb *cb = cb_data; + cb->warned_bad_reflog = 0; + cb->name_for_errormsg = path; + for_each_reflog_ent(path, handle_one_reflog_ent, cb_data); + return 0; +} + +static void handle_reflog(struct rev_info *revs, unsigned flags) +{ + struct all_refs_cb cb; + cb.all_revs = revs; + cb.all_flags = flags; + for_each_ref(handle_one_reflog, &cb); +} + +static int add_parents_only(struct rev_info *revs, const char *arg, int flags) +{ + unsigned char sha1[20]; + struct object *it; + struct commit *commit; + struct commit_list *parents; + + if (*arg == '^') { + flags ^= UNINTERESTING; + arg++; + } + if (get_sha1(arg, sha1)) + return 0; + while (1) { + it = get_reference(revs, arg, sha1, 0); + if (it->type != OBJ_TAG) + break; + hashcpy(sha1, ((struct tag*)it)->tagged->sha1); + } + if (it->type != OBJ_COMMIT) + return 0; + commit = (struct commit *)it; + for (parents = commit->parents; parents; parents = parents->next) { + it = &parents->item->object; + it->flags |= flags; + add_pending_object(revs, it, arg); + } + return 1; +} + +void init_revisions(struct rev_info *revs, const char *prefix) +{ + memset(revs, 0, sizeof(*revs)); + + 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->lifo = 1; + revs->dense = 1; + revs->prefix = prefix; + revs->max_age = -1; + revs->min_age = -1; + revs->skip_count = -1; + revs->max_count = -1; + + revs->prune_fn = NULL; + revs->prune_data = NULL; + + revs->topo_setter = topo_sort_default_setter; + revs->topo_getter = topo_sort_default_getter; + + revs->commit_format = CMIT_FMT_DEFAULT; + + diff_setup(&revs->diffopt); +} + +static void add_pending_commit_list(struct rev_info *revs, + struct commit_list *commit_list, + unsigned int flags) +{ + while (commit_list) { + struct object *object = &commit_list->item->object; + object->flags |= flags; + add_pending_object(revs, object, sha1_to_hex(object->sha1)); + commit_list = commit_list->next; + } +} + +static void prepare_show_merge(struct rev_info *revs) +{ + struct commit_list *bases; + struct commit *head, *other; + unsigned char sha1[20]; + const char **prune = NULL; + int i, prune_num = 1; /* counting terminating NULL */ + + if (get_sha1("HEAD", sha1) || !(head = lookup_commit(sha1))) + die("--merge without HEAD?"); + if (get_sha1("MERGE_HEAD", sha1) || !(other = lookup_commit(sha1))) + die("--merge without MERGE_HEAD?"); + add_pending_object(revs, &head->object, "HEAD"); + add_pending_object(revs, &other->object, "MERGE_HEAD"); + bases = get_merge_bases(head, other, 1); + while (bases) { + struct commit *it = bases->item; + struct commit_list *n = bases->next; + free(bases); + bases = n; + it->object.flags |= UNINTERESTING; + add_pending_object(revs, &it->object, "(merge-base)"); + } + + if (!active_nr) + read_cache(); + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (!ce_stage(ce)) + continue; + if (ce_path_match(ce, revs->prune_data)) { + prune_num++; + prune = xrealloc(prune, sizeof(*prune) * prune_num); + prune[prune_num-2] = ce->name; + prune[prune_num-1] = NULL; + } + while ((i+1 < active_nr) && + ce_same_name(ce, active_cache[i+1])) + i++; + } + revs->prune_data = prune; +} + +int handle_revision_arg(const char *arg, struct rev_info *revs, + int flags, + int cant_be_filename) +{ + char *dotdot; + struct object *object; + unsigned char sha1[20]; + int local_flags; + + dotdot = strstr(arg, ".."); + if (dotdot) { + unsigned char from_sha1[20]; + const char *next = dotdot + 2; + const char *this = arg; + int symmetric = *next == '.'; + unsigned int flags_exclude = flags ^ UNINTERESTING; + + *dotdot = 0; + next += symmetric; + + if (!*next) + next = "HEAD"; + if (dotdot == arg) + this = "HEAD"; + if (!get_sha1(this, from_sha1) && + !get_sha1(next, sha1)) { + struct commit *a, *b; + struct commit_list *exclude; + + a = lookup_commit_reference(from_sha1); + b = lookup_commit_reference(sha1); + if (!a || !b) { + die(symmetric ? + "Invalid symmetric difference expression %s...%s" : + "Invalid revision range %s..%s", + arg, next); + } + + if (!cant_be_filename) { + *dotdot = '.'; + verify_non_filename(revs->prefix, arg); + } + + if (symmetric) { + exclude = get_merge_bases(a, b, 1); + add_pending_commit_list(revs, exclude, + flags_exclude); + free_commit_list(exclude); + a->object.flags |= flags | SYMMETRIC_LEFT; + } else + a->object.flags |= flags_exclude; + b->object.flags |= flags; + add_pending_object(revs, &a->object, this); + add_pending_object(revs, &b->object, next); + return 0; + } + *dotdot = '.'; + } + dotdot = strstr(arg, "^@"); + if (dotdot && !dotdot[2]) { + *dotdot = 0; + if (add_parents_only(revs, arg, flags)) + return 0; + *dotdot = '^'; + } + dotdot = strstr(arg, "^!"); + if (dotdot && !dotdot[2]) { + *dotdot = 0; + if (!add_parents_only(revs, arg, flags ^ UNINTERESTING)) + *dotdot = '^'; + } + + local_flags = 0; + if (*arg == '^') { + local_flags = UNINTERESTING; + arg++; + } + if (get_sha1(arg, sha1)) + return -1; + if (!cant_be_filename) + verify_non_filename(revs->prefix, arg); + object = get_reference(revs, arg, sha1, flags ^ local_flags); + add_pending_object(revs, object, arg); + return 0; +} + +static void add_grep(struct rev_info *revs, const char *ptn, enum grep_pat_token what) +{ + if (!revs->grep_filter) { + struct grep_opt *opt = xcalloc(1, sizeof(*opt)); + opt->status_only = 1; + opt->pattern_tail = &(opt->pattern_list); + opt->regflags = REG_NEWLINE; + revs->grep_filter = opt; + } + append_grep_pattern(revs->grep_filter, ptn, + "command line", 0, what); +} + +static void add_header_grep(struct rev_info *revs, const char *field, const char *pattern) +{ + char *pat; + const char *prefix; + int patlen, fldlen; + + fldlen = strlen(field); + patlen = strlen(pattern); + pat = xmalloc(patlen + fldlen + 10); + prefix = ".*"; + if (*pattern == '^') { + prefix = ""; + pattern++; + } + sprintf(pat, "^%s %s%s", field, prefix, pattern); + add_grep(revs, pat, GREP_PATTERN_HEAD); +} + +static void add_message_grep(struct rev_info *revs, const char *pattern) +{ + add_grep(revs, pattern, GREP_PATTERN_BODY); +} + +static void add_ignore_packed(struct rev_info *revs, const char *name) +{ + int num = ++revs->num_ignore_packed; + + revs->ignore_packed = xrealloc(revs->ignore_packed, + sizeof(const char **) * (num + 1)); + revs->ignore_packed[num-1] = name; + revs->ignore_packed[num] = NULL; +} + +/* + * Parse revision information, filling in the "rev_info" structure, + * and removing the used arguments from the argument list. + * + * Returns the number of arguments left that weren't recognized + * (which are also moved to the head of the argument list) + */ +int setup_revisions(int argc, const char **argv, struct rev_info *revs, const char *def) +{ + int i, flags, seen_dashdash, show_merge; + const char **unrecognized = argv + 1; + int left = 1; + int all_match = 0; + + /* First, search for "--" */ + seen_dashdash = 0; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (strcmp(arg, "--")) + continue; + argv[i] = NULL; + argc = i; + revs->prune_data = get_pathspec(revs->prefix, argv + i + 1); + seen_dashdash = 1; + break; + } + + flags = show_merge = 0; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (*arg == '-') { + int opts; + if (!strncmp(arg, "--max-count=", 12)) { + revs->max_count = atoi(arg + 12); + continue; + } + if (!strncmp(arg, "--skip=", 7)) { + revs->skip_count = atoi(arg + 7); + continue; + } + /* accept -<digit>, like traditional "head" */ + if ((*arg == '-') && isdigit(arg[1])) { + revs->max_count = atoi(arg + 1); + continue; + } + if (!strcmp(arg, "-n")) { + if (argc <= i + 1) + die("-n requires an argument"); + revs->max_count = atoi(argv[++i]); + continue; + } + if (!strncmp(arg,"-n",2)) { + revs->max_count = atoi(arg + 2); + continue; + } + if (!strncmp(arg, "--max-age=", 10)) { + revs->max_age = atoi(arg + 10); + continue; + } + if (!strncmp(arg, "--since=", 8)) { + revs->max_age = approxidate(arg + 8); + continue; + } + if (!strncmp(arg, "--after=", 8)) { + revs->max_age = approxidate(arg + 8); + continue; + } + if (!strncmp(arg, "--min-age=", 10)) { + revs->min_age = atoi(arg + 10); + continue; + } + if (!strncmp(arg, "--before=", 9)) { + revs->min_age = approxidate(arg + 9); + continue; + } + if (!strncmp(arg, "--until=", 8)) { + revs->min_age = approxidate(arg + 8); + continue; + } + if (!strcmp(arg, "--all")) { + handle_all(revs, flags); + continue; + } + if (!strcmp(arg, "--reflog")) { + handle_reflog(revs, flags); + continue; + } + if (!strcmp(arg, "-g") || + !strcmp(arg, "--walk-reflogs")) { + init_reflog_walk(&revs->reflog_info); + continue; + } + if (!strcmp(arg, "--not")) { + flags ^= UNINTERESTING; + continue; + } + if (!strcmp(arg, "--default")) { + if (++i >= argc) + die("bad --default argument"); + def = argv[i]; + continue; + } + if (!strcmp(arg, "--merge")) { + show_merge = 1; + continue; + } + if (!strcmp(arg, "--topo-order")) { + revs->topo_order = 1; + continue; + } + if (!strcmp(arg, "--date-order")) { + revs->lifo = 0; + revs->topo_order = 1; + continue; + } + if (!strcmp(arg, "--parents")) { + revs->parents = 1; + continue; + } + if (!strcmp(arg, "--dense")) { + revs->dense = 1; + continue; + } + if (!strcmp(arg, "--sparse")) { + revs->dense = 0; + continue; + } + if (!strcmp(arg, "--remove-empty")) { + revs->remove_empty_trees = 1; + continue; + } + if (!strcmp(arg, "--no-merges")) { + revs->no_merges = 1; + continue; + } + if (!strcmp(arg, "--boundary")) { + revs->boundary = 1; + continue; + } + if (!strcmp(arg, "--left-right")) { + revs->left_right = 1; + continue; + } + if (!strcmp(arg, "--objects")) { + revs->tag_objects = 1; + revs->tree_objects = 1; + revs->blob_objects = 1; + continue; + } + if (!strcmp(arg, "--objects-edge")) { + revs->tag_objects = 1; + revs->tree_objects = 1; + revs->blob_objects = 1; + revs->edge_hint = 1; + continue; + } + if (!strcmp(arg, "--unpacked")) { + revs->unpacked = 1; + free(revs->ignore_packed); + revs->ignore_packed = NULL; + revs->num_ignore_packed = 0; + continue; + } + if (!strncmp(arg, "--unpacked=", 11)) { + revs->unpacked = 1; + add_ignore_packed(revs, arg+11); + continue; + } + if (!strcmp(arg, "-r")) { + revs->diff = 1; + revs->diffopt.recursive = 1; + continue; + } + if (!strcmp(arg, "-t")) { + revs->diff = 1; + revs->diffopt.recursive = 1; + revs->diffopt.tree_in_recursive = 1; + continue; + } + if (!strcmp(arg, "-m")) { + revs->ignore_merges = 0; + continue; + } + if (!strcmp(arg, "-c")) { + revs->diff = 1; + revs->dense_combined_merges = 0; + revs->combine_merges = 1; + continue; + } + if (!strcmp(arg, "--cc")) { + revs->diff = 1; + revs->dense_combined_merges = 1; + revs->combine_merges = 1; + continue; + } + if (!strcmp(arg, "-v")) { + revs->verbose_header = 1; + continue; + } + if (!strncmp(arg, "--pretty", 8)) { + revs->verbose_header = 1; + revs->commit_format = get_commit_format(arg+8); + continue; + } + if (!strcmp(arg, "--root")) { + revs->show_root_diff = 1; + continue; + } + if (!strcmp(arg, "--no-commit-id")) { + revs->no_commit_id = 1; + continue; + } + if (!strcmp(arg, "--always")) { + revs->always_show_header = 1; + continue; + } + if (!strcmp(arg, "--no-abbrev")) { + revs->abbrev = 0; + continue; + } + if (!strcmp(arg, "--abbrev")) { + 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; + } + if (!strcmp(arg, "--full-diff")) { + revs->diff = 1; + revs->full_diff = 1; + continue; + } + if (!strcmp(arg, "--full-history")) { + revs->simplify_history = 0; + continue; + } + if (!strcmp(arg, "--relative-date")) { + revs->relative_date = 1; + continue; + } + + /* + * Grepping the commit log + */ + if (!strncmp(arg, "--author=", 9)) { + add_header_grep(revs, "author", arg+9); + continue; + } + if (!strncmp(arg, "--committer=", 12)) { + add_header_grep(revs, "committer", arg+12); + continue; + } + if (!strncmp(arg, "--grep=", 7)) { + add_message_grep(revs, arg+7); + continue; + } + if (!strcmp(arg, "--all-match")) { + all_match = 1; + continue; + } + if (!strncmp(arg, "--encoding=", 11)) { + arg += 11; + if (strcmp(arg, "none")) + git_log_output_encoding = strdup(arg); + else + git_log_output_encoding = ""; + continue; + } + + opts = diff_opt_parse(&revs->diffopt, argv+i, argc-i); + if (opts > 0) { + revs->diff = 1; + i += opts - 1; + continue; + } + *unrecognized++ = arg; + left++; + continue; + } + + if (handle_revision_arg(arg, revs, flags, seen_dashdash)) { + int j; + if (seen_dashdash || *arg == '^') + die("bad revision '%s'", arg); + + /* If we didn't have a "--": + * (1) all filenames must exist; + * (2) all rev-args must not be interpretable + * as a valid filename. + * but the latter we have checked in the main loop. + */ + for (j = i; j < argc; j++) + verify_filename(revs->prefix, argv[j]); + + revs->prune_data = get_pathspec(revs->prefix, + argv + i); + break; + } + } + + if (show_merge) + prepare_show_merge(revs); + if (def && !revs->pending.nr) { + unsigned char sha1[20]; + struct object *object; + if (get_sha1(def, sha1)) + die("bad default revision '%s'", def); + object = get_reference(revs, def, sha1, 0); + add_pending_object(revs, object, def); + } + + if (revs->topo_order) + revs->limited = 1; + + if (revs->prune_data) { + diff_tree_setup_paths(revs->prune_data, &revs->pruning); + revs->prune_fn = try_to_simplify_commit; + if (!revs->full_diff) + diff_tree_setup_paths(revs->prune_data, &revs->diffopt); + } + if (revs->combine_merges) { + revs->ignore_merges = 0; + if (revs->dense_combined_merges && !revs->diffopt.output_format) + revs->diffopt.output_format = DIFF_FORMAT_PATCH; + } + revs->diffopt.abbrev = revs->abbrev; + if (diff_setup_done(&revs->diffopt) < 0) + die("diff_setup_done failed"); + + if (revs->grep_filter) { + revs->grep_filter->all_match = all_match; + compile_grep_patterns(revs->grep_filter); + } + + return left; +} + +void prepare_revision_walk(struct rev_info *revs) +{ + int nr = revs->pending.nr; + struct object_array_entry *e, *list; + + e = list = revs->pending.objects; + revs->pending.nr = 0; + revs->pending.alloc = 0; + revs->pending.objects = NULL; + while (--nr >= 0) { + struct commit *commit = handle_commit(revs, e->item, e->name); + if (commit) { + if (!(commit->object.flags & SEEN)) { + commit->object.flags |= SEEN; + insert_by_date(commit, &revs->commits); + } + } + e++; + } + free(list); + + if (revs->no_walk) + return; + if (revs->limited) + limit_list(revs); + if (revs->topo_order) + sort_in_topological_order_fn(&revs->commits, revs->lifo, + revs->topo_setter, + revs->topo_getter); +} + +static int rewrite_one(struct rev_info *revs, struct commit **pp) +{ + for (;;) { + struct commit *p = *pp; + if (!revs->limited) + add_parents_to_list(revs, p, &revs->commits); + if (p->parents && p->parents->next) + return 0; + if (p->object.flags & (TREECHANGE | UNINTERESTING)) + return 0; + if (!p->parents) + return -1; + *pp = p->parents->item; + } +} + +static void rewrite_parents(struct rev_info *revs, struct commit *commit) +{ + struct commit_list **pp = &commit->parents; + while (*pp) { + struct commit_list *parent = *pp; + if (rewrite_one(revs, &parent->item) < 0) { + *pp = parent->next; + continue; + } + pp = &parent->next; + } +} + +static void mark_boundary_to_show(struct commit *commit) +{ + struct commit_list *p = commit->parents; + while (p) { + commit = p->item; + p = p->next; + if (commit->object.flags & BOUNDARY) + commit->object.flags |= BOUNDARY_SHOW; + } +} + +static int commit_match(struct commit *commit, struct rev_info *opt) +{ + if (!opt->grep_filter) + return 1; + return grep_buffer(opt->grep_filter, + NULL, /* we say nothing, not even filename */ + commit->buffer, strlen(commit->buffer)); +} + +static struct commit *get_revision_1(struct rev_info *revs) +{ + if (!revs->commits) + return NULL; + + do { + struct commit_list *entry = revs->commits; + struct commit *commit = entry->item; + + revs->commits = entry->next; + free(entry); + + if (revs->reflog_info) + fake_reflog_parent(revs->reflog_info, commit); + + /* + * If we haven't done the list limiting, we need to look at + * the parents here. We also need to do the date-based limiting + * that we'd otherwise have done in limit_list(). + */ + if (!revs->limited) { + if (revs->max_age != -1 && + (commit->date < revs->max_age)) + continue; + add_parents_to_list(revs, commit, &revs->commits); + } + if (commit->object.flags & SHOWN) + continue; + + if (revs->unpacked && has_sha1_pack(commit->object.sha1, + revs->ignore_packed)) + continue; + + /* We want to show boundary commits only when their + * children are shown. When path-limiter is in effect, + * rewrite_parents() drops some commits from getting shown, + * and there is no point showing boundary parents that + * are not shown. After rewrite_parents() rewrites the + * parents of a commit that is shown, we mark the boundary + * parents with BOUNDARY_SHOW. + */ + if (commit->object.flags & BOUNDARY_SHOW) { + commit->object.flags |= SHOWN; + return commit; + } + if (commit->object.flags & UNINTERESTING) + continue; + if (revs->min_age != -1 && (commit->date > revs->min_age)) + continue; + if (revs->no_merges && + commit->parents && commit->parents->next) + continue; + if (!commit_match(commit, revs)) + continue; + if (revs->prune_fn && revs->dense) { + /* Commit without changes? */ + if (!(commit->object.flags & TREECHANGE)) { + /* drop merges unless we want parenthood */ + if (!revs->parents) + continue; + /* non-merge - always ignore it */ + if (!commit->parents || !commit->parents->next) + continue; + } + if (revs->parents) + rewrite_parents(revs, commit); + } + if (revs->boundary) + mark_boundary_to_show(commit); + commit->object.flags |= SHOWN; + return commit; + } while (revs->commits); + return NULL; +} + +struct commit *get_revision(struct rev_info *revs) +{ + struct commit *c = NULL; + + if (0 < revs->skip_count) { + while ((c = get_revision_1(revs)) != NULL) { + if (revs->skip_count-- <= 0) + break; + } + } + + /* Check the max_count ... */ + switch (revs->max_count) { + case -1: + break; + case 0: + return NULL; + default: + revs->max_count--; + } + if (c) + return c; + return get_revision_1(revs); +} diff --git a/revision.h b/revision.h new file mode 100644 index 0000000000..d93481f68f --- /dev/null +++ b/revision.h @@ -0,0 +1,127 @@ +#ifndef REVISION_H +#define REVISION_H + +#define SEEN (1u<<0) +#define UNINTERESTING (1u<<1) +#define TREECHANGE (1u<<2) +#define SHOWN (1u<<3) +#define TMP_MARK (1u<<4) /* for isolated cases; clean after use */ +#define BOUNDARY (1u<<5) +#define BOUNDARY_SHOW (1u<<6) +#define ADDED (1u<<7) /* Parents already parsed and added? */ +#define SYMMETRIC_LEFT (1u<<8) + +struct rev_info; +struct log_info; + +typedef void (prune_fn_t)(struct rev_info *revs, struct commit *commit); + +struct rev_info { + /* Starting list */ + struct commit_list *commits; + struct object_array pending; + + /* Basic information */ + const char *prefix; + void *prune_data; + prune_fn_t *prune_fn; + + /* Traversal flags */ + unsigned int dense:1, + no_merges:1, + no_walk:1, + remove_empty_trees:1, + simplify_history:1, + lifo:1, + topo_order:1, + tag_objects:1, + tree_objects:1, + blob_objects:1, + edge_hint:1, + limited:1, + unpacked:1, /* see also ignore_packed below */ + boundary:1, + left_right:1, + parents:1; + + /* Diff flags */ + unsigned int diff:1, + full_diff:1, + show_root_diff:1, + no_commit_id:1, + verbose_header:1, + ignore_merges:1, + combine_merges:1, + dense_combined_merges:1, + always_show_header:1; + + /* Format info */ + unsigned int shown_one:1, + abbrev_commit:1, + relative_date:1; + + const char **ignore_packed; /* pretend objects in these are unpacked */ + int num_ignore_packed; + + unsigned int abbrev; + enum cmit_fmt commit_format; + struct log_info *loginfo; + int nr, total; + const char *mime_boundary; + const char *message_id; + const char *ref_message_id; + const char *add_signoff; + const char *extra_headers; + const char *log_reencode; + + /* Filter by commit log message */ + struct grep_opt *grep_filter; + + /* special limits */ + int skip_count; + int max_count; + unsigned long max_age; + unsigned long min_age; + + /* diff info for patches and for paths limiting */ + struct diff_options diffopt; + struct diff_options pruning; + + topo_sort_set_fn_t topo_setter; + topo_sort_get_fn_t topo_getter; + + struct reflog_walk_info *reflog_info; +}; + +#define REV_TREE_SAME 0 +#define REV_TREE_NEW 1 +#define REV_TREE_DIFFERENT 2 + +/* revision.c */ +extern int rev_same_tree_as_empty(struct rev_info *, struct tree *t1); +extern int rev_compare_tree(struct rev_info *, struct tree *t1, struct tree *t2); + +extern void init_revisions(struct rev_info *revs, const char *prefix); +extern int setup_revisions(int argc, const char **argv, struct rev_info *revs, const char *def); +extern int handle_revision_arg(const char *arg, struct rev_info *revs,int flags,int cant_be_filename); + +extern void prepare_revision_walk(struct rev_info *revs); +extern struct commit *get_revision(struct rev_info *revs); + +extern void mark_parents_uninteresting(struct commit *commit); +extern void mark_tree_uninteresting(struct tree *tree); + +struct name_path { + struct name_path *up; + int elem_len; + const char *elem; +}; + +extern void add_object(struct object *obj, + struct object_array *p, + struct name_path *path, + const char *name); + +extern void add_pending_object(struct rev_info *revs, struct object *obj, const char *name); + +#endif @@ -0,0 +1,83 @@ +#include "cache.h" +#include "rsh.h" +#include "quote.h" + +#define COMMAND_SIZE 4096 + +int setup_connection(int *fd_in, int *fd_out, const char *remote_prog, + char *url, int rmt_argc, char **rmt_argv) +{ + char *host; + char *path; + int sv[2]; + char command[COMMAND_SIZE]; + char *posn; + int sizen; + int of; + int i; + pid_t pid; + + if (!strcmp(url, "-")) { + *fd_in = 0; + *fd_out = 1; + return 0; + } + + host = strstr(url, "//"); + if (host) { + host += 2; + path = strchr(host, '/'); + } else { + host = url; + path = strchr(host, ':'); + if (path) + *(path++) = '\0'; + } + if (!path) { + return error("Bad URL: %s", url); + } + /* $GIT_RSH <host> "env GIT_DIR=<path> <remote_prog> <args...>" */ + sizen = COMMAND_SIZE; + posn = command; + of = 0; + of |= add_to_string(&posn, &sizen, "env ", 0); + of |= add_to_string(&posn, &sizen, GIT_DIR_ENVIRONMENT "=", 0); + of |= add_to_string(&posn, &sizen, path, 1); + of |= add_to_string(&posn, &sizen, " ", 0); + of |= add_to_string(&posn, &sizen, remote_prog, 1); + + for ( i = 0 ; i < rmt_argc ; i++ ) { + of |= add_to_string(&posn, &sizen, " ", 0); + of |= add_to_string(&posn, &sizen, rmt_argv[i], 1); + } + + of |= add_to_string(&posn, &sizen, " -", 0); + + if ( of ) + return error("Command line too long"); + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv)) + return error("Couldn't create socket"); + + 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"; + ssh_basename = strrchr(ssh, '/'); + if (!ssh_basename) + ssh_basename = ssh; + else + ssh_basename++; + close(sv[1]); + dup2(sv[0], 0); + dup2(sv[0], 1); + execlp(ssh, ssh_basename, host, command, NULL); + } + close(sv[0]); + *fd_in = sv[1]; + *fd_out = sv[1]; + return 0; +} @@ -0,0 +1,7 @@ +#ifndef RSH_H +#define RSH_H + +int setup_connection(int *fd_in, int *fd_out, const char *remote_prog, + char *url, int rmt_argc, char **rmt_argv); + +#endif diff --git a/run-command.c b/run-command.c new file mode 100644 index 0000000000..cfbad74d14 --- /dev/null +++ b/run-command.c @@ -0,0 +1,93 @@ +#include "cache.h" +#include "run-command.h" +#include "exec_cmd.h" + +int run_command_v_opt(const char **argv, int flags) +{ + pid_t pid = fork(); + + if (pid < 0) + return -ERR_RUN_COMMAND_FORK; + if (!pid) { + if (flags & RUN_COMMAND_NO_STDIN) { + int fd = open("/dev/null", O_RDWR); + dup2(fd, 0); + close(fd); + } + if (flags & RUN_COMMAND_STDOUT_TO_STDERR) + dup2(2, 1); + if (flags & RUN_GIT_CMD) { + execv_git_cmd(argv); + } else { + execvp(argv[0], (char *const*) argv); + } + die("exec %s failed.", argv[0]); + } + for (;;) { + int status, code; + pid_t waiting = waitpid(pid, &status, 0); + + if (waiting < 0) { + if (errno == EINTR) + continue; + error("waitpid failed (%s)", strerror(errno)); + return -ERR_RUN_COMMAND_WAITPID; + } + if (waiting != pid) + return -ERR_RUN_COMMAND_WAITPID_WRONG_PID; + if (WIFSIGNALED(status)) + return -ERR_RUN_COMMAND_WAITPID_SIGNAL; + + if (!WIFEXITED(status)) + return -ERR_RUN_COMMAND_WAITPID_NOEXIT; + code = WEXITSTATUS(status); + if (code) + return -code; + return 0; + } +} + +int run_command_v(const char **argv) +{ + return run_command_v_opt(argv, 0); +} + +static int run_command_va_opt(int opt, const char *cmd, va_list param) +{ + int argc; + const char *argv[MAX_RUN_COMMAND_ARGS]; + const char *arg; + + argv[0] = (char*) cmd; + argc = 1; + while (argc < MAX_RUN_COMMAND_ARGS) { + arg = argv[argc++] = va_arg(param, char *); + if (!arg) + break; + } + if (MAX_RUN_COMMAND_ARGS <= argc) + return error("too many args to run %s", cmd); + return run_command_v_opt(argv, opt); +} + +int run_command_opt(int opt, const char *cmd, ...) +{ + va_list params; + int r; + + va_start(params, cmd); + r = run_command_va_opt(opt, cmd, params); + va_end(params); + return r; +} + +int run_command(const char *cmd, ...) +{ + va_list params; + int r; + + va_start(params, cmd); + r = run_command_va_opt(0, cmd, params); + va_end(params); + return r; +} diff --git a/run-command.h b/run-command.h new file mode 100644 index 0000000000..59c4476ced --- /dev/null +++ b/run-command.h @@ -0,0 +1,22 @@ +#ifndef RUN_COMMAND_H +#define RUN_COMMAND_H + +#define MAX_RUN_COMMAND_ARGS 256 +enum { + ERR_RUN_COMMAND_FORK = 10000, + ERR_RUN_COMMAND_EXEC, + ERR_RUN_COMMAND_WAITPID, + ERR_RUN_COMMAND_WAITPID_WRONG_PID, + ERR_RUN_COMMAND_WAITPID_SIGNAL, + ERR_RUN_COMMAND_WAITPID_NOEXIT, +}; + +#define RUN_COMMAND_NO_STDIN 1 +#define RUN_GIT_CMD 2 /*If this is to be git sub-command */ +#define RUN_COMMAND_STDOUT_TO_STDERR 4 +int run_command_v_opt(const char **argv, int opt); +int run_command_v(const char **argv); +int run_command_opt(int opt, const char *cmd, ...); +int run_command(const char *cmd, ...); + +#endif diff --git a/send-pack.c b/send-pack.c new file mode 100644 index 0000000000..33e69dbe18 --- /dev/null +++ b/send-pack.c @@ -0,0 +1,430 @@ +#include "cache.h" +#include "commit.h" +#include "tag.h" +#include "refs.h" +#include "pkt-line.h" +#include "exec_cmd.h" + +static const char send_pack_usage[] = +"git-send-pack [--all] [--force] [--receive-pack=<git-receive-pack>] [--verbose] [--thin] [<host>:]<directory> [<ref>...]\n" +" --all and explicit <ref> specification are mutually exclusive."; +static const char *receivepack = "git-receive-pack"; +static int verbose; +static int send_all; +static int force_update; +static int use_thin_pack; + +/* + * Make a pack stream and spit it out into file descriptor fd + */ +static int pack_objects(int fd, struct ref *refs) +{ + int pipe_fd[2]; + pid_t pid; + + if (pipe(pipe_fd) < 0) + return error("send-pack: pipe failed"); + pid = fork(); + if (pid < 0) + return error("send-pack: unable to fork git-pack-objects"); + if (!pid) { + /* + * The child becomes pack-objects --revs; we feed + * the revision parameters to it via its stdin and + * let its stdout go back to the other end. + */ + static const char *args[] = { + "pack-objects", + "--all-progress", + "--revs", + "--stdout", + NULL, + NULL, + }; + if (use_thin_pack) + args[4] = "--thin"; + dup2(pipe_fd[0], 0); + dup2(fd, 1); + close(pipe_fd[0]); + close(pipe_fd[1]); + close(fd); + execv_git_cmd(args); + die("git-pack-objects exec failed (%s)", strerror(errno)); + } + + /* + * We feed the pack-objects we just spawned with revision + * parameters by writing to the pipe. + */ + close(pipe_fd[0]); + close(fd); + + while (refs) { + char buf[42]; + + if (!is_null_sha1(refs->old_sha1) && + has_sha1_file(refs->old_sha1)) { + memcpy(buf + 1, sha1_to_hex(refs->old_sha1), 40); + buf[0] = '^'; + buf[41] = '\n'; + if (!write_or_whine(pipe_fd[1], buf, 42, + "send-pack: send refs")) + break; + } + if (!is_null_sha1(refs->new_sha1)) { + memcpy(buf, sha1_to_hex(refs->new_sha1), 40); + buf[40] = '\n'; + if (!write_or_whine(pipe_fd[1], buf, 41, + "send-pack: send refs")) + break; + } + refs = refs->next; + } + close(pipe_fd[1]); + + for (;;) { + int status, code; + pid_t waiting = waitpid(pid, &status, 0); + + if (waiting < 0) { + if (errno == EINTR) + continue; + return error("waitpid failed (%s)", strerror(errno)); + } + if ((waiting != pid) || WIFSIGNALED(status) || + !WIFEXITED(status)) + return error("pack-objects died with strange error"); + code = WEXITSTATUS(status); + if (code) + return -code; + return 0; + } +} + +static void unmark_and_free(struct commit_list *list, unsigned int mark) +{ + while (list) { + struct commit_list *temp = list; + temp->item->object.flags &= ~mark; + list = temp->next; + free(temp); + } +} + +static int ref_newer(const unsigned char *new_sha1, + const unsigned char *old_sha1) +{ + struct object *o; + struct commit *old, *new; + struct commit_list *list, *used; + int found = 0; + + /* Both new and old must be commit-ish and new is descendant of + * old. Otherwise we require --force. + */ + o = deref_tag(parse_object(old_sha1), NULL, 0); + if (!o || o->type != OBJ_COMMIT) + return 0; + old = (struct commit *) o; + + o = deref_tag(parse_object(new_sha1), NULL, 0); + if (!o || o->type != OBJ_COMMIT) + return 0; + new = (struct commit *) o; + + if (parse_commit(new) < 0) + return 0; + + used = list = NULL; + commit_list_insert(new, &list); + while (list) { + new = pop_most_recent_commit(&list, 1); + commit_list_insert(new, &used); + if (new == old) { + found = 1; + break; + } + } + unmark_and_free(list, 1); + unmark_and_free(used, 1); + return found; +} + +static struct ref *local_refs, **local_tail; +static struct ref *remote_refs, **remote_tail; + +static int one_local_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + struct ref *ref; + int len = strlen(refname) + 1; + ref = xcalloc(1, sizeof(*ref) + len); + hashcpy(ref->new_sha1, sha1); + memcpy(ref->name, refname, len); + *local_tail = ref; + local_tail = &ref->next; + return 0; +} + +static void get_local_heads(void) +{ + local_tail = &local_refs; + for_each_ref(one_local_ref, NULL); +} + +static int receive_status(int in) +{ + char line[1000]; + int ret = 0; + int len = packet_read_line(in, line, sizeof(line)); + if (len < 10 || memcmp(line, "unpack ", 7)) { + fprintf(stderr, "did not receive status back\n"); + return -1; + } + if (memcmp(line, "unpack ok\n", 10)) { + fputs(line, stderr); + ret = -1; + } + while (1) { + len = packet_read_line(in, line, sizeof(line)); + if (!len) + break; + if (len < 3 || + (memcmp(line, "ok", 2) && memcmp(line, "ng", 2))) { + fprintf(stderr, "protocol error: %s\n", line); + ret = -1; + break; + } + if (!memcmp(line, "ok", 2)) + continue; + fputs(line, stderr); + ret = -1; + } + return ret; +} + +static int send_pack(int in, int out, int nr_refspec, char **refspec) +{ + struct ref *ref; + int new_refs; + int ret = 0; + int ask_for_status_report = 0; + int allow_deleting_refs = 0; + int expect_status_report = 0; + + /* No funny business with the matcher */ + remote_tail = get_remote_heads(in, &remote_refs, 0, NULL, REF_NORMAL); + get_local_heads(); + + /* Does the other end support the reporting? */ + if (server_supports("report-status")) + ask_for_status_report = 1; + if (server_supports("delete-refs")) + allow_deleting_refs = 1; + + /* match them up */ + if (!remote_tail) + remote_tail = &remote_refs; + if (match_refs(local_refs, remote_refs, &remote_tail, + nr_refspec, refspec, send_all)) + return -1; + + if (!remote_refs) { + fprintf(stderr, "No refs in common and none specified; doing nothing.\n"); + return 0; + } + + /* + * Finally, tell the other end! + */ + new_refs = 0; + for (ref = remote_refs; ref; ref = ref->next) { + char old_hex[60], *new_hex; + int delete_ref; + + if (!ref->peer_ref) + continue; + + delete_ref = is_null_sha1(ref->peer_ref->new_sha1); + if (delete_ref && !allow_deleting_refs) { + error("remote does not support deleting refs"); + ret = -2; + continue; + } + if (!delete_ref && + !hashcmp(ref->old_sha1, ref->peer_ref->new_sha1)) { + if (verbose) + fprintf(stderr, "'%s': up-to-date\n", ref->name); + continue; + } + + /* This part determines what can overwrite what. + * The rules are: + * + * (0) you can always use --force or +A:B notation to + * selectively force individual ref pairs. + * + * (1) if the old thing does not exist, it is OK. + * + * (2) if you do not have the old thing, you are not allowed + * to overwrite it; you would not know what you are losing + * otherwise. + * + * (3) if both new and old are commit-ish, and new is a + * descendant of old, it is OK. + * + * (4) regardless of all of the above, removing :B is + * always allowed. + */ + + if (!force_update && + !delete_ref && + !is_null_sha1(ref->old_sha1) && + !ref->force) { + if (!has_sha1_file(ref->old_sha1) || + !ref_newer(ref->peer_ref->new_sha1, + ref->old_sha1)) { + /* We do not have the remote ref, or + * we know that the remote ref is not + * an ancestor of what we are trying to + * push. Either way this can be losing + * commits at the remote end and likely + * we were not up to date to begin with. + */ + error("remote '%s' is not a strict " + "subset of local ref '%s'. " + "maybe you are not up-to-date and " + "need to pull first?", + ref->name, + ref->peer_ref->name); + ret = -2; + continue; + } + } + hashcpy(ref->new_sha1, ref->peer_ref->new_sha1); + if (!delete_ref) + new_refs++; + strcpy(old_hex, sha1_to_hex(ref->old_sha1)); + new_hex = sha1_to_hex(ref->new_sha1); + + if (ask_for_status_report) { + packet_write(out, "%s %s %s%c%s", + old_hex, new_hex, ref->name, 0, + "report-status"); + ask_for_status_report = 0; + expect_status_report = 1; + } + else + packet_write(out, "%s %s %s", + old_hex, new_hex, ref->name); + if (delete_ref) + fprintf(stderr, "deleting '%s'\n", ref->name); + else { + fprintf(stderr, "updating '%s'", ref->name); + if (strcmp(ref->name, ref->peer_ref->name)) + fprintf(stderr, " using '%s'", + ref->peer_ref->name); + fprintf(stderr, "\n from %s\n to %s\n", + old_hex, new_hex); + } + } + + packet_flush(out); + if (new_refs) + ret = pack_objects(out, remote_refs); + close(out); + + if (expect_status_report) { + if (receive_status(in)) + ret = -4; + } + + if (!new_refs && ret == 0) + fprintf(stderr, "Everything up-to-date\n"); + return ret; +} + +static void verify_remote_names(int nr_heads, char **heads) +{ + int i; + + for (i = 0; i < nr_heads; i++) { + const char *remote = strchr(heads[i], ':'); + + remote = remote ? (remote + 1) : heads[i]; + switch (check_ref_format(remote)) { + case 0: /* ok */ + case -2: /* ok but a single level -- that is fine for + * a match pattern. + */ + continue; + } + die("remote part of refspec is not a valid name in %s", + heads[i]); + } +} + +int main(int argc, char **argv) +{ + int i, nr_heads = 0; + char *dest = NULL; + char **heads = NULL; + int fd[2], ret; + pid_t pid; + + setup_git_directory(); + git_config(git_default_config); + + argv++; + for (i = 1; i < argc; i++, argv++) { + char *arg = *argv; + + if (*arg == '-') { + if (!strncmp(arg, "--receive-pack=", 15)) { + receivepack = arg + 15; + continue; + } + if (!strncmp(arg, "--exec=", 7)) { + receivepack = arg + 7; + continue; + } + if (!strcmp(arg, "--all")) { + send_all = 1; + continue; + } + if (!strcmp(arg, "--force")) { + force_update = 1; + continue; + } + if (!strcmp(arg, "--verbose")) { + verbose = 1; + continue; + } + if (!strcmp(arg, "--thin")) { + use_thin_pack = 1; + continue; + } + usage(send_pack_usage); + } + if (!dest) { + dest = arg; + continue; + } + heads = argv; + nr_heads = argc - i; + break; + } + if (!dest) + usage(send_pack_usage); + if (heads && send_all) + usage(send_pack_usage); + verify_remote_names(nr_heads, heads); + + pid = git_connect(fd, dest, receivepack); + if (pid < 0) + return 1; + ret = send_pack(fd[0], fd[1], nr_heads, heads); + close(fd[0]); + close(fd[1]); + ret |= finish_connect(pid); + return !!ret; +} diff --git a/server-info.c b/server-info.c new file mode 100644 index 0000000000..f9be5a7f60 --- /dev/null +++ b/server-info.c @@ -0,0 +1,250 @@ +#include "cache.h" +#include "refs.h" +#include "object.h" +#include "commit.h" +#include "tag.h" + +/* refs */ +static FILE *info_ref_fp; + +static int add_info_ref(const char *path, const unsigned char *sha1, int flag, void *cb_data) +{ + struct object *o = parse_object(sha1); + if (!o) + return -1; + + fprintf(info_ref_fp, "%s %s\n", sha1_to_hex(sha1), path); + if (o->type == OBJ_TAG) { + o = deref_tag(o, path, 0); + if (o) + fprintf(info_ref_fp, "%s %s^{}\n", + sha1_to_hex(o->sha1), path); + } + return 0; +} + +static int update_info_refs(int force) +{ + char *path0 = xstrdup(git_path("info/refs")); + int len = strlen(path0); + char *path1 = xmalloc(len + 2); + + strcpy(path1, path0); + strcpy(path1 + len, "+"); + + safe_create_leading_directories(path0); + info_ref_fp = fopen(path1, "w"); + if (!info_ref_fp) + return error("unable to update %s", path0); + for_each_ref(add_info_ref, NULL); + fclose(info_ref_fp); + rename(path1, path0); + free(path0); + free(path1); + return 0; +} + +/* packs */ +static struct pack_info { + struct packed_git *p; + int old_num; + int new_num; + int nr_alloc; + int nr_heads; + unsigned char (*head)[20]; +} **info; +static int num_pack; +static const char *objdir; +static int objdirlen; + +static struct pack_info *find_pack_by_name(const char *name) +{ + int i; + for (i = 0; i < num_pack; i++) { + struct packed_git *p = info[i]->p; + /* skip "/pack/" after ".git/objects" */ + if (!strcmp(p->pack_name + objdirlen + 6, name)) + return info[i]; + } + return NULL; +} + +/* Returns non-zero when we detect that the info in the + * old file is useless. + */ +static int parse_pack_def(const char *line, int old_cnt) +{ + struct pack_info *i = find_pack_by_name(line + 2); + if (i) { + i->old_num = old_cnt; + return 0; + } + else { + /* The file describes a pack that is no longer here */ + return 1; + } +} + +/* Returns non-zero when we detect that the info in the + * old file is useless. + */ +static int read_pack_info_file(const char *infofile) +{ + FILE *fp; + char line[1000]; + int old_cnt = 0; + + fp = fopen(infofile, "r"); + if (!fp) + return 1; /* nonexistent is not an error. */ + + while (fgets(line, sizeof(line), fp)) { + int len = strlen(line); + if (line[len-1] == '\n') + line[--len] = 0; + + if (!len) + continue; + + switch (line[0]) { + case 'P': /* P name */ + if (parse_pack_def(line, old_cnt++)) + goto out_stale; + break; + case 'D': /* we used to emit D but that was misguided. */ + goto out_stale; + break; + case 'T': /* we used to emit T but nobody uses it. */ + goto out_stale; + break; + default: + error("unrecognized: %s", line); + break; + } + } + fclose(fp); + return 0; + out_stale: + fclose(fp); + return 1; +} + +static int compare_info(const void *a_, const void *b_) +{ + struct pack_info * const* a = a_; + struct pack_info * const* b = b_; + + if (0 <= (*a)->old_num && 0 <= (*b)->old_num) + /* Keep the order in the original */ + return (*a)->old_num - (*b)->old_num; + else if (0 <= (*a)->old_num) + /* Only A existed in the original so B is obviously newer */ + return -1; + else if (0 <= (*b)->old_num) + /* The other way around. */ + return 1; + + /* then it does not matter but at least keep the comparison stable */ + if ((*a)->p == (*b)->p) + return 0; + else if ((*a)->p < (*b)->p) + return -1; + else + return 1; +} + +static void init_pack_info(const char *infofile, int force) +{ + struct packed_git *p; + int stale; + int i = 0; + + objdir = get_object_directory(); + objdirlen = strlen(objdir); + + prepare_packed_git(); + for (p = packed_git; p; p = p->next) { + /* we ignore things on alternate path since they are + * not available to the pullers in general. + */ + if (!p->pack_local) + continue; + i++; + } + num_pack = i; + info = xcalloc(num_pack, sizeof(struct pack_info *)); + for (i = 0, p = packed_git; p; p = p->next) { + if (!p->pack_local) + continue; + info[i] = xcalloc(1, sizeof(struct pack_info)); + info[i]->p = p; + info[i]->old_num = -1; + i++; + } + + if (infofile && !force) + stale = read_pack_info_file(infofile); + else + stale = 1; + + for (i = 0; i < num_pack; i++) { + if (stale) { + info[i]->old_num = -1; + info[i]->nr_heads = 0; + } + } + + /* renumber them */ + qsort(info, num_pack, sizeof(info[0]), compare_info); + for (i = 0; i < num_pack; i++) + info[i]->new_num = i; +} + +static void write_pack_info_file(FILE *fp) +{ + int i; + for (i = 0; i < num_pack; i++) + fprintf(fp, "P %s\n", info[i]->p->pack_name + objdirlen + 6); + fputc('\n', fp); +} + +static int update_info_packs(int force) +{ + char infofile[PATH_MAX]; + char name[PATH_MAX]; + int namelen; + FILE *fp; + + namelen = sprintf(infofile, "%s/info/packs", get_object_directory()); + strcpy(name, infofile); + strcpy(name + namelen, "+"); + + init_pack_info(infofile, force); + + safe_create_leading_directories(name); + fp = fopen(name, "w"); + if (!fp) + return error("cannot open %s", name); + write_pack_info_file(fp); + fclose(fp); + rename(name, infofile); + return 0; +} + +/* public */ +int update_server_info(int force) +{ + /* We would add more dumb-server support files later, + * including index of available pack files and their + * intended audiences. + */ + int errs = 0; + + errs = errs | update_info_refs(force); + errs = errs | update_info_packs(force); + + /* remove leftover rev-cache file if there is any */ + unlink(git_path("info/rev-cache")); + + return errs; +} diff --git a/setup.c b/setup.c new file mode 100644 index 0000000000..e9d3f5aab6 --- /dev/null +++ b/setup.c @@ -0,0 +1,296 @@ +#include "cache.h" + +const char *prefix_path(const char *prefix, int len, const char *path) +{ + const char *orig = path; + for (;;) { + char c; + if (*path != '.') + break; + c = path[1]; + /* "." */ + if (!c) { + path++; + break; + } + /* "./" */ + if (c == '/') { + path += 2; + continue; + } + if (c != '.') + break; + c = path[2]; + if (!c) + path += 2; + else if (c == '/') + path += 3; + else + break; + /* ".." and "../" */ + /* Remove last component of the prefix */ + do { + if (!len) + die("'%s' is outside repository", orig); + len--; + } while (len && prefix[len-1] != '/'); + continue; + } + if (len) { + int speclen = strlen(path); + char *n = xmalloc(speclen + len + 1); + + memcpy(n, prefix, len); + memcpy(n + len, path, speclen+1); + path = n; + } + return path; +} + +/* + * Unlike prefix_path, this should be used if the named file does + * not have to interact with index entry; i.e. name of a random file + * on the filesystem. + */ +const char *prefix_filename(const char *pfx, int pfx_len, const char *arg) +{ + static char path[PATH_MAX]; + if (!pfx || !*pfx || arg[0] == '/') + return arg; + memcpy(path, pfx, pfx_len); + strcpy(path + pfx_len, arg); + return path; +} + +/* + * Verify a filename that we got as an argument for a pathspec + * entry. Note that a filename that begins with "-" never verifies + * as true, because even if such a filename were to exist, we want + * it to be preceded by the "--" marker (or we want the user to + * use a format like "./-filename") + */ +void verify_filename(const char *prefix, const char *arg) +{ + const char *name; + struct stat st; + + if (*arg == '-') + die("bad flag '%s' used after filename", arg); + name = prefix ? prefix_filename(prefix, strlen(prefix), arg) : arg; + if (!lstat(name, &st)) + return; + if (errno == ENOENT) + die("ambiguous argument '%s': unknown revision or path not in the working tree.\n" + "Use '--' to separate paths from revisions", arg); + die("'%s': %s", arg, strerror(errno)); +} + +/* + * Opposite of the above: the command line did not have -- marker + * and we parsed the arg as a refname. It should not be interpretable + * as a filename. + */ +void verify_non_filename(const char *prefix, const char *arg) +{ + const char *name; + struct stat st; + + if (is_inside_git_dir()) + return; + if (*arg == '-') + return; /* flag */ + name = prefix ? prefix_filename(prefix, strlen(prefix), arg) : arg; + if (!lstat(name, &st)) + die("ambiguous argument '%s': both revision and filename\n" + "Use '--' to separate filenames from revisions", arg); + if (errno != ENOENT) + die("'%s': %s", arg, strerror(errno)); +} + +const char **get_pathspec(const char *prefix, const char **pathspec) +{ + const char *entry = *pathspec; + const char **p; + int prefixlen; + + if (!prefix && !entry) + return NULL; + + if (!entry) { + static const char *spec[2]; + spec[0] = prefix; + spec[1] = NULL; + return spec; + } + + /* Otherwise we have to re-write the entries.. */ + p = pathspec; + prefixlen = prefix ? strlen(prefix) : 0; + do { + *p = prefix_path(prefix, prefixlen, entry); + } while ((entry = *++p) != NULL); + return (const char **) pathspec; +} + +/* + * Test if it looks like we're at a git directory. + * We want to see: + * + * - either a objects/ directory _or_ the proper + * GIT_OBJECT_DIRECTORY environment variable + * - a refs/ directory + * - either a HEAD symlink or a HEAD file that is formatted as + * a proper "ref:", or a regular file HEAD that has a properly + * formatted sha1 object name. + */ +static int is_git_directory(const char *suspect) +{ + char path[PATH_MAX]; + size_t len = strlen(suspect); + + strcpy(path, suspect); + if (getenv(DB_ENVIRONMENT)) { + if (access(getenv(DB_ENVIRONMENT), X_OK)) + return 0; + } + else { + strcpy(path + len, "/objects"); + if (access(path, X_OK)) + return 0; + } + + strcpy(path + len, "/refs"); + if (access(path, X_OK)) + return 0; + + strcpy(path + len, "/HEAD"); + if (validate_headref(path)) + return 0; + + return 1; +} + +static int inside_git_dir = -1; + +int is_inside_git_dir(void) +{ + if (inside_git_dir < 0) { + char buffer[1024]; + + if (is_bare_repository()) + return (inside_git_dir = 1); + if (getcwd(buffer, sizeof(buffer))) { + const char *git_dir = get_git_dir(), *cwd = buffer; + while (*git_dir && *git_dir == *cwd) { + git_dir++; + cwd++; + } + inside_git_dir = !*git_dir; + } else + inside_git_dir = 0; + } + return inside_git_dir; +} + +const char *setup_git_directory_gently(int *nongit_ok) +{ + static char cwd[PATH_MAX+1]; + const char *gitdirenv; + int len, offset; + + /* + * If GIT_DIR is set explicitly, we're not going + * to do any discovery, but we still do repository + * validation. + */ + gitdirenv = getenv(GIT_DIR_ENVIRONMENT); + if (gitdirenv) { + if (PATH_MAX - 40 < strlen(gitdirenv)) + die("'$%s' too big", GIT_DIR_ENVIRONMENT); + if (is_git_directory(gitdirenv)) + return NULL; + if (nongit_ok) { + *nongit_ok = 1; + return NULL; + } + die("Not a git repository: '%s'", gitdirenv); + } + + if (!getcwd(cwd, sizeof(cwd)) || cwd[0] != '/') + die("Unable to read current working directory"); + + offset = len = strlen(cwd); + for (;;) { + if (is_git_directory(".git")) + break; + chdir(".."); + do { + if (!offset) { + if (is_git_directory(cwd)) { + if (chdir(cwd)) + die("Cannot come back to cwd"); + setenv(GIT_DIR_ENVIRONMENT, cwd, 1); + inside_git_dir = 1; + return NULL; + } + if (nongit_ok) { + if (chdir(cwd)) + die("Cannot come back to cwd"); + *nongit_ok = 1; + return NULL; + } + die("Not a git repository"); + } + } while (cwd[--offset] != '/'); + } + + if (offset == len) + return NULL; + + /* Make "offset" point to past the '/', and add a '/' at the end */ + offset++; + cwd[len++] = '/'; + cwd[len] = 0; + inside_git_dir = !strncmp(cwd + offset, ".git/", 5); + return cwd + offset; +} + +int git_config_perm(const char *var, const char *value) +{ + if (value) { + if (!strcmp(value, "umask")) + return PERM_UMASK; + if (!strcmp(value, "group")) + return PERM_GROUP; + if (!strcmp(value, "all") || + !strcmp(value, "world") || + !strcmp(value, "everybody")) + return PERM_EVERYBODY; + } + return git_config_bool(var, value); +} + +int check_repository_format_version(const char *var, const char *value) +{ + if (strcmp(var, "core.repositoryformatversion") == 0) + repository_format_version = git_config_int(var, value); + else if (strcmp(var, "core.sharedrepository") == 0) + shared_repository = git_config_perm(var, value); + return 0; +} + +int check_repository_format(void) +{ + git_config(check_repository_format_version); + if (GIT_REPO_VERSION < repository_format_version) + die ("Expected git repo version <= %d, found %d", + GIT_REPO_VERSION, repository_format_version); + return 0; +} + +const char *setup_git_directory(void) +{ + const char *retval = setup_git_directory_gently(NULL); + check_repository_format(); + return retval; +} diff --git a/sha1_file.c b/sha1_file.c new file mode 100644 index 0000000000..0d4bf80e74 --- /dev/null +++ b/sha1_file.c @@ -0,0 +1,2158 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + * + * This handles basic git sha1 object files - packing, unpacking, + * creation etc. + */ +#include "cache.h" +#include "delta.h" +#include "pack.h" +#include "blob.h" +#include "commit.h" +#include "tag.h" +#include "tree.h" + +#ifndef O_NOATIME +#if defined(__linux__) && (defined(__i386__) || defined(__PPC__)) +#define O_NOATIME 01000000 +#else +#define O_NOATIME 0 +#endif +#endif + +#ifdef NO_C99_FORMAT +#define SZ_FMT "lu" +#else +#define SZ_FMT "zu" +#endif + +const unsigned char null_sha1[20]; + +static unsigned int sha1_file_open_flag = O_NOATIME; + +signed char hexval_table[256] = { + -1, -1, -1, -1, -1, -1, -1, -1, /* 00-07 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 08-0f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 10-17 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 18-1f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 20-27 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 28-2f */ + 0, 1, 2, 3, 4, 5, 6, 7, /* 30-37 */ + 8, 9, -1, -1, -1, -1, -1, -1, /* 38-3f */ + -1, 10, 11, 12, 13, 14, 15, -1, /* 40-47 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 48-4f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 50-57 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 58-5f */ + -1, 10, 11, 12, 13, 14, 15, -1, /* 60-67 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 68-67 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 70-77 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 78-7f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 80-87 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 88-8f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 90-97 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 98-9f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* a0-a7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* a8-af */ + -1, -1, -1, -1, -1, -1, -1, -1, /* b0-b7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* b8-bf */ + -1, -1, -1, -1, -1, -1, -1, -1, /* c0-c7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* c8-cf */ + -1, -1, -1, -1, -1, -1, -1, -1, /* d0-d7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* d8-df */ + -1, -1, -1, -1, -1, -1, -1, -1, /* e0-e7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* e8-ef */ + -1, -1, -1, -1, -1, -1, -1, -1, /* f0-f7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* f8-ff */ +}; + +int get_sha1_hex(const char *hex, unsigned char *sha1) +{ + int i; + for (i = 0; i < 20; i++) { + unsigned int val = (hexval(hex[0]) << 4) | hexval(hex[1]); + if (val & ~0xff) + return -1; + *sha1++ = val; + hex += 2; + } + return 0; +} + +int safe_create_leading_directories(char *path) +{ + char *pos = path; + struct stat st; + + if (*pos == '/') + pos++; + + while (pos) { + pos = strchr(pos, '/'); + if (!pos) + break; + *pos = 0; + if (!stat(path, &st)) { + /* path exists */ + if (!S_ISDIR(st.st_mode)) { + *pos = '/'; + return -3; + } + } + else if (mkdir(path, 0777)) { + *pos = '/'; + return -1; + } + else if (adjust_shared_perm(path)) { + *pos = '/'; + return -2; + } + *pos++ = '/'; + } + return 0; +} + +char * sha1_to_hex(const unsigned char *sha1) +{ + static int bufno; + static char hexbuffer[4][50]; + static const char hex[] = "0123456789abcdef"; + char *buffer = hexbuffer[3 & ++bufno], *buf = buffer; + int i; + + for (i = 0; i < 20; i++) { + unsigned int val = *sha1++; + *buf++ = hex[val >> 4]; + *buf++ = hex[val & 0xf]; + } + *buf = '\0'; + + return buffer; +} + +static void fill_sha1_path(char *pathbuf, const unsigned char *sha1) +{ + int i; + for (i = 0; i < 20; i++) { + static char hex[] = "0123456789abcdef"; + unsigned int val = sha1[i]; + char *pos = pathbuf + i*2 + (i > 0); + *pos++ = hex[val >> 4]; + *pos = hex[val & 0xf]; + } +} + +/* + * NOTE! This returns a statically allocated buffer, so you have to be + * careful about using it. Do a "xstrdup()" if you need to save the + * filename. + * + * Also note that this returns the location for creating. Reading + * SHA1 file can happen from any alternate directory listed in the + * DB_ENVIRONMENT environment variable if it is not found in + * the primary object database. + */ +char *sha1_file_name(const unsigned char *sha1) +{ + static char *name, *base; + + if (!base) { + const char *sha1_file_directory = get_object_directory(); + int len = strlen(sha1_file_directory); + base = xmalloc(len + 60); + memcpy(base, sha1_file_directory, len); + memset(base+len, 0, 60); + base[len] = '/'; + base[len+3] = '/'; + name = base + len + 1; + } + fill_sha1_path(name, sha1); + return base; +} + +char *sha1_pack_name(const unsigned char *sha1) +{ + static const char hex[] = "0123456789abcdef"; + static char *name, *base, *buf; + int i; + + if (!base) { + const char *sha1_file_directory = get_object_directory(); + int len = strlen(sha1_file_directory); + base = xmalloc(len + 60); + sprintf(base, "%s/pack/pack-1234567890123456789012345678901234567890.pack", sha1_file_directory); + name = base + len + 11; + } + + buf = name; + + for (i = 0; i < 20; i++) { + unsigned int val = *sha1++; + *buf++ = hex[val >> 4]; + *buf++ = hex[val & 0xf]; + } + + return base; +} + +char *sha1_pack_index_name(const unsigned char *sha1) +{ + static const char hex[] = "0123456789abcdef"; + static char *name, *base, *buf; + int i; + + if (!base) { + const char *sha1_file_directory = get_object_directory(); + int len = strlen(sha1_file_directory); + base = xmalloc(len + 60); + sprintf(base, "%s/pack/pack-1234567890123456789012345678901234567890.idx", sha1_file_directory); + name = base + len + 11; + } + + buf = name; + + for (i = 0; i < 20; i++) { + unsigned int val = *sha1++; + *buf++ = hex[val >> 4]; + *buf++ = hex[val & 0xf]; + } + + return base; +} + +struct alternate_object_database *alt_odb_list; +static struct alternate_object_database **alt_odb_tail; + +static void read_info_alternates(const char * alternates, int depth); + +/* + * Prepare alternate object database registry. + * + * The variable alt_odb_list points at the list of struct + * alternate_object_database. The elements on this list come from + * non-empty elements from colon separated ALTERNATE_DB_ENVIRONMENT + * environment variable, and $GIT_OBJECT_DIRECTORY/info/alternates, + * whose contents is similar to that environment variable but can be + * LF separated. Its base points at a statically allocated buffer that + * contains "/the/directory/corresponding/to/.git/objects/...", while + * its name points just after the slash at the end of ".git/objects/" + * in the example above, and has enough space to hold 40-byte hex + * SHA1, an extra slash for the first level indirection, and the + * terminating NUL. + */ +static int link_alt_odb_entry(const char * entry, int len, const char * relative_base, int depth) +{ + struct stat st; + const char *objdir = get_object_directory(); + struct alternate_object_database *ent; + struct alternate_object_database *alt; + /* 43 = 40-byte + 2 '/' + terminating NUL */ + int pfxlen = len; + int entlen = pfxlen + 43; + int base_len = -1; + + if (*entry != '/' && relative_base) { + /* Relative alt-odb */ + if (base_len < 0) + base_len = strlen(relative_base) + 1; + entlen += base_len; + pfxlen += base_len; + } + ent = xmalloc(sizeof(*ent) + entlen); + + if (*entry != '/' && relative_base) { + memcpy(ent->base, relative_base, base_len - 1); + ent->base[base_len - 1] = '/'; + memcpy(ent->base + base_len, entry, len); + } + else + memcpy(ent->base, entry, pfxlen); + + ent->name = ent->base + pfxlen + 1; + ent->base[pfxlen + 3] = '/'; + ent->base[pfxlen] = ent->base[entlen-1] = 0; + + /* Detect cases where alternate disappeared */ + if (stat(ent->base, &st) || !S_ISDIR(st.st_mode)) { + error("object directory %s does not exist; " + "check .git/objects/info/alternates.", + ent->base); + free(ent); + return -1; + } + + /* Prevent the common mistake of listing the same + * thing twice, or object directory itself. + */ + for (alt = alt_odb_list; alt; alt = alt->next) { + if (!memcmp(ent->base, alt->base, pfxlen)) { + free(ent); + return -1; + } + } + if (!memcmp(ent->base, objdir, pfxlen)) { + free(ent); + return -1; + } + + /* add the alternate entry */ + *alt_odb_tail = ent; + alt_odb_tail = &(ent->next); + ent->next = NULL; + + /* recursively add alternates */ + read_info_alternates(ent->base, depth + 1); + + ent->base[pfxlen] = '/'; + + return 0; +} + +static void link_alt_odb_entries(const char *alt, const char *ep, int sep, + const char *relative_base, int depth) +{ + const char *cp, *last; + + if (depth > 5) { + error("%s: ignoring alternate object stores, nesting too deep.", + relative_base); + return; + } + + last = alt; + while (last < ep) { + cp = last; + if (cp < ep && *cp == '#') { + while (cp < ep && *cp != sep) + cp++; + last = cp + 1; + continue; + } + while (cp < ep && *cp != sep) + cp++; + if (last != cp) { + if ((*last != '/') && depth) { + error("%s: ignoring relative alternate object store %s", + relative_base, last); + } else { + link_alt_odb_entry(last, cp - last, + relative_base, depth); + } + } + while (cp < ep && *cp == sep) + cp++; + last = cp; + } +} + +static void read_info_alternates(const char * relative_base, int depth) +{ + char *map; + struct stat st; + char path[PATH_MAX]; + int fd; + + sprintf(path, "%s/info/alternates", relative_base); + fd = open(path, O_RDONLY); + if (fd < 0) + return; + if (fstat(fd, &st) || (st.st_size == 0)) { + close(fd); + return; + } + map = xmmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + + link_alt_odb_entries(map, map + st.st_size, '\n', relative_base, depth); + + munmap(map, st.st_size); +} + +void prepare_alt_odb(void) +{ + const char *alt; + + alt = getenv(ALTERNATE_DB_ENVIRONMENT); + if (!alt) alt = ""; + + if (alt_odb_tail) + return; + alt_odb_tail = &alt_odb_list; + link_alt_odb_entries(alt, alt + strlen(alt), ':', NULL, 0); + + read_info_alternates(get_object_directory(), 0); +} + +static char *find_sha1_file(const unsigned char *sha1, struct stat *st) +{ + char *name = sha1_file_name(sha1); + struct alternate_object_database *alt; + + if (!stat(name, st)) + return name; + prepare_alt_odb(); + for (alt = alt_odb_list; alt; alt = alt->next) { + name = alt->name; + fill_sha1_path(name, sha1); + if (!stat(alt->base, st)) + return alt->base; + } + return NULL; +} + +static unsigned int pack_used_ctr; +static unsigned int pack_mmap_calls; +static unsigned int peak_pack_open_windows; +static unsigned int pack_open_windows; +static size_t peak_pack_mapped; +static size_t pack_mapped; +static size_t page_size; +struct packed_git *packed_git; + +void pack_report() +{ + fprintf(stderr, + "pack_report: getpagesize() = %10" SZ_FMT "\n" + "pack_report: core.packedGitWindowSize = %10" SZ_FMT "\n" + "pack_report: core.packedGitLimit = %10" SZ_FMT "\n", + page_size, + packed_git_window_size, + packed_git_limit); + fprintf(stderr, + "pack_report: pack_used_ctr = %10u\n" + "pack_report: pack_mmap_calls = %10u\n" + "pack_report: pack_open_windows = %10u / %10u\n" + "pack_report: pack_mapped = " + "%10" SZ_FMT " / %10" SZ_FMT "\n", + pack_used_ctr, + pack_mmap_calls, + pack_open_windows, peak_pack_open_windows, + pack_mapped, peak_pack_mapped); +} + +static int check_packed_git_idx(const char *path, unsigned long *idx_size_, + void **idx_map_) +{ + void *idx_map; + uint32_t *index; + unsigned long idx_size; + int nr, i; + int fd = open(path, O_RDONLY); + struct stat st; + if (fd < 0) + return -1; + if (fstat(fd, &st)) { + close(fd); + return -1; + } + idx_size = st.st_size; + idx_map = xmmap(NULL, idx_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + + index = idx_map; + *idx_map_ = idx_map; + *idx_size_ = idx_size; + + /* check index map */ + if (idx_size < 4*256 + 20 + 20) + return error("index file %s is too small", path); + + /* a future index format would start with this, as older git + * binaries would fail the non-monotonic index check below. + * give a nicer warning to the user if we can. + */ + if (index[0] == htonl(PACK_IDX_SIGNATURE)) + return error("index file %s is a newer version" + " and is not supported by this binary" + " (try upgrading GIT to a newer version)", + path); + + nr = 0; + for (i = 0; i < 256; i++) { + unsigned int n = ntohl(index[i]); + if (n < nr) + return error("non-monotonic index %s", path); + nr = n; + } + + /* + * Total size: + * - 256 index entries 4 bytes each + * - 24-byte entries * nr (20-byte sha1 + 4-byte offset) + * - 20-byte SHA1 of the packfile + * - 20-byte SHA1 file checksum + */ + if (idx_size != 4*256 + nr * 24 + 20 + 20) + return error("wrong index file size in %s", path); + + return 0; +} + +static void scan_windows(struct packed_git *p, + struct packed_git **lru_p, + struct pack_window **lru_w, + struct pack_window **lru_l) +{ + struct pack_window *w, *w_l; + + for (w_l = NULL, w = p->windows; w; w = w->next) { + if (!w->inuse_cnt) { + if (!*lru_w || w->last_used < (*lru_w)->last_used) { + *lru_p = p; + *lru_w = w; + *lru_l = w_l; + } + } + w_l = w; + } +} + +static int unuse_one_window(struct packed_git *current) +{ + struct packed_git *p, *lru_p = NULL; + struct pack_window *lru_w = NULL, *lru_l = NULL; + + if (current) + scan_windows(current, &lru_p, &lru_w, &lru_l); + for (p = packed_git; p; p = p->next) + scan_windows(p, &lru_p, &lru_w, &lru_l); + if (lru_p) { + munmap(lru_w->base, lru_w->len); + pack_mapped -= lru_w->len; + if (lru_l) + lru_l->next = lru_w->next; + else { + lru_p->windows = lru_w->next; + if (!lru_p->windows && lru_p != current) { + close(lru_p->pack_fd); + lru_p->pack_fd = -1; + } + } + free(lru_w); + pack_open_windows--; + return 1; + } + return 0; +} + +void release_pack_memory(size_t need) +{ + size_t cur = pack_mapped; + while (need >= (cur - pack_mapped) && unuse_one_window(NULL)) + ; /* nothing */ +} + +void unuse_pack(struct pack_window **w_cursor) +{ + struct pack_window *w = *w_cursor; + if (w) { + w->inuse_cnt--; + *w_cursor = NULL; + } +} + +/* + * Do not call this directly as this leaks p->pack_fd on error return; + * call open_packed_git() instead. + */ +static int open_packed_git_1(struct packed_git *p) +{ + struct stat st; + struct pack_header hdr; + unsigned char sha1[20]; + unsigned char *idx_sha1; + long fd_flag; + + p->pack_fd = open(p->pack_name, O_RDONLY); + if (p->pack_fd < 0 || fstat(p->pack_fd, &st)) + return -1; + + /* If we created the struct before we had the pack we lack size. */ + if (!p->pack_size) { + if (!S_ISREG(st.st_mode)) + return error("packfile %s not a regular file", p->pack_name); + p->pack_size = st.st_size; + } else if (p->pack_size != st.st_size) + return error("packfile %s size changed", p->pack_name); + + /* We leave these file descriptors open with sliding mmap; + * there is no point keeping them open across exec(), though. + */ + fd_flag = fcntl(p->pack_fd, F_GETFD, 0); + if (fd_flag < 0) + return error("cannot determine file descriptor flags"); + fd_flag |= FD_CLOEXEC; + if (fcntl(p->pack_fd, F_SETFD, fd_flag) == -1) + return error("cannot set FD_CLOEXEC"); + + /* Verify we recognize this pack file format. */ + if (read_in_full(p->pack_fd, &hdr, sizeof(hdr)) != sizeof(hdr)) + return error("file %s is far too short to be a packfile", p->pack_name); + if (hdr.hdr_signature != htonl(PACK_SIGNATURE)) + return error("file %s is not a GIT packfile", p->pack_name); + if (!pack_version_ok(hdr.hdr_version)) + return error("packfile %s is version %u and not supported" + " (try upgrading GIT to a newer version)", + p->pack_name, ntohl(hdr.hdr_version)); + + /* Verify the pack matches its index. */ + if (num_packed_objects(p) != ntohl(hdr.hdr_entries)) + return error("packfile %s claims to have %u objects" + " while index size indicates %u objects", + p->pack_name, ntohl(hdr.hdr_entries), + num_packed_objects(p)); + if (lseek(p->pack_fd, p->pack_size - sizeof(sha1), SEEK_SET) == -1) + return error("end of packfile %s is unavailable", p->pack_name); + if (read_in_full(p->pack_fd, sha1, sizeof(sha1)) != sizeof(sha1)) + return error("packfile %s signature is unavailable", p->pack_name); + idx_sha1 = ((unsigned char *)p->index_base) + p->index_size - 40; + if (hashcmp(sha1, idx_sha1)) + return error("packfile %s does not match index", p->pack_name); + return 0; +} + +static int open_packed_git(struct packed_git *p) +{ + if (!open_packed_git_1(p)) + return 0; + if (p->pack_fd != -1) { + close(p->pack_fd); + p->pack_fd = -1; + } + return -1; +} + +static int in_window(struct pack_window *win, unsigned long offset) +{ + /* We must promise at least 20 bytes (one hash) after the + * offset is available from this window, otherwise the offset + * is not actually in this window and a different window (which + * has that one hash excess) must be used. This is to support + * the object header and delta base parsing routines below. + */ + off_t win_off = win->offset; + return win_off <= offset + && (offset + 20) <= (win_off + win->len); +} + +unsigned char* use_pack(struct packed_git *p, + struct pack_window **w_cursor, + unsigned long offset, + unsigned int *left) +{ + struct pack_window *win = *w_cursor; + + if (p->pack_fd == -1 && open_packed_git(p)) + die("packfile %s cannot be accessed", p->pack_name); + + /* Since packfiles end in a hash of their content and its + * pointless to ask for an offset into the middle of that + * hash, and the in_window function above wouldn't match + * don't allow an offset too close to the end of the file. + */ + if (offset > (p->pack_size - 20)) + die("offset beyond end of packfile (truncated pack?)"); + + if (!win || !in_window(win, offset)) { + if (win) + win->inuse_cnt--; + for (win = p->windows; win; win = win->next) { + if (in_window(win, offset)) + break; + } + if (!win) { + if (!page_size) + page_size = getpagesize(); + win = xcalloc(1, sizeof(*win)); + win->offset = (offset / page_size) * page_size; + win->len = p->pack_size - win->offset; + if (win->len > packed_git_window_size) + win->len = packed_git_window_size; + pack_mapped += win->len; + while (packed_git_limit < pack_mapped + && unuse_one_window(p)) + ; /* nothing */ + win->base = xmmap(NULL, win->len, + PROT_READ, MAP_PRIVATE, + p->pack_fd, win->offset); + if (win->base == MAP_FAILED) + die("packfile %s cannot be mapped: %s", + p->pack_name, + strerror(errno)); + pack_mmap_calls++; + pack_open_windows++; + if (pack_mapped > peak_pack_mapped) + peak_pack_mapped = pack_mapped; + if (pack_open_windows > peak_pack_open_windows) + peak_pack_open_windows = pack_open_windows; + win->next = p->windows; + p->windows = win; + } + } + if (win != *w_cursor) { + win->last_used = pack_used_ctr++; + win->inuse_cnt++; + *w_cursor = win; + } + offset -= win->offset; + if (left) + *left = win->len - offset; + return win->base + offset; +} + +struct packed_git *add_packed_git(char *path, int path_len, int local) +{ + struct stat st; + struct packed_git *p; + unsigned long idx_size; + void *idx_map; + unsigned char sha1[20]; + + if (check_packed_git_idx(path, &idx_size, &idx_map)) + return NULL; + + /* do we have a corresponding .pack file? */ + strcpy(path + path_len - 4, ".pack"); + if (stat(path, &st) || !S_ISREG(st.st_mode)) { + munmap(idx_map, idx_size); + return NULL; + } + /* ok, it looks sane as far as we can check without + * actually mapping the pack file. + */ + p = xmalloc(sizeof(*p) + path_len + 2); + strcpy(p->pack_name, path); + p->index_size = idx_size; + p->pack_size = st.st_size; + p->index_base = idx_map; + p->next = NULL; + p->windows = NULL; + p->pack_fd = -1; + p->pack_local = local; + if ((path_len > 44) && !get_sha1_hex(path + path_len - 44, sha1)) + hashcpy(p->sha1, sha1); + return p; +} + +struct packed_git *parse_pack_index(unsigned char *sha1) +{ + char *path = sha1_pack_index_name(sha1); + return parse_pack_index_file(sha1, path); +} + +struct packed_git *parse_pack_index_file(const unsigned char *sha1, char *idx_path) +{ + struct packed_git *p; + unsigned long idx_size; + void *idx_map; + char *path; + + if (check_packed_git_idx(idx_path, &idx_size, &idx_map)) + return NULL; + + path = sha1_pack_name(sha1); + + p = xmalloc(sizeof(*p) + strlen(path) + 2); + strcpy(p->pack_name, path); + p->index_size = idx_size; + p->pack_size = 0; + p->index_base = idx_map; + p->next = NULL; + p->windows = NULL; + p->pack_fd = -1; + hashcpy(p->sha1, sha1); + return p; +} + +void install_packed_git(struct packed_git *pack) +{ + pack->next = packed_git; + packed_git = pack; +} + +static void prepare_packed_git_one(char *objdir, int local) +{ + char path[PATH_MAX]; + int len; + DIR *dir; + struct dirent *de; + + sprintf(path, "%s/pack", objdir); + len = strlen(path); + dir = opendir(path); + if (!dir) { + if (errno != ENOENT) + error("unable to open object pack directory: %s: %s", + path, strerror(errno)); + return; + } + path[len++] = '/'; + while ((de = readdir(dir)) != NULL) { + int namelen = strlen(de->d_name); + struct packed_git *p; + + if (!has_extension(de->d_name, ".idx")) + continue; + + /* Don't reopen a pack we already have. */ + 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; + /* See if it really is a valid .idx file with corresponding + * .pack file that we can map. + */ + p = add_packed_git(path, len + namelen, local); + if (!p) + continue; + install_packed_git(p); + } + closedir(dir); +} + +static int prepare_packed_git_run_once = 0; +void prepare_packed_git(void) +{ + struct alternate_object_database *alt; + + if (prepare_packed_git_run_once) + return; + prepare_packed_git_one(get_object_directory(), 1); + prepare_alt_odb(); + for (alt = alt_odb_list; alt; alt = alt->next) { + alt->name[-1] = 0; + prepare_packed_git_one(alt->base, 0); + alt->name[-1] = '/'; + } + prepare_packed_git_run_once = 1; +} + +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) +{ + unsigned char real_sha1[20]; + hash_sha1_file(map, size, type, real_sha1); + return hashcmp(sha1, real_sha1) ? -1 : 0; +} + +void *map_sha1_file(const unsigned char *sha1, unsigned long *size) +{ + struct stat st; + void *map; + int fd; + char *filename = find_sha1_file(sha1, &st); + + if (!filename) { + return NULL; + } + + fd = open(filename, O_RDONLY | sha1_file_open_flag); + if (fd < 0) { + /* See if it works without O_NOATIME */ + switch (sha1_file_open_flag) { + default: + fd = open(filename, O_RDONLY); + if (fd >= 0) + break; + /* Fallthrough */ + case 0: + return NULL; + } + + /* If it failed once, it will probably fail again. + * Stop using O_NOATIME + */ + sha1_file_open_flag = 0; + } + map = xmmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + *size = st.st_size; + return map; +} + +int legacy_loose_object(unsigned char *map) +{ + unsigned int word; + + /* + * Is it a zlib-compressed buffer? If so, the first byte + * must be 0x78 (15-bit window size, deflated), and the + * first 16-bit word is evenly divisible by 31 + */ + word = (map[0] << 8) + map[1]; + if (map[0] == 0x78 && !(word % 31)) + return 1; + else + return 0; +} + +unsigned long unpack_object_header_gently(const unsigned char *buf, unsigned long len, enum object_type *type, unsigned long *sizep) +{ + unsigned shift; + unsigned char c; + unsigned long size; + unsigned long used = 0; + + c = buf[used++]; + *type = (c >> 4) & 7; + size = c & 15; + shift = 4; + while (c & 0x80) { + if (len <= used) + return 0; + if (sizeof(long) * 8 <= shift) + return 0; + c = buf[used++]; + size += (c & 0x7f) << shift; + shift += 7; + } + *sizep = size; + return used; +} + +static int unpack_sha1_header(z_stream *stream, unsigned char *map, unsigned long mapsize, void *buffer, unsigned long bufsiz) +{ + unsigned long size, used; + static const char valid_loose_object_type[8] = { + 0, /* OBJ_EXT */ + 1, 1, 1, 1, /* "commit", "tree", "blob", "tag" */ + 0, /* "delta" and others are invalid in a loose object */ + }; + enum object_type type; + + /* Get the data stream */ + memset(stream, 0, sizeof(*stream)); + stream->next_in = map; + stream->avail_in = mapsize; + stream->next_out = buffer; + stream->avail_out = bufsiz; + + if (legacy_loose_object(map)) { + inflateInit(stream); + return inflate(stream, 0); + } + + used = unpack_object_header_gently(map, mapsize, &type, &size); + if (!used || !valid_loose_object_type[type]) + return -1; + map += used; + mapsize -= used; + + /* Set up the stream for the rest.. */ + stream->next_in = map; + stream->avail_in = mapsize; + inflateInit(stream); + + /* And generate the fake traditional header */ + stream->total_out = 1 + snprintf(buffer, bufsiz, "%s %lu", + type_names[type], size); + return 0; +} + +static void *unpack_sha1_rest(z_stream *stream, void *buffer, unsigned long size) +{ + int bytes = strlen(buffer) + 1; + unsigned char *buf = xmalloc(1+size); + unsigned long n; + + n = stream->total_out - bytes; + if (n > size) + n = size; + memcpy(buf, (char *) buffer + bytes, n); + bytes = n; + if (bytes < size) { + stream->next_out = buf + bytes; + stream->avail_out = size - bytes; + while (inflate(stream, Z_FINISH) == Z_OK) + /* nothing */; + } + buf[size] = 0; + inflateEnd(stream); + return buf; +} + +/* + * We used to just use "sscanf()", but that's actually way + * too permissive for what we want to check. So do an anal + * object header parse by hand. + */ +static int parse_sha1_header(char *hdr, char *type, unsigned long *sizep) +{ + int i; + unsigned long size; + + /* + * The type can be at most ten bytes (including the + * terminating '\0' that we add), and is followed by + * a space. + */ + i = 10; + for (;;) { + char c = *hdr++; + if (c == ' ') + break; + if (!--i) + return -1; + *type++ = c; + } + *type = 0; + + /* + * The length must follow immediately, and be in canonical + * decimal format (ie "010" is not valid). + */ + size = *hdr++ - '0'; + if (size > 9) + return -1; + if (size) { + for (;;) { + unsigned long c = *hdr - '0'; + if (c > 9) + break; + hdr++; + size = size * 10 + c; + } + } + *sizep = size; + + /* + * The length must be followed by a zero byte + */ + return *hdr ? -1 : 0; +} + +void * unpack_sha1_file(void *map, unsigned long mapsize, char *type, unsigned long *size) +{ + int ret; + z_stream stream; + char hdr[8192]; + + ret = unpack_sha1_header(&stream, map, mapsize, hdr, sizeof(hdr)); + if (ret < Z_OK || parse_sha1_header(hdr, type, size) < 0) + return NULL; + + return unpack_sha1_rest(&stream, hdr, *size); +} + +static unsigned long get_delta_base(struct packed_git *p, + struct pack_window **w_curs, + unsigned long offset, + enum object_type kind, + unsigned long delta_obj_offset, + unsigned long *base_obj_offset) +{ + unsigned char *base_info = use_pack(p, w_curs, offset, NULL); + unsigned long base_offset; + + /* use_pack() assured us we have [base_info, base_info + 20) + * as a range that we can look at without walking off the + * end of the mapped window. Its actually the hash size + * that is assured. An OFS_DELTA longer than the hash size + * is stupid, as then a REF_DELTA would be smaller to store. + */ + if (kind == OBJ_OFS_DELTA) { + unsigned used = 0; + unsigned char c = base_info[used++]; + base_offset = c & 127; + while (c & 128) { + base_offset += 1; + if (!base_offset || base_offset & ~(~0UL >> 7)) + die("offset value overflow for delta base object"); + c = base_info[used++]; + base_offset = (base_offset << 7) + (c & 127); + } + base_offset = delta_obj_offset - base_offset; + if (base_offset >= delta_obj_offset) + die("delta base offset out of bound"); + offset += used; + } else if (kind == OBJ_REF_DELTA) { + /* The base entry _must_ be in the same pack */ + base_offset = find_pack_entry_one(base_info, p); + if (!base_offset) + die("failed to find delta-pack base object %s", + sha1_to_hex(base_info)); + offset += 20; + } else + die("I am totally screwed"); + *base_obj_offset = base_offset; + return offset; +} + +/* forward declaration for a mutually recursive function */ +static int packed_object_info(struct packed_git *p, unsigned long offset, + char *type, unsigned long *sizep); + +static int packed_delta_info(struct packed_git *p, + struct pack_window **w_curs, + unsigned long offset, + enum object_type kind, + unsigned long obj_offset, + char *type, + unsigned long *sizep) +{ + unsigned long base_offset; + + offset = get_delta_base(p, w_curs, offset, kind, + obj_offset, &base_offset); + + /* We choose to only get the type of the base object and + * ignore potentially corrupt pack file that expects the delta + * based on a base with a wrong size. This saves tons of + * inflate() calls. + */ + if (packed_object_info(p, base_offset, type, NULL)) + die("cannot get info for delta-pack base"); + + if (sizep) { + const unsigned char *data; + unsigned char delta_head[20], *in; + unsigned long result_size; + z_stream stream; + int st; + + memset(&stream, 0, sizeof(stream)); + stream.next_out = delta_head; + stream.avail_out = sizeof(delta_head); + + inflateInit(&stream); + do { + in = use_pack(p, w_curs, offset, &stream.avail_in); + stream.next_in = in; + st = inflate(&stream, Z_FINISH); + offset += stream.next_in - in; + } while ((st == Z_OK || st == Z_BUF_ERROR) + && stream.total_out < sizeof(delta_head)); + inflateEnd(&stream); + if ((st != Z_STREAM_END) && + stream.total_out != sizeof(delta_head)) + die("delta data unpack-initial failed"); + + /* Examine the initial part of the delta to figure out + * the result size. + */ + data = delta_head; + + /* ignore base size */ + get_delta_hdr_size(&data, delta_head+sizeof(delta_head)); + + /* Read the result size */ + result_size = get_delta_hdr_size(&data, delta_head+sizeof(delta_head)); + *sizep = result_size; + } + return 0; +} + +static unsigned long unpack_object_header(struct packed_git *p, + struct pack_window **w_curs, + unsigned long offset, + enum object_type *type, + unsigned long *sizep) +{ + unsigned char *base; + unsigned int left; + unsigned long used; + + /* use_pack() assures us we have [base, base + 20) available + * as a range that we can look at at. (Its actually the hash + * size that is assured.) With our object header encoding + * the maximum deflated object size is 2^137, which is just + * insane, so we know won't exceed what we have been given. + */ + base = use_pack(p, w_curs, offset, &left); + used = unpack_object_header_gently(base, left, type, sizep); + if (!used) + die("object offset outside of pack file"); + + return offset + used; +} + +void packed_object_info_detail(struct packed_git *p, + unsigned long offset, + char *type, + unsigned long *size, + unsigned long *store_size, + unsigned int *delta_chain_length, + unsigned char *base_sha1) +{ + struct pack_window *w_curs = NULL; + unsigned long obj_offset, val; + unsigned char *next_sha1; + enum object_type kind; + + *delta_chain_length = 0; + obj_offset = offset; + offset = unpack_object_header(p, &w_curs, offset, &kind, size); + + for (;;) { + switch (kind) { + default: + die("pack %s contains unknown object type %d", + p->pack_name, kind); + case OBJ_COMMIT: + case OBJ_TREE: + case OBJ_BLOB: + case OBJ_TAG: + strcpy(type, type_names[kind]); + *store_size = 0; /* notyet */ + unuse_pack(&w_curs); + return; + case OBJ_OFS_DELTA: + get_delta_base(p, &w_curs, offset, kind, + obj_offset, &offset); + if (*delta_chain_length == 0) { + /* TODO: find base_sha1 as pointed by offset */ + } + break; + case OBJ_REF_DELTA: + next_sha1 = use_pack(p, &w_curs, offset, NULL); + if (*delta_chain_length == 0) + hashcpy(base_sha1, next_sha1); + offset = find_pack_entry_one(next_sha1, p); + break; + } + obj_offset = offset; + offset = unpack_object_header(p, &w_curs, offset, &kind, &val); + (*delta_chain_length)++; + } +} + +static int packed_object_info(struct packed_git *p, unsigned long offset, + char *type, unsigned long *sizep) +{ + struct pack_window *w_curs = NULL; + unsigned long size, obj_offset = offset; + enum object_type kind; + int r; + + offset = unpack_object_header(p, &w_curs, offset, &kind, &size); + + switch (kind) { + case OBJ_OFS_DELTA: + case OBJ_REF_DELTA: + r = packed_delta_info(p, &w_curs, offset, kind, + obj_offset, type, sizep); + unuse_pack(&w_curs); + return r; + case OBJ_COMMIT: + case OBJ_TREE: + case OBJ_BLOB: + case OBJ_TAG: + strcpy(type, type_names[kind]); + unuse_pack(&w_curs); + break; + default: + die("pack %s contains unknown object type %d", + p->pack_name, kind); + } + if (sizep) + *sizep = size; + return 0; +} + +static void *unpack_compressed_entry(struct packed_git *p, + struct pack_window **w_curs, + unsigned long offset, + unsigned long size) +{ + int st; + z_stream stream; + unsigned char *buffer, *in; + + buffer = xmalloc(size + 1); + buffer[size] = 0; + memset(&stream, 0, sizeof(stream)); + stream.next_out = buffer; + stream.avail_out = size; + + inflateInit(&stream); + do { + in = use_pack(p, w_curs, offset, &stream.avail_in); + stream.next_in = in; + st = inflate(&stream, Z_FINISH); + offset += stream.next_in - in; + } while (st == Z_OK || st == Z_BUF_ERROR); + inflateEnd(&stream); + if ((st != Z_STREAM_END) || stream.total_out != size) { + free(buffer); + return NULL; + } + + return buffer; +} + +static void *unpack_delta_entry(struct packed_git *p, + struct pack_window **w_curs, + unsigned long offset, + unsigned long delta_size, + enum object_type kind, + unsigned long obj_offset, + char *type, + unsigned long *sizep) +{ + void *delta_data, *result, *base; + unsigned long result_size, base_size, base_offset; + + offset = get_delta_base(p, w_curs, offset, kind, + obj_offset, &base_offset); + base = unpack_entry(p, base_offset, type, &base_size); + if (!base) + die("failed to read delta base object at %lu from %s", + base_offset, p->pack_name); + + delta_data = unpack_compressed_entry(p, w_curs, offset, delta_size); + result = patch_delta(base, base_size, + delta_data, delta_size, + &result_size); + if (!result) + die("failed to apply delta"); + free(delta_data); + free(base); + *sizep = result_size; + return result; +} + +void *unpack_entry(struct packed_git *p, unsigned long offset, + char *type, unsigned long *sizep) +{ + struct pack_window *w_curs = NULL; + unsigned long size, obj_offset = offset; + enum object_type kind; + void *retval; + + offset = unpack_object_header(p, &w_curs, offset, &kind, &size); + switch (kind) { + case OBJ_OFS_DELTA: + case OBJ_REF_DELTA: + retval = unpack_delta_entry(p, &w_curs, offset, size, + kind, obj_offset, type, sizep); + break; + case OBJ_COMMIT: + case OBJ_TREE: + case OBJ_BLOB: + case OBJ_TAG: + strcpy(type, type_names[kind]); + *sizep = size; + retval = unpack_compressed_entry(p, &w_curs, offset, size); + break; + default: + die("unknown object type %i in %s", kind, p->pack_name); + } + unuse_pack(&w_curs); + return retval; +} + +int num_packed_objects(const struct packed_git *p) +{ + /* See check_packed_git_idx() */ + return (p->index_size - 20 - 20 - 4*256) / 24; +} + +int nth_packed_object_sha1(const struct packed_git *p, int n, + unsigned char* sha1) +{ + void *index = p->index_base + 256; + if (n < 0 || num_packed_objects(p) <= n) + return -1; + hashcpy(sha1, (unsigned char *) index + (24 * n) + 4); + return 0; +} + +unsigned long find_pack_entry_one(const unsigned char *sha1, + struct packed_git *p) +{ + uint32_t *level1_ofs = p->index_base; + int hi = ntohl(level1_ofs[*sha1]); + int lo = ((*sha1 == 0x0) ? 0 : ntohl(level1_ofs[*sha1 - 1])); + void *index = p->index_base + 256; + + do { + int mi = (lo + hi) / 2; + int cmp = hashcmp((unsigned char *)index + (24 * mi) + 4, sha1); + if (!cmp) + return ntohl(*((uint32_t *)((char *)index + (24 * mi)))); + if (cmp > 0) + hi = mi; + else + lo = mi+1; + } while (lo < hi); + return 0; +} + +static int matches_pack_name(struct packed_git *p, const char *ig) +{ + const char *last_c, *c; + + if (!strcmp(p->pack_name, ig)) + return 0; + + for (c = p->pack_name, last_c = c; *c;) + if (*c == '/') + last_c = ++c; + else + ++c; + if (!strcmp(last_c, ig)) + return 0; + + return 1; +} + +static int find_pack_entry(const unsigned char *sha1, struct pack_entry *e, const char **ignore_packed) +{ + struct packed_git *p; + unsigned long offset; + + prepare_packed_git(); + + for (p = packed_git; p; p = p->next) { + if (ignore_packed) { + const char **ig; + for (ig = ignore_packed; *ig; ig++) + if (!matches_pack_name(p, *ig)) + break; + if (*ig) + continue; + } + offset = find_pack_entry_one(sha1, p); + if (offset) { + /* + * We are about to tell the caller where they can + * locate the requested object. We better make + * sure the packfile is still here and can be + * accessed before supplying that answer, as + * it may have been deleted since the index + * was loaded! + */ + if (p->pack_fd == -1 && open_packed_git(p)) { + error("packfile %s cannot be accessed", p->pack_name); + continue; + } + e->offset = offset; + e->p = p; + hashcpy(e->sha1, sha1); + return 1; + } + } + return 0; +} + +struct packed_git *find_sha1_pack(const unsigned char *sha1, + struct packed_git *packs) +{ + struct packed_git *p; + + for (p = packs; p; p = p->next) { + if (find_pack_entry_one(sha1, p)) + return p; + } + return NULL; + +} + +static int sha1_loose_object_info(const unsigned char *sha1, char *type, unsigned long *sizep) +{ + int status; + unsigned long mapsize, size; + void *map; + z_stream stream; + char hdr[128]; + + map = map_sha1_file(sha1, &mapsize); + if (!map) + 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", + sha1_to_hex(sha1)); + if (parse_sha1_header(hdr, type, &size) < 0) + status = error("unable to parse %s header", sha1_to_hex(sha1)); + else { + status = 0; + if (sizep) + *sizep = size; + } + inflateEnd(&stream); + munmap(map, mapsize); + return status; +} + +int sha1_object_info(const unsigned char *sha1, char *type, unsigned long *sizep) +{ + struct pack_entry e; + + if (!find_pack_entry(sha1, &e, NULL)) { + reprepare_packed_git(); + if (!find_pack_entry(sha1, &e, NULL)) + return sha1_loose_object_info(sha1, type, sizep); + } + return packed_object_info(e.p, e.offset, type, sizep); +} + +static void *read_packed_sha1(const unsigned char *sha1, char *type, unsigned long *size) +{ + struct pack_entry e; + + if (!find_pack_entry(sha1, &e, NULL)) + return NULL; + else + return unpack_entry(e.p, e.offset, type, size); +} + +/* + * This is meant to hold a *small* number of objects that you would + * want read_sha1_file() to be able to return, but yet you do not want + * to write them into the object store (e.g. a browse-only + * application). + */ +static struct cached_object { + unsigned char sha1[20]; + const char *type; + void *buf; + unsigned long size; +} *cached_objects; +static int cached_object_nr, cached_object_alloc; + +static struct cached_object *find_cached_object(const unsigned char *sha1) +{ + int i; + struct cached_object *co = cached_objects; + + for (i = 0; i < cached_object_nr; i++, co++) { + if (!hashcmp(co->sha1, sha1)) + return co; + } + return NULL; +} + +int pretend_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *sha1) +{ + struct cached_object *co; + + hash_sha1_file(buf, len, type, sha1); + if (has_sha1_file(sha1) || find_cached_object(sha1)) + return 0; + if (cached_object_alloc <= cached_object_nr) { + cached_object_alloc = alloc_nr(cached_object_alloc); + cached_objects = xrealloc(cached_objects, + sizeof(*cached_objects) * + cached_object_alloc); + } + co = &cached_objects[cached_object_nr++]; + co->size = len; + co->type = strdup(type); + hashcpy(co->sha1, sha1); + return 0; +} + +void * read_sha1_file(const unsigned char *sha1, char *type, unsigned long *size) +{ + unsigned long mapsize; + void *map, *buf; + struct cached_object *co; + + co = find_cached_object(sha1); + if (co) { + buf = xmalloc(co->size + 1); + memcpy(buf, co->buf, co->size); + ((char*)buf)[co->size] = 0; + strcpy(type, co->type); + *size = co->size; + return buf; + } + + buf = read_packed_sha1(sha1, type, size); + if (buf) + return buf; + map = map_sha1_file(sha1, &mapsize); + if (map) { + buf = unpack_sha1_file(map, mapsize, type, size); + munmap(map, mapsize); + return buf; + } + reprepare_packed_git(); + return read_packed_sha1(sha1, type, size); +} + +void *read_object_with_reference(const unsigned char *sha1, + const char *required_type, + unsigned long *size, + unsigned char *actual_sha1_return) +{ + char type[20]; + void *buffer; + unsigned long isize; + unsigned char actual_sha1[20]; + + hashcpy(actual_sha1, sha1); + while (1) { + int ref_length = -1; + const char *ref_type = NULL; + + buffer = read_sha1_file(actual_sha1, type, &isize); + if (!buffer) + return NULL; + if (!strcmp(type, required_type)) { + *size = isize; + if (actual_sha1_return) + hashcpy(actual_sha1_return, actual_sha1); + return buffer; + } + /* Handle references */ + else if (!strcmp(type, commit_type)) + ref_type = "tree "; + else if (!strcmp(type, tag_type)) + ref_type = "object "; + else { + free(buffer); + return NULL; + } + ref_length = strlen(ref_type); + + if (memcmp(buffer, ref_type, ref_length) || + get_sha1_hex((char *) buffer + ref_length, actual_sha1)) { + free(buffer); + return NULL; + } + free(buffer); + /* Now we have the ID of the referred-to object in + * actual_sha1. Check again. */ + } +} + +static void write_sha1_file_prepare(void *buf, unsigned long len, + const char *type, unsigned char *sha1, + unsigned char *hdr, int *hdrlen) +{ + SHA_CTX c; + + /* Generate the header */ + *hdrlen = sprintf((char *)hdr, "%s %lu", type, len)+1; + + /* Sha1.. */ + SHA1_Init(&c); + SHA1_Update(&c, hdr, *hdrlen); + SHA1_Update(&c, buf, len); + SHA1_Final(sha1, &c); +} + +/* + * Link the tempfile to the final place, possibly creating the + * last directory level as you do so. + * + * Returns the errno on failure, 0 on success. + */ +static int link_temp_to_file(const char *tmpfile, const char *filename) +{ + int ret; + char *dir; + + if (!link(tmpfile, filename)) + return 0; + + /* + * Try to mkdir the last path component if that failed. + * + * Re-try the "link()" regardless of whether the mkdir + * succeeds, since a race might mean that somebody + * else succeeded. + */ + ret = errno; + dir = strrchr(filename, '/'); + if (dir) { + *dir = 0; + if (!mkdir(filename, 0777) && adjust_shared_perm(filename)) { + *dir = '/'; + return -2; + } + *dir = '/'; + if (!link(tmpfile, filename)) + return 0; + ret = errno; + } + return ret; +} + +/* + * Move the just written object into its final resting place + */ +int move_temp_to_file(const char *tmpfile, const char *filename) +{ + int ret = link_temp_to_file(tmpfile, filename); + + /* + * Coda hack - coda doesn't like cross-directory links, + * so we fall back to a rename, which will mean that it + * won't be able to check collisions, but that's not a + * big deal. + * + * The same holds for FAT formatted media. + * + * When this succeeds, we just return 0. We have nothing + * left to unlink. + */ + if (ret && ret != EEXIST) { + if (!rename(tmpfile, filename)) + return 0; + ret = errno; + } + unlink(tmpfile); + if (ret) { + if (ret != EEXIST) { + return error("unable to write sha1 filename %s: %s\n", filename, strerror(ret)); + } + /* FIXME!!! Collision check here ? */ + } + + return 0; +} + +static int write_buffer(int fd, const void *buf, size_t len) +{ + if (write_in_full(fd, buf, len) < 0) + return error("file write error (%s)", strerror(errno)); + return 0; +} + +static int write_binary_header(unsigned char *hdr, enum object_type type, unsigned long len) +{ + int hdr_len; + unsigned char c; + + c = (type << 4) | (len & 15); + len >>= 4; + hdr_len = 1; + while (len) { + *hdr++ = c | 0x80; + hdr_len++; + c = (len & 0x7f); + len >>= 7; + } + *hdr = c; + return hdr_len; +} + +static void setup_object_header(z_stream *stream, const char *type, unsigned long len) +{ + int obj_type, hdr; + + if (use_legacy_headers) { + while (deflate(stream, 0) == Z_OK) + /* nothing */; + return; + } + if (!strcmp(type, blob_type)) + obj_type = OBJ_BLOB; + else if (!strcmp(type, tree_type)) + obj_type = OBJ_TREE; + else if (!strcmp(type, commit_type)) + obj_type = OBJ_COMMIT; + else if (!strcmp(type, tag_type)) + obj_type = OBJ_TAG; + else + die("trying to generate bogus object of type '%s'", type); + hdr = write_binary_header(stream->next_out, obj_type, len); + stream->total_out = hdr; + stream->next_out += hdr; + stream->avail_out -= hdr; +} + +int hash_sha1_file(void *buf, unsigned long len, const char *type, + unsigned char *sha1) +{ + unsigned char hdr[50]; + int hdrlen; + write_sha1_file_prepare(buf, len, type, sha1, hdr, &hdrlen); + return 0; +} + +int write_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *returnsha1) +{ + int size; + unsigned char *compressed; + z_stream stream; + unsigned char sha1[20]; + char *filename; + static char tmpfile[PATH_MAX]; + unsigned char hdr[50]; + int fd, hdrlen; + + /* Normally if we have it in the pack then we do not bother writing + * it out into .git/objects/??/?{38} file. + */ + write_sha1_file_prepare(buf, len, type, sha1, hdr, &hdrlen); + filename = sha1_file_name(sha1); + if (returnsha1) + hashcpy(returnsha1, sha1); + if (has_sha1_file(sha1)) + return 0; + fd = open(filename, O_RDONLY); + if (fd >= 0) { + /* + * FIXME!!! We might do collision checking here, but we'd + * need to uncompress the old file and check it. Later. + */ + close(fd); + return 0; + } + + if (errno != ENOENT) { + return error("sha1 file %s: %s\n", filename, strerror(errno)); + } + + snprintf(tmpfile, sizeof(tmpfile), "%s/obj_XXXXXX", get_object_directory()); + + fd = mkstemp(tmpfile); + if (fd < 0) { + if (errno == EPERM) + return error("insufficient permission for adding an object to repository database %s\n", get_object_directory()); + else + return error("unable to create temporary sha1 filename %s: %s\n", tmpfile, strerror(errno)); + } + + /* Set it up */ + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, zlib_compression_level); + size = 8 + deflateBound(&stream, len+hdrlen); + compressed = xmalloc(size); + + /* Compress it */ + stream.next_out = compressed; + stream.avail_out = size; + + /* First header.. */ + stream.next_in = hdr; + stream.avail_in = hdrlen; + setup_object_header(&stream, type, len); + + /* Then the data itself.. */ + stream.next_in = buf; + stream.avail_in = len; + while (deflate(&stream, Z_FINISH) == Z_OK) + /* nothing */; + deflateEnd(&stream); + size = stream.total_out; + + if (write_buffer(fd, compressed, size) < 0) + die("unable to write sha1 file"); + fchmod(fd, 0444); + close(fd); + free(compressed); + + return move_temp_to_file(tmpfile, filename); +} + +/* + * We need to unpack and recompress the object for writing + * it out to a different file. + */ +static void *repack_object(const unsigned char *sha1, unsigned long *objsize) +{ + size_t size; + z_stream stream; + unsigned char *unpacked; + unsigned long len; + char type[20]; + char hdr[50]; + int hdrlen; + void *buf; + + /* need to unpack and recompress it by itself */ + unpacked = read_packed_sha1(sha1, type, &len); + if (!unpacked) + error("cannot read sha1_file for %s", sha1_to_hex(sha1)); + + hdrlen = sprintf(hdr, "%s %lu", type, len) + 1; + + /* Set it up */ + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, zlib_compression_level); + size = deflateBound(&stream, len + hdrlen); + buf = xmalloc(size); + + /* Compress it */ + stream.next_out = buf; + stream.avail_out = size; + + /* First header.. */ + stream.next_in = (void *)hdr; + stream.avail_in = hdrlen; + while (deflate(&stream, 0) == Z_OK) + /* nothing */; + + /* Then the data itself.. */ + stream.next_in = unpacked; + stream.avail_in = len; + while (deflate(&stream, Z_FINISH) == Z_OK) + /* nothing */; + deflateEnd(&stream); + free(unpacked); + + *objsize = stream.total_out; + return buf; +} + +int write_sha1_to_fd(int fd, const unsigned char *sha1) +{ + int retval; + unsigned long objsize; + void *buf = map_sha1_file(sha1, &objsize); + + if (buf) { + retval = write_buffer(fd, buf, objsize); + munmap(buf, objsize); + return retval; + } + + buf = repack_object(sha1, &objsize); + retval = write_buffer(fd, buf, objsize); + free(buf); + return retval; +} + +int write_sha1_from_fd(const unsigned char *sha1, int fd, char *buffer, + size_t bufsize, size_t *bufposn) +{ + char tmpfile[PATH_MAX]; + int local; + z_stream stream; + unsigned char real_sha1[20]; + unsigned char discard[4096]; + int ret; + SHA_CTX c; + + snprintf(tmpfile, sizeof(tmpfile), "%s/obj_XXXXXX", get_object_directory()); + + local = mkstemp(tmpfile); + if (local < 0) { + if (errno == EPERM) + return error("insufficient permission for adding an object to repository database %s\n", get_object_directory()); + else + return error("unable to create temporary sha1 filename %s: %s\n", tmpfile, strerror(errno)); + } + + memset(&stream, 0, sizeof(stream)); + + inflateInit(&stream); + + SHA1_Init(&c); + + do { + ssize_t size; + if (*bufposn) { + stream.avail_in = *bufposn; + stream.next_in = (unsigned char *) buffer; + do { + stream.next_out = discard; + stream.avail_out = sizeof(discard); + ret = inflate(&stream, Z_SYNC_FLUSH); + SHA1_Update(&c, discard, sizeof(discard) - + stream.avail_out); + } while (stream.avail_in && ret == Z_OK); + if (write_buffer(local, buffer, *bufposn - stream.avail_in) < 0) + die("unable to write sha1 file"); + memmove(buffer, buffer + *bufposn - stream.avail_in, + stream.avail_in); + *bufposn = stream.avail_in; + if (ret != Z_OK) + break; + } + size = xread(fd, buffer + *bufposn, bufsize - *bufposn); + if (size <= 0) { + close(local); + unlink(tmpfile); + if (!size) + return error("Connection closed?"); + perror("Reading from connection"); + return -1; + } + *bufposn += size; + } while (1); + inflateEnd(&stream); + + close(local); + SHA1_Final(real_sha1, &c); + if (ret != Z_STREAM_END) { + unlink(tmpfile); + return error("File %s corrupted", sha1_to_hex(sha1)); + } + if (hashcmp(sha1, real_sha1)) { + unlink(tmpfile); + return error("File %s has bad hash", sha1_to_hex(sha1)); + } + + return move_temp_to_file(tmpfile, sha1_file_name(sha1)); +} + +int has_pack_index(const unsigned char *sha1) +{ + struct stat st; + if (stat(sha1_pack_index_name(sha1), &st)) + return 0; + return 1; +} + +int has_pack_file(const unsigned char *sha1) +{ + struct stat st; + if (stat(sha1_pack_name(sha1), &st)) + return 0; + return 1; +} + +int has_sha1_pack(const unsigned char *sha1, const char **ignore_packed) +{ + struct pack_entry e; + return find_pack_entry(sha1, &e, ignore_packed); +} + +int has_sha1_file(const unsigned char *sha1) +{ + struct stat st; + struct pack_entry e; + + if (find_pack_entry(sha1, &e, NULL)) + return 1; + return find_sha1_file(sha1, &st) ? 1 : 0; +} + +/* + * reads from fd as long as possible into a supplied buffer of size bytes. + * If necessary the buffer's size is increased using realloc() + * + * returns 0 if anything went fine and -1 otherwise + * + * NOTE: both buf and size may change, but even when -1 is returned + * you still have to free() it yourself. + */ +int read_pipe(int fd, char** return_buf, unsigned long* return_size) +{ + char* buf = *return_buf; + unsigned long size = *return_size; + int iret; + unsigned long off = 0; + + do { + iret = xread(fd, buf + off, size - off); + if (iret > 0) { + off += iret; + if (off == size) { + size *= 2; + buf = xrealloc(buf, size); + } + } + } while (iret > 0); + + *return_buf = buf; + *return_size = off; + + if (iret < 0) + return -1; + return 0; +} + +int index_pipe(unsigned char *sha1, int fd, const char *type, int write_object) +{ + unsigned long size = 4096; + char *buf = xmalloc(size); + int ret; + + if (read_pipe(fd, &buf, &size)) { + free(buf); + return -1; + } + + if (!type) + type = blob_type; + if (write_object) + ret = write_sha1_file(buf, size, type, sha1); + else + ret = hash_sha1_file(buf, size, type, sha1); + free(buf); + return ret; +} + +int index_fd(unsigned char *sha1, int fd, struct stat *st, int write_object, const char *type) +{ + unsigned long size = st->st_size; + void *buf; + int ret; + + buf = ""; + if (size) + buf = xmmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + + if (!type) + type = blob_type; + if (write_object) + ret = write_sha1_file(buf, size, type, sha1); + else + ret = hash_sha1_file(buf, size, type, sha1); + if (size) + munmap(buf, size); + return ret; +} + +int index_path(unsigned char *sha1, const char *path, struct stat *st, int write_object) +{ + int fd; + char *target; + + switch (st->st_mode & S_IFMT) { + case S_IFREG: + fd = open(path, O_RDONLY); + if (fd < 0) + return error("open(\"%s\"): %s", path, + strerror(errno)); + if (index_fd(sha1, fd, st, write_object, NULL) < 0) + return error("%s: failed to insert into database", + path); + break; + case S_IFLNK: + target = xmalloc(st->st_size+1); + if (readlink(path, target, st->st_size+1) != st->st_size) { + char *errstr = strerror(errno); + free(target); + return error("readlink(\"%s\"): %s", path, + errstr); + } + if (!write_object) + hash_sha1_file(target, st->st_size, blob_type, sha1); + else if (write_sha1_file(target, st->st_size, blob_type, sha1)) + return error("%s: failed to insert into database", + path); + free(target); + break; + default: + return error("%s: unsupported file type", path); + } + return 0; +} + +int read_pack_header(int fd, struct pack_header *header) +{ + char *c = (char*)header; + ssize_t remaining = sizeof(struct pack_header); + do { + ssize_t r = xread(fd, c, remaining); + if (r <= 0) + /* "eof before pack header was fully read" */ + return PH_ERROR_EOF; + remaining -= r; + c += r; + } while (remaining > 0); + if (header->hdr_signature != htonl(PACK_SIGNATURE)) + /* "protocol error (pack signature mismatch detected)" */ + return PH_ERROR_PACK_SIGNATURE; + if (!pack_version_ok(header->hdr_version)) + /* "protocol error (pack version unsupported)" */ + return PH_ERROR_PROTOCOL; + return 0; +} diff --git a/sha1_name.c b/sha1_name.c new file mode 100644 index 0000000000..a7efa96f35 --- /dev/null +++ b/sha1_name.c @@ -0,0 +1,647 @@ +#include "cache.h" +#include "tag.h" +#include "commit.h" +#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) +{ + struct alternate_object_database *alt; + char hex[40]; + int found = 0; + static struct alternate_object_database *fakeent; + + if (!fakeent) { + const char *objdir = get_object_directory(); + int objdir_len = strlen(objdir); + int entlen = objdir_len + 43; + fakeent = xmalloc(sizeof(*fakeent) + entlen); + memcpy(fakeent->base, objdir, objdir_len); + fakeent->name = fakeent->base + objdir_len + 1; + fakeent->name[-1] = '/'; + } + fakeent->next = alt_odb_list; + + sprintf(hex, "%.2s", name); + for (alt = fakeent; alt && found < 2; alt = alt->next) { + struct dirent *de; + DIR *dir; + sprintf(alt->name, "%.2s/", name); + dir = opendir(alt->base); + if (!dir) + continue; + while ((de = readdir(dir)) != NULL) { + if (strlen(de->d_name) != 38) + continue; + if (memcmp(de->d_name, name + 2, len - 2)) + continue; + if (!found) { + memcpy(hex + 2, de->d_name, 38); + found++; + } + else if (memcmp(hex + 2, de->d_name, 38)) { + found = 2; + break; + } + } + closedir(dir); + } + if (found == 1) + return get_sha1_hex(hex, sha1) == 0; + return found; +} + +static int match_sha(unsigned len, const unsigned char *a, const unsigned char *b) +{ + do { + if (*a != *b) + return 0; + a++; + b++; + len -= 2; + } while (len > 1); + if (len) + if ((*a ^ *b) & 0xf0) + return 0; + return 1; +} + +static int find_short_packed_object(int len, const unsigned char *match, unsigned char *sha1) +{ + struct packed_git *p; + unsigned char found_sha1[20]; + int found = 0; + + prepare_packed_git(); + for (p = packed_git; p && found < 2; p = p->next) { + unsigned num = num_packed_objects(p); + unsigned first = 0, last = num; + while (first < last) { + unsigned mid = (first + last) / 2; + unsigned char now[20]; + int cmp; + + nth_packed_object_sha1(p, mid, now); + cmp = hashcmp(match, now); + if (!cmp) { + first = mid; + break; + } + if (cmp > 0) { + first = mid+1; + continue; + } + last = mid; + } + if (first < num) { + unsigned char now[20], next[20]; + nth_packed_object_sha1(p, first, now); + if (match_sha(len, match, now)) { + if (nth_packed_object_sha1(p, first+1, next) || + !match_sha(len, match, next)) { + /* unique within this pack */ + if (!found) { + hashcpy(found_sha1, now); + found++; + } + else if (hashcmp(found_sha1, now)) { + found = 2; + break; + } + } + else { + /* not even unique within this pack */ + found = 2; + break; + } + } + } + } + if (found == 1) + hashcpy(sha1, found_sha1); + return found; +} + +#define SHORT_NAME_NOT_FOUND (-1) +#define SHORT_NAME_AMBIGUOUS (-2) + +static int find_unique_short_object(int len, char *canonical, + unsigned char *res, unsigned char *sha1) +{ + int has_unpacked, has_packed; + unsigned char unpacked_sha1[20], packed_sha1[20]; + + has_unpacked = find_short_object_filename(len, canonical, unpacked_sha1); + has_packed = find_short_packed_object(len, res, packed_sha1); + if (!has_unpacked && !has_packed) + return SHORT_NAME_NOT_FOUND; + if (1 < has_unpacked || 1 < has_packed) + return SHORT_NAME_AMBIGUOUS; + if (has_unpacked != has_packed) { + hashcpy(sha1, (has_packed ? packed_sha1 : unpacked_sha1)); + return 0; + } + /* Both have unique ones -- do they match? */ + if (hashcmp(packed_sha1, unpacked_sha1)) + return SHORT_NAME_AMBIGUOUS; + hashcpy(sha1, packed_sha1); + return 0; +} + +static int get_short_sha1(const char *name, int len, unsigned char *sha1, + int quietly) +{ + int i, status; + char canonical[40]; + unsigned char res[20]; + + if (len < MINIMUM_ABBREV || len > 40) + return -1; + hashclr(res); + memset(canonical, 'x', 40); + for (i = 0; i < len ;i++) { + unsigned char c = name[i]; + unsigned char val; + if (c >= '0' && c <= '9') + val = c - '0'; + else if (c >= 'a' && c <= 'f') + val = c - 'a' + 10; + else if (c >= 'A' && c <='F') { + val = c - 'A' + 10; + c -= 'A' - 'a'; + } + else + return -1; + canonical[i] = c; + if (!(i & 1)) + val <<= 4; + res[i >> 1] |= val; + } + + status = find_unique_short_object(i, canonical, res, sha1); + if (!quietly && (status == SHORT_NAME_AMBIGUOUS)) + return error("short SHA1 %.*s is ambiguous.", len, canonical); + return status; +} + +const char *find_unique_abbrev(const unsigned char *sha1, int len) +{ + int status, is_null; + static char hex[41]; + + is_null = is_null_sha1(sha1); + memcpy(hex, sha1_to_hex(sha1), 40); + if (len == 40 || !len) + return hex; + while (len < 40) { + unsigned char sha1_ret[20]; + status = get_short_sha1(hex, len, sha1_ret, 1); + if (!status || + (is_null && status != SHORT_NAME_AMBIGUOUS)) { + hex[len] = 0; + return hex; + } + if (status != SHORT_NAME_AMBIGUOUS) + return NULL; + len++; + } + return NULL; +} + +static int ambiguous_path(const char *path, int len) +{ + int slash = 1; + int cnt; + + for (cnt = 0; cnt < len; cnt++) { + switch (*path++) { + case '\0': + break; + case '/': + if (slash) + break; + slash = 1; + continue; + case '.': + continue; + default: + slash = 0; + continue; + } + break; + } + return slash; +} + +static const char *ref_fmt[] = { + "%.*s", + "refs/%.*s", + "refs/tags/%.*s", + "refs/heads/%.*s", + "refs/remotes/%.*s", + "refs/remotes/%.*s/HEAD", + NULL +}; + +int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref) +{ + const char **p, *r; + int refs_found = 0; + + *ref = NULL; + for (p = ref_fmt; *p; p++) { + unsigned char sha1_from_ref[20]; + unsigned char *this_result; + + this_result = refs_found ? sha1_from_ref : sha1; + r = resolve_ref(mkpath(*p, len, str), this_result, 1, NULL); + if (r) { + if (!refs_found++) + *ref = xstrdup(r); + if (!warn_ambiguous_refs) + break; + } + } + return refs_found; +} + +int dwim_log(const char *str, int len, unsigned char *sha1, char **log) +{ + const char **p; + int logs_found = 0; + + *log = NULL; + for (p = ref_fmt; *p; p++) { + struct stat st; + unsigned char hash[20]; + char path[PATH_MAX]; + const char *ref, *it; + + strcpy(path, mkpath(*p, len, str)); + ref = resolve_ref(path, hash, 0, NULL); + if (!ref) + continue; + if (!stat(git_path("logs/%s", path), &st) && + S_ISREG(st.st_mode)) + it = path; + else if (strcmp(ref, path) && + !stat(git_path("logs/%s", ref), &st) && + S_ISREG(st.st_mode)) + it = ref; + else + continue; + if (!logs_found++) { + *log = xstrdup(it); + hashcpy(sha1, hash); + } + if (!warn_ambiguous_refs) + break; + } + return logs_found; +} + +static int get_sha1_basic(const char *str, int len, unsigned char *sha1) +{ + static const char *warning = "warning: refname '%.*s' is ambiguous.\n"; + char *real_ref = NULL; + int refs_found = 0; + int at, reflog_len; + + if (len == 40 && !get_sha1_hex(str, sha1)) + return 0; + + /* basic@{time or number} format to query ref-log */ + reflog_len = at = 0; + if (str[len-1] == '}') { + for (at = 0; at < len - 1; at++) { + if (str[at] == '@' && str[at+1] == '{') { + reflog_len = (len-1) - (at+2); + len = at; + break; + } + } + } + + /* Accept only unambiguous ref paths. */ + if (len && ambiguous_path(str, len)) + return -1; + + if (!len && reflog_len) { + /* allow "@{...}" to mean the current branch reflog */ + refs_found = dwim_ref("HEAD", 4, sha1, &real_ref); + } else if (reflog_len) + refs_found = dwim_log(str, len, sha1, &real_ref); + else + refs_found = dwim_ref(str, len, sha1, &real_ref); + + if (!refs_found) + return -1; + + if (warn_ambiguous_refs && refs_found > 1) + fprintf(stderr, warning, len, str); + + if (reflog_len) { + int nth, i; + unsigned long at_time; + unsigned long co_time; + int co_tz, co_cnt; + + /* Is it asking for N-th entry, or approxidate? */ + for (i = nth = 0; 0 <= nth && i < reflog_len; i++) { + char ch = str[at+2+i]; + if ('0' <= ch && ch <= '9') + nth = nth * 10 + ch - '0'; + else + nth = -1; + } + if (0 <= nth) + at_time = 0; + else + at_time = approxidate(str + at + 2); + if (read_ref_at(real_ref, at_time, nth, sha1, NULL, + &co_time, &co_tz, &co_cnt)) { + if (at_time) + fprintf(stderr, + "warning: Log for '%.*s' only goes " + "back to %s.\n", len, str, + show_rfc2822_date(co_time, co_tz)); + else + fprintf(stderr, + "warning: Log for '%.*s' only has " + "%d entries.\n", len, str, co_cnt); + } + } + + free(real_ref); + return 0; +} + +static int get_sha1_1(const char *name, int len, unsigned char *sha1); + +static int get_parent(const char *name, int len, + unsigned char *result, int idx) +{ + unsigned char sha1[20]; + int ret = get_sha1_1(name, len, sha1); + struct commit *commit; + struct commit_list *p; + + if (ret) + return ret; + commit = lookup_commit_reference(sha1); + if (!commit) + return -1; + if (parse_commit(commit)) + return -1; + if (!idx) { + hashcpy(result, commit->object.sha1); + return 0; + } + p = commit->parents; + while (p) { + if (!--idx) { + hashcpy(result, p->item->object.sha1); + return 0; + } + p = p->next; + } + return -1; +} + +static int get_nth_ancestor(const char *name, int len, + unsigned char *result, int generation) +{ + unsigned char sha1[20]; + int ret = get_sha1_1(name, len, sha1); + if (ret) + return ret; + + while (generation--) { + struct commit *commit = lookup_commit_reference(sha1); + + if (!commit || parse_commit(commit) || !commit->parents) + return -1; + hashcpy(sha1, commit->parents->item->object.sha1); + } + hashcpy(result, sha1); + return 0; +} + +static int peel_onion(const char *name, int len, unsigned char *sha1) +{ + unsigned char outer[20]; + const char *sp; + unsigned int expected_type = 0; + struct object *o; + + /* + * "ref^{type}" dereferences ref repeatedly until you cannot + * dereference anymore, or you get an object of given type, + * whichever comes first. "ref^{}" means just dereference + * tags until you get a non-tag. "ref^0" is a shorthand for + * "ref^{commit}". "commit^{tree}" could be used to find the + * top-level tree of the given commit. + */ + if (len < 4 || name[len-1] != '}') + return -1; + + for (sp = name + len - 1; name <= sp; sp--) { + int ch = *sp; + if (ch == '{' && name < sp && sp[-1] == '^') + break; + } + if (sp <= name) + return -1; + + sp++; /* beginning of type name, or closing brace for empty */ + if (!strncmp(commit_type, sp, 6) && sp[6] == '}') + expected_type = OBJ_COMMIT; + else if (!strncmp(tree_type, sp, 4) && sp[4] == '}') + expected_type = OBJ_TREE; + else if (!strncmp(blob_type, sp, 4) && sp[4] == '}') + expected_type = OBJ_BLOB; + else if (sp[0] == '}') + expected_type = OBJ_NONE; + else + return -1; + + if (get_sha1_1(name, sp - name - 2, outer)) + return -1; + + o = parse_object(outer); + if (!o) + return -1; + if (!expected_type) { + o = deref_tag(o, name, sp - name - 2); + if (!o || (!o->parsed && !parse_object(o->sha1))) + return -1; + hashcpy(sha1, o->sha1); + } + else { + /* At this point, the syntax look correct, so + * if we do not get the needed object, we should + * barf. + */ + + while (1) { + if (!o || (!o->parsed && !parse_object(o->sha1))) + return -1; + if (o->type == expected_type) { + hashcpy(sha1, o->sha1); + return 0; + } + if (o->type == OBJ_TAG) + o = ((struct tag*) o)->tagged; + else if (o->type == OBJ_COMMIT) + o = &(((struct commit *) o)->tree->object); + else + return error("%.*s: expected %s type, but the object dereferences to %s type", + len, name, typename(expected_type), + typename(o->type)); + if (!o->parsed) + parse_object(o->sha1); + } + } + return 0; +} + +static int get_describe_name(const char *name, int len, unsigned char *sha1) +{ + const char *cp; + + for (cp = name + len - 1; name + 2 <= cp; cp--) { + char ch = *cp; + if (hexval(ch) & ~0377) { + /* We must be looking at g in "SOMETHING-g" + * for it to be describe output. + */ + if (ch == 'g' && cp[-1] == '-') { + cp++; + len -= cp - name; + return get_short_sha1(cp, len, sha1, 1); + } + } + } + return -1; +} + +static int get_sha1_1(const char *name, int len, unsigned char *sha1) +{ + int ret, has_suffix; + const char *cp; + + /* "name~3" is "name^^^", + * "name~" and "name~0" are name -- not "name^0"! + * "name^" is not "name^0"; it is "name^1". + */ + has_suffix = 0; + for (cp = name + len - 1; name <= cp; cp--) { + int ch = *cp; + if ('0' <= ch && ch <= '9') + continue; + if (ch == '~' || ch == '^') + has_suffix = ch; + break; + } + + if (has_suffix) { + int num = 0; + int len1 = cp - name; + cp++; + while (cp < name + len) + num = num * 10 + *cp++ - '0'; + if (has_suffix == '^') { + if (!num && len1 == len - 1) + num = 1; + return get_parent(name, len1, sha1, num); + } + /* else if (has_suffix == '~') -- goes without saying */ + return get_nth_ancestor(name, len1, sha1, num); + } + + ret = peel_onion(name, len, sha1); + if (!ret) + return 0; + + ret = get_sha1_basic(name, len, sha1); + if (!ret) + return 0; + + /* It could be describe output that is "SOMETHING-gXXXX" */ + ret = get_describe_name(name, len, sha1); + if (!ret) + return 0; + + return get_short_sha1(name, len, sha1, 0); +} + +/* + * This is like "get_sha1_basic()", except it allows "sha1 expressions", + * notably "xyz^" for "parent of xyz" + */ +int get_sha1(const char *name, unsigned char *sha1) +{ + int ret, bracket_depth; + unsigned unused; + int namelen = strlen(name); + const char *cp; + + prepare_alt_odb(); + ret = get_sha1_1(name, namelen, sha1); + if (!ret) + return ret; + /* sha1:path --> object name of path in ent sha1 + * :path -> object name of path in index + * :[0-3]:path -> object name of path in index at stage + */ + if (name[0] == ':') { + int stage = 0; + struct cache_entry *ce; + int pos; + if (namelen < 3 || + name[2] != ':' || + name[1] < '0' || '3' < name[1]) + cp = name + 1; + else { + stage = name[1] - '0'; + cp = name + 3; + } + namelen = namelen - (cp - name); + if (!active_cache) + read_cache(); + if (active_nr < 0) + return -1; + pos = cache_name_pos(cp, namelen); + if (pos < 0) + pos = -pos - 1; + while (pos < active_nr) { + ce = active_cache[pos]; + if (ce_namelen(ce) != namelen || + memcmp(ce->name, cp, namelen)) + break; + if (ce_stage(ce) == stage) { + hashcpy(sha1, ce->sha1); + return 0; + } + pos++; + } + return -1; + } + 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, + &unused); + } + return ret; +} diff --git a/shallow.c b/shallow.c new file mode 100644 index 0000000000..d17868929c --- /dev/null +++ b/shallow.c @@ -0,0 +1,104 @@ +#include "cache.h" +#include "commit.h" +#include "tag.h" + +static int is_shallow = -1; + +int register_shallow(const unsigned char *sha1) +{ + struct commit_graft *graft = + xmalloc(sizeof(struct commit_graft)); + struct commit *commit = lookup_commit(sha1); + + hashcpy(graft->sha1, sha1); + graft->nr_parent = -1; + if (commit && commit->object.parsed) + commit->parents = NULL; + return register_commit_graft(graft, 0); +} + +int is_repository_shallow(void) +{ + FILE *fp; + char buf[1024]; + + if (is_shallow >= 0) + return is_shallow; + + fp = fopen(git_path("shallow"), "r"); + if (!fp) { + is_shallow = 0; + return is_shallow; + } + is_shallow = 1; + + while (fgets(buf, sizeof(buf), fp)) { + unsigned char sha1[20]; + if (get_sha1_hex(buf, sha1)) + die("bad shallow line: %s", buf); + register_shallow(sha1); + } + fclose(fp); + return is_shallow; +} + +struct commit_list *get_shallow_commits(struct object_array *heads, int depth, + int shallow_flag, int not_shallow_flag) +{ + int i = 0, cur_depth = 0; + struct commit_list *result = NULL; + struct object_array stack = {0, 0, NULL}; + struct commit *commit = NULL; + + while (commit || i < heads->nr || stack.nr) { + struct commit_list *p; + if (!commit) { + if (i < heads->nr) { + commit = (struct commit *) + deref_tag(heads->objects[i++].item, NULL, 0); + if (commit->object.type != OBJ_COMMIT) { + commit = NULL; + continue; + } + if (!commit->util) + commit->util = xmalloc(sizeof(int)); + *(int *)commit->util = 0; + cur_depth = 0; + } else { + commit = (struct commit *) + stack.objects[--stack.nr].item; + cur_depth = *(int *)commit->util; + } + } + parse_commit(commit); + commit->object.flags |= not_shallow_flag; + cur_depth++; + for (p = commit->parents, commit = NULL; p; p = p->next) { + if (!p->item->util) { + int *pointer = xmalloc(sizeof(int)); + p->item->util = pointer; + *pointer = cur_depth; + } else { + int *pointer = p->item->util; + if (cur_depth >= *pointer) + continue; + *pointer = cur_depth; + } + if (cur_depth < depth) { + if (p->next) + add_object_array(&p->item->object, + NULL, &stack); + else { + commit = p->item; + cur_depth = *(int *)commit->util; + } + } else { + commit_list_insert(p->item, &result); + p->item->object.flags |= shallow_flag; + } + } + } + + return result; +} + diff --git a/shell.c b/shell.c new file mode 100644 index 0000000000..8c08cf0fb3 --- /dev/null +++ b/shell.c @@ -0,0 +1,61 @@ +#include "cache.h" +#include "quote.h" +#include "exec_cmd.h" + +static int do_generic_cmd(const char *me, char *arg) +{ + const char *my_argv[4]; + + if (!arg || !(arg = sq_dequote(arg))) + die("bad argument"); + if (strncmp(me, "git-", 4)) + die("bad command"); + + my_argv[0] = me + 4; + my_argv[1] = arg; + my_argv[2] = NULL; + + return execv_git_cmd(my_argv); +} + +static struct commands { + const char *name; + int (*exec)(const char *me, char *arg); +} cmd_list[] = { + { "git-receive-pack", do_generic_cmd }, + { "git-upload-pack", do_generic_cmd }, + { NULL }, +}; + +int main(int argc, char **argv) +{ + char *prog; + struct commands *cmd; + + /* We want to see "-c cmd args", and nothing else */ + if (argc != 3 || strcmp(argv[1], "-c")) + die("What do you think I am? A shell?"); + + prog = argv[2]; + argv += 2; + argc -= 2; + for (cmd = cmd_list ; cmd->name ; cmd++) { + int len = strlen(cmd->name); + char *arg; + if (strncmp(cmd->name, prog, len)) + continue; + arg = NULL; + switch (prog[len]) { + case '\0': + arg = NULL; + break; + case ' ': + arg = prog + len + 1; + break; + default: + continue; + } + exit(cmd->exec(cmd->name, arg)); + } + die("unrecognized command '%s'", prog); +} diff --git a/show-index.c b/show-index.c new file mode 100644 index 0000000000..a30a2de5d1 --- /dev/null +++ b/show-index.c @@ -0,0 +1,28 @@ +#include "cache.h" + +int main(int argc, char **argv) +{ + int i; + unsigned nr; + unsigned int entry[6]; + static unsigned int top_index[256]; + + if (fread(top_index, sizeof(top_index), 1, stdin) != 1) + die("unable to read index"); + nr = 0; + for (i = 0; i < 256; i++) { + unsigned n = ntohl(top_index[i]); + if (n < nr) + die("corrupt index file"); + nr = n; + } + for (i = 0; i < nr; i++) { + unsigned offset; + + if (fread(entry, 24, 1, stdin) != 1) + die("unable to read entry %u/%u", i, nr); + offset = ntohl(entry[0]); + printf("%u %s\n", offset, sha1_to_hex((void *)(entry+1))); + } + return 0; +} diff --git a/sideband.c b/sideband.c new file mode 100644 index 0000000000..277fa3c10d --- /dev/null +++ b/sideband.c @@ -0,0 +1,78 @@ +#include "pkt-line.h" +#include "sideband.h" + +/* + * Receive multiplexed output stream over git native protocol. + * in_stream is the input stream from the remote, which carries data + * in pkt_line format with band designator. Demultiplex it into out + * and err and return error appropriately. Band #1 carries the + * primary payload. Things coming over band #2 is not necessarily + * error; they are usually informative message on the standard error + * stream, aka "verbose"). A message over band #3 is a signal that + * the remote died unexpectedly. A flush() concludes the stream. + */ +int recv_sideband(const char *me, int in_stream, int out, int err) +{ + char buf[7 + LARGE_PACKET_MAX + 1]; + strcpy(buf, "remote:"); + while (1) { + int band, len; + len = packet_read_line(in_stream, buf+7, LARGE_PACKET_MAX); + if (len == 0) + break; + if (len < 1) { + len = sprintf(buf, "%s: protocol error: no band designator\n", me); + safe_write(err, buf, len); + return SIDEBAND_PROTOCOL_ERROR; + } + band = buf[7] & 0xff; + len--; + switch (band) { + case 3: + buf[7] = ' '; + buf[8+len] = '\n'; + safe_write(err, buf, 8+len+1); + return SIDEBAND_REMOTE_ERROR; + case 2: + buf[7] = ' '; + safe_write(err, buf, 8+len); + continue; + case 1: + safe_write(out, buf+8, len); + continue; + default: + len = sprintf(buf, + "%s: protocol error: bad band #%d\n", + me, band); + safe_write(err, buf, len); + return SIDEBAND_PROTOCOL_ERROR; + } + } + return 0; +} + +/* + * fd is connected to the remote side; send the sideband data + * over multiplexed packet stream. + */ +ssize_t send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_max) +{ + ssize_t ssz = sz; + const char *p = data; + + while (sz) { + unsigned n; + char hdr[5]; + + n = sz; + if (packet_max - 5 < n) + n = packet_max - 5; + sprintf(hdr, "%04x", n + 5); + hdr[4] = band; + safe_write(fd, hdr, 5); + safe_write(fd, p, n); + p += n; + sz -= n; + } + return ssz; +} diff --git a/sideband.h b/sideband.h new file mode 100644 index 0000000000..a84b6917c7 --- /dev/null +++ b/sideband.h @@ -0,0 +1,13 @@ +#ifndef SIDEBAND_H +#define SIDEBAND_H + +#define SIDEBAND_PROTOCOL_ERROR -2 +#define SIDEBAND_REMOTE_ERROR -1 + +#define DEFAULT_PACKET_MAX 1000 +#define LARGE_PACKET_MAX 65520 + +int recv_sideband(const char *me, int in_stream, int out, int err); +ssize_t send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_max); + +#endif diff --git a/ssh-fetch.c b/ssh-fetch.c new file mode 100644 index 0000000000..bdf51a7a14 --- /dev/null +++ b/ssh-fetch.c @@ -0,0 +1,166 @@ +#ifndef COUNTERPART_ENV_NAME +#define COUNTERPART_ENV_NAME "GIT_SSH_UPLOAD" +#endif +#ifndef COUNTERPART_PROGRAM_NAME +#define COUNTERPART_PROGRAM_NAME "git-ssh-upload" +#endif +#ifndef MY_PROGRAM_NAME +#define MY_PROGRAM_NAME "git-ssh-fetch" +#endif + +#include "cache.h" +#include "commit.h" +#include "rsh.h" +#include "fetch.h" +#include "refs.h" + +static int fd_in; +static int fd_out; + +static unsigned char remote_version; +static unsigned char local_version = 1; + +static int prefetches; + +static struct object_list *in_transit; +static struct object_list **end_of_transit = &in_transit; + +void prefetch(unsigned char *sha1) +{ + char type = 'o'; + struct object_list *node; + if (prefetches > 100) { + fetch(in_transit->item->sha1); + } + node = xmalloc(sizeof(struct object_list)); + node->next = NULL; + node->item = lookup_unknown_object(sha1); + *end_of_transit = node; + end_of_transit = &node->next; + /* XXX: what if these writes fail? */ + write_in_full(fd_out, &type, 1); + write_in_full(fd_out, sha1, 20); + prefetches++; +} + +static char conn_buf[4096]; +static size_t conn_buf_posn; + +int fetch(unsigned char *sha1) +{ + int ret; + signed char remote; + struct object_list *temp; + + if (hashcmp(sha1, in_transit->item->sha1)) { + /* we must have already fetched it to clean the queue */ + return has_sha1_file(sha1) ? 0 : -1; + } + prefetches--; + temp = in_transit; + in_transit = in_transit->next; + if (!in_transit) + end_of_transit = &in_transit; + free(temp); + + if (conn_buf_posn) { + remote = conn_buf[0]; + memmove(conn_buf, conn_buf + 1, --conn_buf_posn); + } else { + if (xread(fd_in, &remote, 1) < 1) + return -1; + } + /* fprintf(stderr, "Got %d\n", remote); */ + if (remote < 0) + return remote; + ret = write_sha1_from_fd(sha1, fd_in, conn_buf, 4096, &conn_buf_posn); + if (!ret) + pull_say("got %s\n", sha1_to_hex(sha1)); + return ret; +} + +static int get_version(void) +{ + char type = 'v'; + if (write_in_full(fd_out, &type, 1) != 1 || + write_in_full(fd_out, &local_version, 1)) { + return error("Couldn't request version from remote end"); + } + if (xread(fd_in, &remote_version, 1) < 1) { + return error("Couldn't read version from remote end"); + } + return 0; +} + +int fetch_ref(char *ref, unsigned char *sha1) +{ + signed char remote; + char type = 'r'; + int length = strlen(ref) + 1; + if (write_in_full(fd_out, &type, 1) != 1 || + write_in_full(fd_out, ref, length) != length) + return -1; + + if (read_in_full(fd_in, &remote, 1) != 1) + return -1; + if (remote < 0) + return remote; + if (read_in_full(fd_in, sha1, 20) != 20) + return -1; + return 0; +} + +static const char ssh_fetch_usage[] = + MY_PROGRAM_NAME + " [-c] [-t] [-a] [-v] [--recover] [-w ref] commit-id url"; +int main(int argc, char **argv) +{ + const char *write_ref = NULL; + char *commit_id; + char *url; + int arg = 1; + const char *prog; + + prog = getenv("GIT_SSH_PUSH"); + 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') { + get_tree = 1; + } else if (argv[arg][1] == 'c') { + get_history = 1; + } else if (argv[arg][1] == 'a') { + get_all = 1; + get_tree = 1; + get_history = 1; + } else if (argv[arg][1] == 'v') { + get_verbosely = 1; + } else if (argv[arg][1] == 'w') { + write_ref = argv[arg + 1]; + arg++; + } else if (!strcmp(argv[arg], "--recover")) { + get_recover = 1; + } + arg++; + } + if (argc < arg + 2) { + usage(ssh_fetch_usage); + return 1; + } + commit_id = argv[arg]; + url = argv[arg + 1]; + + if (setup_connection(&fd_in, &fd_out, prog, url, arg, argv + 1)) + return 1; + + if (get_version()) + return 1; + + if (pull(1, &commit_id, &write_ref, url)) + return 1; + + return 0; +} diff --git a/ssh-pull.c b/ssh-pull.c new file mode 100644 index 0000000000..868ce4d41f --- /dev/null +++ b/ssh-pull.c @@ -0,0 +1,4 @@ +#define COUNTERPART_ENV_NAME "GIT_SSH_PUSH" +#define COUNTERPART_PROGRAM_NAME "git-ssh-push" +#define MY_PROGRAM_NAME "git-ssh-pull" +#include "ssh-fetch.c" diff --git a/ssh-push.c b/ssh-push.c new file mode 100644 index 0000000000..a562df1b31 --- /dev/null +++ b/ssh-push.c @@ -0,0 +1,4 @@ +#define COUNTERPART_ENV_NAME "GIT_SSH_PULL" +#define COUNTERPART_PROGRAM_NAME "git-ssh-pull" +#define MY_PROGRAM_NAME "git-ssh-push" +#include "ssh-upload.c" diff --git a/ssh-upload.c b/ssh-upload.c new file mode 100644 index 0000000000..2f04572787 --- /dev/null +++ b/ssh-upload.c @@ -0,0 +1,143 @@ +#ifndef COUNTERPART_ENV_NAME +#define COUNTERPART_ENV_NAME "GIT_SSH_FETCH" +#endif +#ifndef COUNTERPART_PROGRAM_NAME +#define COUNTERPART_PROGRAM_NAME "git-ssh-fetch" +#endif +#ifndef MY_PROGRAM_NAME +#define MY_PROGRAM_NAME "git-ssh-upload" +#endif + +#include "cache.h" +#include "rsh.h" +#include "refs.h" + +static unsigned char local_version = 1; +static unsigned char remote_version; + +static int verbose; + +static int serve_object(int fd_in, int fd_out) { + ssize_t size; + unsigned char sha1[20]; + signed char remote; + + size = read_in_full(fd_in, sha1, 20); + if (size < 0) { + perror("git-ssh-upload: read "); + return -1; + } + if (!size) + return -1; + + if (verbose) + fprintf(stderr, "Serving %s\n", sha1_to_hex(sha1)); + + remote = 0; + + if (!has_sha1_file(sha1)) { + fprintf(stderr, "git-ssh-upload: could not find %s\n", + sha1_to_hex(sha1)); + remote = -1; + } + + if (write_in_full(fd_out, &remote, 1) != 1) + return 0; + + if (remote < 0) + return 0; + + return write_sha1_to_fd(fd_out, sha1); +} + +static int serve_version(int fd_in, int fd_out) +{ + if (xread(fd_in, &remote_version, 1) < 1) + return -1; + write_in_full(fd_out, &local_version, 1); + return 0; +} + +static int serve_ref(int fd_in, int fd_out) +{ + char ref[PATH_MAX]; + unsigned char sha1[20]; + int posn = 0; + signed char remote = 0; + do { + if (posn >= PATH_MAX || xread(fd_in, ref + posn, 1) < 1) + return -1; + posn++; + } while (ref[posn - 1]); + + if (verbose) + fprintf(stderr, "Serving %s\n", ref); + + if (get_ref_sha1(ref, sha1)) + remote = -1; + if (write_in_full(fd_out, &remote, 1) != 1) + return 0; + if (remote) + return 0; + write_in_full(fd_out, sha1, 20); + return 0; +} + + +static void service(int fd_in, int fd_out) { + char type; + int retval; + do { + retval = xread(fd_in, &type, 1); + if (retval < 1) { + if (retval < 0) + perror("git-ssh-upload: read "); + return; + } + if (type == 'v' && serve_version(fd_in, fd_out)) + return; + if (type == 'o' && serve_object(fd_in, fd_out)) + return; + if (type == 'r' && serve_ref(fd_in, fd_out)) + return; + } while (1); +} + +static const char ssh_push_usage[] = + MY_PROGRAM_NAME " [-c] [-t] [-a] [-w ref] commit-id url"; + +int main(int argc, char **argv) +{ + int arg = 1; + char *commit_id; + char *url; + int fd_in, fd_out; + const char *prog; + unsigned char sha1[20]; + char hex[41]; + + prog = getenv(COUNTERPART_ENV_NAME); + if (!prog) prog = COUNTERPART_PROGRAM_NAME; + + setup_git_directory(); + + while (arg < argc && argv[arg][0] == '-') { + if (argv[arg][1] == 'w') + arg++; + arg++; + } + if (argc < arg + 2) + usage(ssh_push_usage); + commit_id = argv[arg]; + url = argv[arg + 1]; + if (get_sha1(commit_id, sha1)) + die("Not a valid object name %s", commit_id); + memcpy(hex, sha1_to_hex(sha1), sizeof(hex)); + argv[arg] = hex; + + if (setup_connection(&fd_in, &fd_out, prog, url, arg, argv + 1)) + return 1; + + service(fd_in, fd_out); + return 0; +} diff --git a/strbuf.c b/strbuf.c new file mode 100644 index 0000000000..7f14b0fb59 --- /dev/null +++ b/strbuf.c @@ -0,0 +1,42 @@ +#include "cache.h" +#include "strbuf.h" + +void strbuf_init(struct strbuf *sb) { + sb->buf = NULL; + sb->eof = sb->alloc = sb->len = 0; +} + +static void strbuf_begin(struct strbuf *sb) { + free(sb->buf); + strbuf_init(sb); +} + +static void inline strbuf_add(struct strbuf *sb, int ch) { + if (sb->alloc <= sb->len) { + sb->alloc = sb->alloc * 3 / 2 + 16; + sb->buf = xrealloc(sb->buf, sb->alloc); + } + sb->buf[sb->len++] = ch; +} + +static void strbuf_end(struct strbuf *sb) { + strbuf_add(sb, 0); +} + +void read_line(struct strbuf *sb, FILE *fp, int term) { + int ch; + strbuf_begin(sb); + if (feof(fp)) { + sb->eof = 1; + return; + } + while ((ch = fgetc(fp)) != EOF) { + if (ch == term) + break; + strbuf_add(sb, ch); + } + if (ch == EOF && sb->len == 0) + sb->eof = 1; + strbuf_end(sb); +} + diff --git a/strbuf.h b/strbuf.h new file mode 100644 index 0000000000..74cc012c2c --- /dev/null +++ b/strbuf.h @@ -0,0 +1,13 @@ +#ifndef STRBUF_H +#define STRBUF_H +struct strbuf { + int alloc; + int len; + int eof; + char *buf; +}; + +extern void strbuf_init(struct strbuf *); +extern void read_line(struct strbuf *, FILE *, int); + +#endif /* STRBUF_H */ diff --git a/t/.gitignore b/t/.gitignore new file mode 100644 index 0000000000..fad67c097b --- /dev/null +++ b/t/.gitignore @@ -0,0 +1 @@ +trash diff --git a/t/Makefile b/t/Makefile new file mode 100644 index 0000000000..19e38508a7 --- /dev/null +++ b/t/Makefile @@ -0,0 +1,31 @@ +# Run tests +# +# Copyright (c) 2005 Junio C Hamano +# + +#GIT_TEST_OPTS=--verbose --debug +SHELL_PATH ?= $(SHELL) +TAR ?= $(TAR) + +# Shell quote; +SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) + +T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) +TSVN = $(wildcard t91[0-9][0-9]-*.sh) + +all: $(T) clean + +$(T): + @echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS) + +clean: + rm -fr trash + +# we can test NO_OPTIMIZE_COMMITS independently of LC_ALL +full-svn-test: + $(MAKE) $(TSVN) GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C + $(MAKE) $(TSVN) GIT_SVN_NO_OPTIMIZE_COMMITS=0 LC_ALL=en_US.UTF-8 + +.PHONY: $(T) clean +.NOTPARALLEL: + diff --git a/t/README b/t/README new file mode 100644 index 0000000000..36f2517617 --- /dev/null +++ b/t/README @@ -0,0 +1,211 @@ +Core GIT Tests +============== + +This directory holds many test scripts for core GIT tools. The +first part of this short document describes how to run the tests +and read their output. + +When fixing the tools or adding enhancements, you are strongly +encouraged to add tests in this directory to cover what you are +trying to fix or enhance. The later part of this short document +describes how your test scripts should be organized. + + +Running Tests +------------- + +The easiest way to run tests is to say "make". This runs all +the tests. + + *** t0000-basic.sh *** + * ok 1: .git/objects should be empty after git-init in an empty repo. + * ok 2: .git/objects should have 256 subdirectories. + * ok 3: git-update-index without --add should fail adding. + ... + * ok 23: no diff after checkout and git-update-index --refresh. + * passed all 23 test(s) + *** t0100-environment-names.sh *** + * ok 1: using old names should issue warnings. + * ok 2: using old names but having new names should not issue warnings. + ... + +Or you can run each test individually from command line, like +this: + + $ sh ./t3001-ls-files-killed.sh + * ok 1: git-update-index --add to add various paths. + * ok 2: git-ls-files -k to show killed files. + * ok 3: validate git-ls-files -k output. + * passed all 3 test(s) + +You can pass --verbose (or -v), --debug (or -d), and --immediate +(or -i) command line argument to the test. + +--verbose:: + This makes the test more verbose. Specifically, the + command being run and their output if any are also + output. + +--debug:: + This may help the person who is developing a new test. + It causes the command defined with test_debug to run. + +--immediate:: + This causes the test to immediately exit upon the first + failed test. + + +Naming Tests +------------ + +The test files are named as: + + tNNNN-commandname-details.sh + +where N is a decimal digit. + +First digit tells the family: + + 0 - the absolute basics and global stuff + 1 - the basic commands concerning database + 2 - the basic commands concerning the working tree + 3 - the other basic commands (e.g. ls-files) + 4 - the diff commands + 5 - the pull and exporting commands + 6 - the revision tree commands (even e.g. merge-base) + 7 - the porcelainish commands concerning the working tree + 8 - the porcelainish commands concerning forensics + 9 - the git tools + +Second digit tells the particular command we are testing. + +Third digit (optionally) tells the particular switch or group of switches +we are testing. + +If you create files under t/ directory (i.e. here) that is not +the top-level test script, never name the file to match the above +pattern. The Makefile here considers all such files as the +top-level test script and tries to run all of them. A care is +especially needed if you are creating a common test library +file, similar to test-lib.sh, because such a library file may +not be suitable for standalone execution. + + +Writing Tests +------------- + +The test script is written as a shell script. It should start +with the standard "#!/bin/sh" with copyright notices, and an +assignment to variable 'test_description', like this: + + #!/bin/sh + # + # Copyright (c) 2005 Junio C Hamano + # + + test_description='xxx test (option --frotz) + + This test registers the following structure in the cache + and tries to run git-ls-files with option --frotz.' + + +Source 'test-lib.sh' +-------------------- + +After assigning test_description, the test script should source +test-lib.sh like this: + + . ./test-lib.sh + +This test harness library does the following things: + + - If the script is invoked with command line argument --help + (or -h), it shows the test_description and exits. + + - Creates an empty test directory with an empty .git/objects + database and chdir(2) into it. This directory is 't/trash' + if you must know, but I do not think you care. + + - Defines standard test helper functions for your scripts to + use. These functions are designed to make all scripts behave + consistently when command line arguments --verbose (or -v), + --debug (or -d), and --immediate (or -i) is given. + + +End with test_done +------------------ + +Your script will be a sequence of tests, using helper functions +from the test harness library. At the end of the script, call +'test_done'. + + +Test harness library +-------------------- + +There are a handful helper functions defined in the test harness +library for your script to use. + + - test_expect_success <message> <script> + + This takes two strings as parameter, and evaluates the + <script>. If it yields success, test is considered + successful. <message> should state what it is testing. + + Example: + + test_expect_success \ + 'git-write-tree should be able to write an empty tree.' \ + 'tree=$(git-write-tree)' + + - test_expect_failure <message> <script> + + This is the opposite of test_expect_success. If <script> + yields success, test is considered a failure. + + Example: + + test_expect_failure \ + 'git-update-index without --add should fail adding.' \ + 'git-update-index should-be-empty' + + - test_debug <script> + + This takes a single argument, <script>, and evaluates it only + when the test script is started with --debug command line + argument. This is primarily meant for use during the + development of a new test script. + + - test_done + + Your test script must have test_done at the end. Its purpose + is to summarize successes and failures in the test script and + exit with an appropriate error code. + + +Tips for Writing Tests +---------------------- + +As with any programming projects, existing programs are the best +source of the information. However, do _not_ emulate +t0000-basic.sh when writing your tests. The test is special in +that it tries to validate the very core of GIT. For example, it +knows that there will be 256 subdirectories under .git/objects/, +and it knows that the object ID of an empty tree is a certain +40-byte string. This is deliberately done so in t0000-basic.sh +because the things the very basic core test tries to achieve is +to serve as a basis for people who are changing the GIT internal +drastically. For these people, after making certain changes, +not seeing failures from the basic test _is_ a failure. And +such drastic changes to the core GIT that even changes these +otherwise supposedly stable object IDs should be accompanied by +an update to t0000-basic.sh. + +However, other tests that simply rely on basic parts of the core +GIT working properly should not have that level of intimate +knowledge of the core GIT internals. If all the test scripts +hardcoded the object IDs like t0000-basic.sh does, that defeats +the purpose of t0000-basic.sh, which is to isolate that level of +validation in one place. Your test also ends up needing +updating when such a change to the internal happens, so do _not_ +do it and leave the low level of validation to t0000-basic.sh. diff --git a/t/annotate-tests.sh b/t/annotate-tests.sh new file mode 100644 index 0000000000..87403da780 --- /dev/null +++ b/t/annotate-tests.sh @@ -0,0 +1,122 @@ +# This file isn't used as a test script directly, instead it is +# sourced from t8001-annotate.sh and t8001-blame.sh. + +check_count () { + head= + case "$1" in -h) head="$2"; shift; shift ;; esac + echo "$PROG file $head" >&4 + $PROG file $head >.result || return 1 + cat .result | perl -e ' + my %expect = (@ARGV); + my %count = (); + while (<STDIN>) { + if (/^[0-9a-f]+\t\(([^\t]+)\t/) { + my $author = $1; + for ($author) { s/^\s*//; s/\s*$//; } + if (exists $expect{$author}) { + $count{$author}++; + } + } + } + my $bad = 0; + while (my ($author, $count) = each %count) { + my $ok; + if ($expect{$author} != $count) { + $bad = 1; + $ok = "bad"; + } + else { + $ok = "good"; + } + print STDERR "Author $author (expected $expect{$author}, attributed $count) $ok\n"; + } + exit($bad); + ' "$@" +} + +test_expect_success \ + 'prepare reference tree' \ + 'echo "1A quick brown fox jumps over the" >file && + echo "lazy dog" >>file && + git add file + GIT_AUTHOR_NAME="A" git commit -a -m "Initial."' + +test_expect_success \ + 'check all lines blamed on A' \ + 'check_count A 2' + +test_expect_success \ + 'Setup new lines blamed on B' \ + 'echo "2A quick brown fox jumps over the" >>file && + echo "lazy dog" >> file && + GIT_AUTHOR_NAME="B" git commit -a -m "Second."' + +test_expect_success \ + 'Two lines blamed on A, two on B' \ + 'check_count A 2 B 2' + +test_expect_success \ + 'merge-setup part 1' \ + 'git checkout -b branch1 master && + echo "3A slow green fox jumps into the" >> file && + echo "well." >> file && + GIT_AUTHOR_NAME="B1" git commit -a -m "Branch1-1"' + +test_expect_success \ + 'Two lines blamed on A, two on B, two on B1' \ + 'check_count A 2 B 2 B1 2' + +test_expect_success \ + 'merge-setup part 2' \ + 'git checkout -b branch2 master && + sed -e "s/2A quick brown/4A quick brown lazy dog/" < file > file.new && + mv file.new file && + GIT_AUTHOR_NAME="B2" git commit -a -m "Branch2-1"' + +test_expect_success \ + 'Two lines blamed on A, one on B, one on B2' \ + 'check_count A 2 B 1 B2 1' + +test_expect_success \ + 'merge-setup part 3' \ + 'git pull . branch1' + +test_expect_success \ + 'Two lines blamed on A, one on B, two on B1, one on B2' \ + 'check_count A 2 B 1 B1 2 B2 1' + +test_expect_success \ + 'Annotating an old revision works' \ + 'check_count -h master A 2 B 2' + +test_expect_success \ + 'Annotating an old revision works' \ + 'check_count -h master^ A 2' + +test_expect_success \ + 'merge-setup part 4' \ + 'echo "evil merge." >>file && + git commit -a --amend' + +test_expect_success \ + 'Two lines blamed on A, one on B, two on B1, one on B2, one on A U Thor' \ + 'check_count A 2 B 1 B1 2 B2 1 "A U Thor" 1' + +test_expect_success \ + 'an incomplete line added' \ + 'echo "incomplete" | tr -d "\\012" >>file && + GIT_AUTHOR_NAME="C" git commit -a -m "Incomplete"' + +test_expect_success \ + 'With incomplete lines.' \ + 'check_count A 2 B 1 B1 2 B2 1 "A U Thor" 1 C 1' + +test_expect_success \ + 'some edit' \ + 'mv file file.orig && + sed -e "s/^3A/99/" -e "/^1A/d" < file.orig > file && + GIT_AUTHOR_NAME="D" git commit -a -m "edit"' + +test_expect_success \ + 'some edit' \ + 'check_count A 1 B 1 B1 1 B2 1 "A U Thor" 1 C 1 D 1' diff --git a/t/diff-lib.sh b/t/diff-lib.sh new file mode 100755 index 0000000000..745a1b0311 --- /dev/null +++ b/t/diff-lib.sh @@ -0,0 +1,41 @@ +: + +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" +sanitize_diff_raw='/^:/s/ '"$_x40"' '"$_x40"' \([A-Z]\)[0-9]* / X X \1# /' +compare_diff_raw () { + # When heuristics are improved, the score numbers would change. + # Ignore them while comparing. + # Also we do not check SHA1 hash generation in this test, which + # is a job for t0000-basic.sh + + sed -e "$sanitize_diff_raw" <"$1" >.tmp-1 + sed -e "$sanitize_diff_raw" <"$2" >.tmp-2 + diff -u .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2 +} + +sanitize_diff_raw_z='/^:/s/ '"$_x40"' '"$_x40"' \([A-Z]\)[0-9]*$/ X X \1#/' +compare_diff_raw_z () { + # When heuristics are improved, the score numbers would change. + # Ignore them while comparing. + # Also we do not check SHA1 hash generation in this test, which + # is a job for t0000-basic.sh + + tr '\0' '\012' <"$1" | sed -e "$sanitize_diff_raw_z" >.tmp-1 + tr '\0' '\012' <"$2" | sed -e "$sanitize_diff_raw_z" >.tmp-2 + diff -u .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2 +} + +compare_diff_patch () { + # When heuristics are improved, the score numbers would change. + # Ignore them while comparing. + sed -e ' + /^[dis]*imilarity index [0-9]*%$/d + /^index [0-9a-f]*\.\.[0-9a-f]/d + ' <"$1" >.tmp-1 + sed -e ' + /^[dis]*imilarity index [0-9]*%$/d + /^index [0-9a-f]*\.\.[0-9a-f]/d + ' <"$2" >.tmp-2 + diff -u .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2 +} diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh new file mode 100644 index 0000000000..bb1d7b84bc --- /dev/null +++ b/t/lib-git-svn.sh @@ -0,0 +1,47 @@ +. ./test-lib.sh + +if test -n "$NO_SVN_TESTS" +then + test_expect_success 'skipping git-svn tests, NO_SVN_TESTS defined' : + test_done + exit +fi + +GIT_DIR=$PWD/.git +GIT_SVN_DIR=$GIT_DIR/svn/git-svn +SVN_TREE=$GIT_SVN_DIR/svn-tree + +svn >/dev/null 2>&1 +if test $? -ne 1 +then + test_expect_success 'skipping git-svn tests, svn not found' : + test_done + exit +fi + +svnrepo=$PWD/svnrepo + +perl -w -e " +use SVN::Core; +use SVN::Repos; +\$SVN::Core::VERSION gt '1.1.0' or exit(42); +system(qw/svnadmin create --fs-type fsfs/, '$svnrepo') == 0 or exit(41); +" >&3 2>&4 +x=$? +if test $x -ne 0 +then + if test $x -eq 42; then + err='Perl SVN libraries must be >= 1.1.0' + elif test $x -eq 41; then + err='svnadmin failed to create fsfs repository' + else + err='Perl SVN libraries not found or unusable, skipping test' + fi + test_expect_success "$err" : + test_done + exit +fi + +svnrepo="file://$svnrepo" + + diff --git a/t/lib-read-tree-m-3way.sh b/t/lib-read-tree-m-3way.sh new file mode 100755 index 0000000000..d195603dfa --- /dev/null +++ b/t/lib-read-tree-m-3way.sh @@ -0,0 +1,158 @@ +: Included from t1000-read-tree-m-3way.sh and others +# Original tree. +mkdir Z +for a in N D M +do + for b in N D M + do + p=$a$b + echo This is $p from the original tree. >$p + echo This is Z/$p from the original tree. >Z/$p + test_expect_success \ + "adding test file $p and Z/$p" \ + 'git-update-index --add $p && + git-update-index --add Z/$p' + done +done +echo This is SS from the original tree. >SS +test_expect_success \ + 'adding test file SS' \ + 'git-update-index --add SS' +cat >TT <<\EOF +This is a trivial merge sample text. +Branch A is expected to upcase this word, here. +There are some filler lines to avoid diff context +conflicts here, +like this one, +and this one, +and this one is yet another one of them. +At the very end, here comes another line, that is +the word, expected to be upcased by Branch B. +This concludes the trivial merge sample file. +EOF +test_expect_success \ + 'adding test file TT' \ + 'git-update-index --add TT' +test_expect_success \ + 'prepare initial tree' \ + 'tree_O=$(git-write-tree)' + +################################################################ +# Branch A and B makes the changes according to the above matrix. + +################################################################ +# Branch A + +to_remove=$(echo D? Z/D?) +rm -f $to_remove +test_expect_success \ + 'change in branch A (removal)' \ + 'git-update-index --remove $to_remove' + +for p in M? Z/M? +do + echo This is modified $p in the branch A. >$p + test_expect_success \ + 'change in branch A (modification)' \ + "git-update-index $p" +done + +for p in AN AA Z/AN Z/AA +do + echo This is added $p in the branch A. >$p + test_expect_success \ + 'change in branch A (addition)' \ + "git-update-index --add $p" +done + +echo This is SS from the modified tree. >SS +echo This is LL from the modified tree. >LL +test_expect_success \ + 'change in branch A (addition)' \ + 'git-update-index --add LL && + git-update-index SS' +mv TT TT- +sed -e '/Branch A/s/word/WORD/g' <TT- >TT +rm -f TT- +test_expect_success \ + 'change in branch A (edit)' \ + 'git-update-index TT' + +mkdir DF +echo Branch A makes a file at DF/DF, creating a directory DF. >DF/DF +test_expect_success \ + 'change in branch A (change file to directory)' \ + 'git-update-index --add DF/DF' + +test_expect_success \ + 'recording branch A tree' \ + 'tree_A=$(git-write-tree)' + +################################################################ +# Branch B +# Start from O + +rm -rf [NDMASLT][NDMASLT] Z DF +mkdir Z +test_expect_success \ + 'reading original tree and checking out' \ + 'git-read-tree $tree_O && + git-checkout-index -a' + +to_remove=$(echo ?D Z/?D) +rm -f $to_remove +test_expect_success \ + 'change in branch B (removal)' \ + "git-update-index --remove $to_remove" + +for p in ?M Z/?M +do + echo This is modified $p in the branch B. >$p + test_expect_success \ + 'change in branch B (modification)' \ + "git-update-index $p" +done + +for p in NA AA Z/NA Z/AA +do + echo This is added $p in the branch B. >$p + test_expect_success \ + 'change in branch B (addition)' \ + "git-update-index --add $p" +done +echo This is SS from the modified tree. >SS +echo This is LL from the modified tree. >LL +test_expect_success \ + 'change in branch B (addition and modification)' \ + 'git-update-index --add LL && + git-update-index SS' +mv TT TT- +sed -e '/Branch B/s/word/WORD/g' <TT- >TT +rm -f TT- +test_expect_success \ + 'change in branch B (modification)' \ + 'git-update-index TT' + +echo Branch B makes a file at DF. >DF +test_expect_success \ + 'change in branch B (addition of a file to conflict with directory)' \ + 'git-update-index --add DF' + +test_expect_success \ + 'recording branch B tree' \ + 'tree_B=$(git-write-tree)' + +test_expect_success \ + 'keep contents of 3 trees for easy access' \ + 'rm -f .git/index && + git-read-tree $tree_O && + mkdir .orig-O && + git-checkout-index --prefix=.orig-O/ -f -q -a && + rm -f .git/index && + git-read-tree $tree_A && + mkdir .orig-A && + git-checkout-index --prefix=.orig-A/ -f -q -a && + rm -f .git/index && + git-read-tree $tree_B && + mkdir .orig-B && + git-checkout-index --prefix=.orig-B/ -f -q -a' diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh new file mode 100755 index 0000000000..186de70243 --- /dev/null +++ b/t/t0000-basic.sh @@ -0,0 +1,284 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Test the very basics part #1. + +The rest of the test suite does not check the basic operation of git +plumbing commands to work very carefully. Their job is to concentrate +on tricky features that caused bugs in the past to detect regression. + +This test runs very basic features, like registering things in cache, +writing tree, etc. + +Note that this test *deliberately* hard-codes many expected object +IDs. When object ID computation changes, like in the previous case of +swapping compression and hashing order, the person who is making the +modification *should* take notice and update the test vectors here. +' + +################################################################ +# It appears that people try to run tests without building... + +../git >/dev/null +if test $? != 1 +then + echo >&2 'You do not seem to have built git yet.' + exit 1 +fi + +. ./test-lib.sh + +################################################################ +# git-init has been done in an empty repository. +# make sure it is empty. + +find .git/objects -type f -print >should-be-empty +test_expect_success \ + '.git/objects should be empty after git-init in an empty repo.' \ + 'cmp -s /dev/null should-be-empty' + +# also it should have 2 subdirectories; no fan-out anymore, pack, and info. +# 3 is counting "objects" itself +find .git/objects -type d -print >full-of-directories +test_expect_success \ + '.git/objects should have 3 subdirectories.' \ + 'test $(wc -l < full-of-directories) = 3' + +################################################################ +# Basics of the basics + +# updating a new file without --add should fail. +test_expect_failure \ + 'git-update-index without --add should fail adding.' \ + 'git-update-index should-be-empty' + +# and with --add it should succeed, even if it is empty (it used to fail). +test_expect_success \ + 'git-update-index with --add should succeed.' \ + 'git-update-index --add should-be-empty' + +test_expect_success \ + 'writing tree out with git-write-tree' \ + 'tree=$(git-write-tree)' + +# we know the shape and contents of the tree and know the object ID for it. +test_expect_success \ + 'validate object ID of a known tree.' \ + 'test "$tree" = 7bb943559a305bdd6bdee2cef6e5df2413c3d30a' + +# Removing paths. +rm -f should-be-empty full-of-directories +test_expect_failure \ + 'git-update-index without --remove should fail removing.' \ + 'git-update-index should-be-empty' + +test_expect_success \ + 'git-update-index with --remove should be able to remove.' \ + 'git-update-index --remove should-be-empty' + +# Empty tree can be written with recent write-tree. +test_expect_success \ + 'git-write-tree should be able to write an empty tree.' \ + 'tree=$(git-write-tree)' + +test_expect_success \ + 'validate object ID of a known tree.' \ + 'test "$tree" = 4b825dc642cb6eb9a060e54bf8d69288fbee4904' + +# Various types of objects +mkdir path2 path3 path3/subp3 +for p in path0 path2/file2 path3/file3 path3/subp3/file3 +do + echo "hello $p" >$p + ln -s "hello $p" ${p}sym +done +test_expect_success \ + 'adding various types of objects with git-update-index --add.' \ + 'find path* ! -type d -print | xargs git-update-index --add' + +# Show them and see that matches what we expect. +test_expect_success \ + 'showing stage with git-ls-files --stage' \ + 'git-ls-files --stage >current' + +cat >expected <<\EOF +100644 f87290f8eb2cbbea7857214459a0739927eab154 0 path0 +120000 15a98433ae33114b085f3eb3bb03b832b3180a01 0 path0sym +100644 3feff949ed00a62d9f7af97c15cd8a30595e7ac7 0 path2/file2 +120000 d8ce161addc5173867a3c3c730924388daedbc38 0 path2/file2sym +100644 0aa34cae68d0878578ad119c86ca2b5ed5b28376 0 path3/file3 +120000 8599103969b43aff7e430efea79ca4636466794f 0 path3/file3sym +100644 00fb5908cb97c2564a9783c0c64087333b3b464f 0 path3/subp3/file3 +120000 6649a1ebe9e9f1c553b66f5a6e74136a07ccc57c 0 path3/subp3/file3sym +EOF +test_expect_success \ + 'validate git-ls-files output for a known tree.' \ + 'diff current expected' + +test_expect_success \ + 'writing tree out with git-write-tree.' \ + 'tree=$(git-write-tree)' +test_expect_success \ + 'validate object ID for a known tree.' \ + 'test "$tree" = 087704a96baf1c2d1c869a8b084481e121c88b5b' + +test_expect_success \ + 'showing tree with git-ls-tree' \ + 'git-ls-tree $tree >current' +cat >expected <<\EOF +100644 blob f87290f8eb2cbbea7857214459a0739927eab154 path0 +120000 blob 15a98433ae33114b085f3eb3bb03b832b3180a01 path0sym +040000 tree 58a09c23e2ca152193f2786e06986b7b6712bdbe path2 +040000 tree 21ae8269cacbe57ae09138dcc3a2887f904d02b3 path3 +EOF +test_expect_success \ + 'git-ls-tree output for a known tree.' \ + 'diff current expected' + +# This changed in ls-tree pathspec change -- recursive does +# not show tree nodes anymore. +test_expect_success \ + 'showing tree with git-ls-tree -r' \ + 'git-ls-tree -r $tree >current' +cat >expected <<\EOF +100644 blob f87290f8eb2cbbea7857214459a0739927eab154 path0 +120000 blob 15a98433ae33114b085f3eb3bb03b832b3180a01 path0sym +100644 blob 3feff949ed00a62d9f7af97c15cd8a30595e7ac7 path2/file2 +120000 blob d8ce161addc5173867a3c3c730924388daedbc38 path2/file2sym +100644 blob 0aa34cae68d0878578ad119c86ca2b5ed5b28376 path3/file3 +120000 blob 8599103969b43aff7e430efea79ca4636466794f path3/file3sym +100644 blob 00fb5908cb97c2564a9783c0c64087333b3b464f path3/subp3/file3 +120000 blob 6649a1ebe9e9f1c553b66f5a6e74136a07ccc57c path3/subp3/file3sym +EOF +test_expect_success \ + 'git-ls-tree -r output for a known tree.' \ + 'diff current expected' + +# But with -r -t we can have both. +test_expect_success \ + 'showing tree with git-ls-tree -r -t' \ + 'git-ls-tree -r -t $tree >current' +cat >expected <<\EOF +100644 blob f87290f8eb2cbbea7857214459a0739927eab154 path0 +120000 blob 15a98433ae33114b085f3eb3bb03b832b3180a01 path0sym +040000 tree 58a09c23e2ca152193f2786e06986b7b6712bdbe path2 +100644 blob 3feff949ed00a62d9f7af97c15cd8a30595e7ac7 path2/file2 +120000 blob d8ce161addc5173867a3c3c730924388daedbc38 path2/file2sym +040000 tree 21ae8269cacbe57ae09138dcc3a2887f904d02b3 path3 +100644 blob 0aa34cae68d0878578ad119c86ca2b5ed5b28376 path3/file3 +120000 blob 8599103969b43aff7e430efea79ca4636466794f path3/file3sym +040000 tree 3c5e5399f3a333eddecce7a9b9465b63f65f51e2 path3/subp3 +100644 blob 00fb5908cb97c2564a9783c0c64087333b3b464f path3/subp3/file3 +120000 blob 6649a1ebe9e9f1c553b66f5a6e74136a07ccc57c path3/subp3/file3sym +EOF +test_expect_success \ + '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' + +cat >badobjects <<EOF +100644 blob 1000000000000000000000000000000000000000 dir/file1 +100644 blob 2000000000000000000000000000000000000000 dir/file2 +100644 blob 3000000000000000000000000000000000000000 dir/file3 +100644 blob 4000000000000000000000000000000000000000 dir/file4 +100644 blob 5000000000000000000000000000000000000000 dir/file5 +EOF + +rm .git/index +test_expect_success \ + 'put invalid objects into the index.' \ + 'git-update-index --index-info < badobjects' + +test_expect_failure \ + 'writing this tree without --missing-ok.' \ + 'git-write-tree' + +test_expect_success \ + 'writing this tree with --missing-ok.' \ + 'git-write-tree --missing-ok' + + +################################################################ +rm .git/index +test_expect_success \ + 'git-read-tree followed by write-tree should be idempotent.' \ + 'git-read-tree $tree && + test -f .git/index && + newtree=$(git-write-tree) && + test "$newtree" = "$tree"' + +cat >expected <<\EOF +:100644 100644 f87290f8eb2cbbea7857214459a0739927eab154 0000000000000000000000000000000000000000 M path0 +:120000 120000 15a98433ae33114b085f3eb3bb03b832b3180a01 0000000000000000000000000000000000000000 M path0sym +:100644 100644 3feff949ed00a62d9f7af97c15cd8a30595e7ac7 0000000000000000000000000000000000000000 M path2/file2 +:120000 120000 d8ce161addc5173867a3c3c730924388daedbc38 0000000000000000000000000000000000000000 M path2/file2sym +:100644 100644 0aa34cae68d0878578ad119c86ca2b5ed5b28376 0000000000000000000000000000000000000000 M path3/file3 +:120000 120000 8599103969b43aff7e430efea79ca4636466794f 0000000000000000000000000000000000000000 M path3/file3sym +:100644 100644 00fb5908cb97c2564a9783c0c64087333b3b464f 0000000000000000000000000000000000000000 M path3/subp3/file3 +:120000 120000 6649a1ebe9e9f1c553b66f5a6e74136a07ccc57c 0000000000000000000000000000000000000000 M path3/subp3/file3sym +EOF +test_expect_success \ + 'validate git-diff-files output for a know cache/work tree state.' \ + 'git-diff-files >current && diff >/dev/null -b current expected' + +test_expect_success \ + 'git-update-index --refresh should succeed.' \ + 'git-update-index --refresh' + +test_expect_success \ + 'no diff after checkout and git-update-index --refresh.' \ + 'git-diff-files >current && cmp -s current /dev/null' + +################################################################ +P=087704a96baf1c2d1c869a8b084481e121c88b5b +test_expect_success \ + 'git-commit-tree records the correct tree in a commit.' \ + 'commit0=$(echo NO | git-commit-tree $P) && + tree=$(git show --pretty=raw $commit0 | + sed -n -e "s/^tree //p" -e "/^author /q") && + test "z$tree" = "z$P"' + +test_expect_success \ + 'git-commit-tree records the correct parent in a commit.' \ + 'commit1=$(echo NO | git-commit-tree $P -p $commit0) && + parent=$(git show --pretty=raw $commit1 | + sed -n -e "s/^parent //p" -e "/^author /q") && + test "z$commit0" = "z$parent"' + +test_expect_success \ + 'git-commit-tree omits duplicated parent in a commit.' \ + 'commit2=$(echo NO | git-commit-tree $P -p $commit0 -p $commit0) && + parent=$(git show --pretty=raw $commit2 | + sed -n -e "s/^parent //p" -e "/^author /q" | + sort -u) && + test "z$commit0" = "z$parent" && + numparent=$(git show --pretty=raw $commit2 | + sed -n -e "s/^parent //p" -e "/^author /q" | + wc -l) && + test $numparent = 1' + +test_expect_success 'update-index D/F conflict' ' + mv path0 tmp && + mv path2 path0 && + mv tmp path2 && + git update-index --add --replace path2 path0/file2 && + numpath0=$(git ls-files path0 | wc -l) && + test $numpath0 = 1 +' + +test_done diff --git a/t/t0010-racy-git.sh b/t/t0010-racy-git.sh new file mode 100755 index 0000000000..e45a9e40e4 --- /dev/null +++ b/t/t0010-racy-git.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +test_description='racy GIT' + +. ./test-lib.sh + +# This test can give false success if your machine is sufficiently +# slow or your trial happened to happen on second boundary. + +for trial in 0 1 2 3 4 +do + rm -f .git/index + echo frotz >infocom + git update-index --add infocom + echo xyzzy >infocom + + files=`git diff-files -p` + test_expect_success \ + "Racy GIT trial #$trial part A" \ + 'test "" != "$files"' + + sleep 1 + echo xyzzy >cornerstone + git update-index --add cornerstone + + files=`git diff-files -p` + test_expect_success \ + "Racy GIT trial #$trial part B" \ + 'test "" != "$files"' + +done + +test_done diff --git a/t/t1000-read-tree-m-3way.sh b/t/t1000-read-tree-m-3way.sh new file mode 100755 index 0000000000..d0af8c3d52 --- /dev/null +++ b/t/t1000-read-tree-m-3way.sh @@ -0,0 +1,514 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Three way merge with read-tree -m + +This test tries three-way merge with read-tree -m + +There is one ancestor (called O for Original) and two branches A +and B derived from it. We want to do a 3-way merge between A and +B, using O as the common ancestor. + + merge A O B + +Decisions are made by comparing contents of O, A and B pathname +by pathname. The result is determined by the following guiding +principle: + + - If only A does something to it and B does not touch it, take + whatever A does. + + - If only B does something to it and A does not touch it, take + whatever B does. + + - If both A and B does something but in the same way, take + whatever they do. + + - If A and B does something but different things, we need a + 3-way merge: + + - We cannot do anything about the following cases: + + * O does not have it. A and B both must be adding to the + same path independently. + + * A deletes it. B must be modifying. + + - Otherwise, A and B are modifying. Run 3-way merge. + +First, the case matrix. + + - Vertical axis is for A'\''s actions. + - Horizontal axis is for B'\''s actions. + +.----------------------------------------------------------------. +| A B | No Action | Delete | Modify | Add | +|------------+------------+------------+------------+------------| +| No Action | | | | | +| | select O | delete | select B | select B | +| | | | | | +|------------+------------+------------+------------+------------| +| Delete | | | ********** | can | +| | delete | delete | merge | not | +| | | | | happen | +|------------+------------+------------+------------+------------| +| Modify | | ********** | ?????????? | can | +| | select A | merge | select A=B | not | +| | | | merge | happen | +|------------+------------+------------+------------+------------| +| Add | | can | can | ?????????? | +| | select A | not | not | select A=B | +| | | happen | happen | merge | +.----------------------------------------------------------------. + +In addition: + + SS: a special case of MM, where A and B makes the same modification. + LL: a special case of AA, where A and B creates the same file. + TT: a special case of MM, where A and B makes mergeable changes. + DF: a special case, where A makes a directory and B makes a file. + +' +. ./test-lib.sh +. ../lib-read-tree-m-3way.sh + +################################################################ +# Trivial "majority when 3 stages exist" merge plus #2ALT, #3ALT +# and #5ALT trivial merges. + +cat >expected <<\EOF +100644 X 2 AA +100644 X 3 AA +100644 X 0 AN +100644 X 1 DD +100644 X 3 DF +100644 X 2 DF/DF +100644 X 1 DM +100644 X 3 DM +100644 X 1 DN +100644 X 3 DN +100644 X 0 LL +100644 X 1 MD +100644 X 2 MD +100644 X 1 MM +100644 X 2 MM +100644 X 3 MM +100644 X 0 MN +100644 X 0 NA +100644 X 1 ND +100644 X 2 ND +100644 X 0 NM +100644 X 0 NN +100644 X 0 SS +100644 X 1 TT +100644 X 2 TT +100644 X 3 TT +100644 X 2 Z/AA +100644 X 3 Z/AA +100644 X 0 Z/AN +100644 X 1 Z/DD +100644 X 1 Z/DM +100644 X 3 Z/DM +100644 X 1 Z/DN +100644 X 3 Z/DN +100644 X 1 Z/MD +100644 X 2 Z/MD +100644 X 1 Z/MM +100644 X 2 Z/MM +100644 X 3 Z/MM +100644 X 0 Z/MN +100644 X 0 Z/NA +100644 X 1 Z/ND +100644 X 2 Z/ND +100644 X 0 Z/NM +100644 X 0 Z/NN +EOF + +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" + +check_result () { + git-ls-files --stage | sed -e 's/ '"$_x40"' / X /' >current && + diff -u expected current +} + +# This is done on an empty work directory, which is the normal +# merge person behaviour. +test_expect_success \ + '3-way merge with git-read-tree -m, empty cache' \ + "rm -fr [NDMALTS][NDMALTSF] Z && + rm .git/index && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +# This starts out with the first head, which is the normal +# patch submitter behaviour. +test_expect_success \ + '3-way merge with git-read-tree -m, match H' \ + "rm -fr [NDMALTS][NDMALTSF] Z && + rm .git/index && + git-read-tree $tree_A && + git-checkout-index -f -u -a && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +: <<\END_OF_CASE_TABLE + +We have so far tested only empty index and clean-and-matching-A index +case which are trivial. Make sure index requirements are also +checked. + +"git-read-tree -m O A B" + + O A B result index requirements +------------------------------------------------------------------- + 1 missing missing missing - must not exist. + ------------------------------------------------------------------ + 2 missing missing exists take B* must match B, if exists. + ------------------------------------------------------------------ + 3 missing exists missing take A* must match A, if exists. + ------------------------------------------------------------------ + 4 missing exists A!=B no merge must match A and be + up-to-date, if exists. + ------------------------------------------------------------------ + 5 missing exists A==B take A must match A, if exists. + ------------------------------------------------------------------ + 6 exists missing missing remove must not exist. + ------------------------------------------------------------------ + 7 exists missing O!=B no merge must not exist. + ------------------------------------------------------------------ + 8 exists missing O==B remove must not exist. + ------------------------------------------------------------------ + 9 exists O!=A missing no merge must match A and be + up-to-date, if exists. + ------------------------------------------------------------------ + 10 exists O==A missing remove ditto + ------------------------------------------------------------------ + 11 exists O!=A O!=B no merge must match A and be + A!=B up-to-date, if exists. + ------------------------------------------------------------------ + 12 exists O!=A O!=B take A must match A, if exists. + A==B + ------------------------------------------------------------------ + 13 exists O!=A O==B take A must match A, if exists. + ------------------------------------------------------------------ + 14 exists O==A O!=B take B if exists, must either (1) + match A and be up-to-date, + or (2) match B. + ------------------------------------------------------------------ + 15 exists O==A O==B take B must match A if exists. + ------------------------------------------------------------------ + 16 exists O==A O==B barf must match A if exists. + *multi* in one in another +------------------------------------------------------------------- + +Note: we need to be careful in case 2 and 3. The tree A may contain +DF (file) when tree B require DF to be a directory by having DF/DF +(file). + +END_OF_CASE_TABLE + +test_expect_failure \ + '1 - must not have an entry not in A.' \ + "rm -f .git/index XX && + echo XX >XX && + git-update-index --add XX && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '2 - must match B in !O && !A && B case.' \ + "rm -f .git/index NA && + cp .orig-B/NA NA && + git-update-index --add NA && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '2 - matching B alone is OK in !O && !A && B case.' \ + "rm -f .git/index NA && + cp .orig-B/NA NA && + git-update-index --add NA && + echo extra >>NA && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '3 - must match A in !O && A && !B case.' \ + "rm -f .git/index AN && + cp .orig-A/AN AN && + git-update-index --add AN && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_success \ + '3 - matching A alone is OK in !O && A && !B case.' \ + "rm -f .git/index AN && + cp .orig-A/AN AN && + git-update-index --add AN && + echo extra >>AN && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_failure \ + '3 (fail) - must match A in !O && A && !B case.' \ + "rm -f .git/index AN && + cp .orig-A/AN AN && + echo extra >>AN && + git-update-index --add AN && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '4 - must match and be up-to-date in !O && A && B && A!=B case.' \ + "rm -f .git/index AA && + cp .orig-A/AA AA && + git-update-index --add AA && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_failure \ + '4 (fail) - must match and be up-to-date in !O && A && B && A!=B case.' \ + "rm -f .git/index AA && + cp .orig-A/AA AA && + git-update-index --add AA && + echo extra >>AA && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_failure \ + '4 (fail) - must match and be up-to-date in !O && A && B && A!=B case.' \ + "rm -f .git/index AA && + cp .orig-A/AA AA && + echo extra >>AA && + git-update-index --add AA && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '5 - must match in !O && A && B && A==B case.' \ + "rm -f .git/index LL && + cp .orig-A/LL LL && + git-update-index --add LL && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_success \ + '5 - must match in !O && A && B && A==B case.' \ + "rm -f .git/index LL && + cp .orig-A/LL LL && + git-update-index --add LL && + echo extra >>LL && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_failure \ + '5 (fail) - must match A in !O && A && B && A==B case.' \ + "rm -f .git/index LL && + cp .orig-A/LL LL && + echo extra >>LL && + git-update-index --add LL && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_failure \ + '6 - must not exist in O && !A && !B case' \ + "rm -f .git/index DD && + echo DD >DD + git-update-index --add DD && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_failure \ + '7 - must not exist in O && !A && B && O!=B case' \ + "rm -f .git/index DM && + cp .orig-B/DM DM && + git-update-index --add DM && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_failure \ + '8 - must not exist in O && !A && B && O==B case' \ + "rm -f .git/index DN && + cp .orig-B/DN DN && + git-update-index --add DN && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '9 - must match and be up-to-date in O && A && !B && O!=A case' \ + "rm -f .git/index MD && + cp .orig-A/MD MD && + git-update-index --add MD && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_failure \ + '9 (fail) - must match and be up-to-date in O && A && !B && O!=A case' \ + "rm -f .git/index MD && + cp .orig-A/MD MD && + git-update-index --add MD && + echo extra >>MD && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_failure \ + '9 (fail) - must match and be up-to-date in O && A && !B && O!=A case' \ + "rm -f .git/index MD && + cp .orig-A/MD MD && + echo extra >>MD && + git-update-index --add MD && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '10 - must match and be up-to-date in O && A && !B && O==A case' \ + "rm -f .git/index ND && + cp .orig-A/ND ND && + git-update-index --add ND && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_failure \ + '10 (fail) - must match and be up-to-date in O && A && !B && O==A case' \ + "rm -f .git/index ND && + cp .orig-A/ND ND && + git-update-index --add ND && + echo extra >>ND && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_failure \ + '10 (fail) - must match and be up-to-date in O && A && !B && O==A case' \ + "rm -f .git/index ND && + cp .orig-A/ND ND && + echo extra >>ND && + git-update-index --add ND && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '11 - must match and be up-to-date in O && A && B && O!=A && O!=B && A!=B case' \ + "rm -f .git/index MM && + cp .orig-A/MM MM && + git-update-index --add MM && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_failure \ + '11 (fail) - must match and be up-to-date in O && A && B && O!=A && O!=B && A!=B case' \ + "rm -f .git/index MM && + cp .orig-A/MM MM && + git-update-index --add MM && + echo extra >>MM && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_failure \ + '11 (fail) - must match and be up-to-date in O && A && B && O!=A && O!=B && A!=B case' \ + "rm -f .git/index MM && + cp .orig-A/MM MM && + echo extra >>MM && + git-update-index --add MM && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '12 - must match A in O && A && B && O!=A && A==B case' \ + "rm -f .git/index SS && + cp .orig-A/SS SS && + git-update-index --add SS && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_success \ + '12 - must match A in O && A && B && O!=A && A==B case' \ + "rm -f .git/index SS && + cp .orig-A/SS SS && + git-update-index --add SS && + echo extra >>SS && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_failure \ + '12 (fail) - must match A in O && A && B && O!=A && A==B case' \ + "rm -f .git/index SS && + cp .orig-A/SS SS && + echo extra >>SS && + git-update-index --add SS && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '13 - must match A in O && A && B && O!=A && O==B case' \ + "rm -f .git/index MN && + cp .orig-A/MN MN && + git-update-index --add MN && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_success \ + '13 - must match A in O && A && B && O!=A && O==B case' \ + "rm -f .git/index MN && + cp .orig-A/MN MN && + git-update-index --add MN && + echo extra >>MN && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_success \ + '14 - must match and be up-to-date in O && A && B && O==A && O!=B case' \ + "rm -f .git/index NM && + cp .orig-A/NM NM && + git-update-index --add NM && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_success \ + '14 - may match B in O && A && B && O==A && O!=B case' \ + "rm -f .git/index NM && + cp .orig-B/NM NM && + git-update-index --add NM && + echo extra >>NM && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_failure \ + '14 (fail) - must match and be up-to-date in O && A && B && O==A && O!=B case' \ + "rm -f .git/index NM && + cp .orig-A/NM NM && + git-update-index --add NM && + echo extra >>NM && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_failure \ + '14 (fail) - must match and be up-to-date in O && A && B && O==A && O!=B case' \ + "rm -f .git/index NM && + cp .orig-A/NM NM && + echo extra >>NM && + git-update-index --add NM && + git-read-tree -m $tree_O $tree_A $tree_B" + +test_expect_success \ + '15 - must match A in O && A && B && O==A && O==B case' \ + "rm -f .git/index NN && + cp .orig-A/NN NN && + git-update-index --add NN && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_success \ + '15 - must match A in O && A && B && O==A && O==B case' \ + "rm -f .git/index NN && + cp .orig-A/NN NN && + git-update-index --add NN && + echo extra >>NN && + git-read-tree -m $tree_O $tree_A $tree_B && + check_result" + +test_expect_failure \ + '15 (fail) - must match A in O && A && B && O==A && O==B case' \ + "rm -f .git/index NN && + cp .orig-A/NN NN && + echo extra >>NN && + git-update-index --add NN && + git-read-tree -m $tree_O $tree_A $tree_B" + +# #16 +test_expect_success \ + '16 - A matches in one and B matches in another.' \ + 'rm -f .git/index F16 && + echo F16 >F16 && + git-update-index --add F16 && + tree0=`git-write-tree` && + echo E16 >F16 && + git-update-index F16 && + tree1=`git-write-tree` && + git-read-tree -m $tree0 $tree1 $tree1 $tree0 && + git-ls-files --stage' + +test_done diff --git a/t/t1001-read-tree-m-2way.sh b/t/t1001-read-tree-m-2way.sh new file mode 100755 index 0000000000..75e4c9a886 --- /dev/null +++ b/t/t1001-read-tree-m-2way.sh @@ -0,0 +1,344 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Two way merge with read-tree -m $H $M + +This test tries two-way merge (aka fast forward with carry forward). + +There is the head (called H) and another commit (called M), which is +simply ahead of H. The index and the work tree contains a state that +is derived from H, but may also have local changes. This test checks +all the combinations described in the two-tree merge "carry forward" +rules, found in <Documentation/git-read-tree.txt>. + +In the test, these paths are used: + bozbar - in H, stays in M, modified from bozbar to gnusto + frotz - not in H added in M + nitfol - in H, stays in M unmodified + rezrov - in H, deleted in M + yomin - not in H nor M +' +. ./test-lib.sh + +read_tree_twoway () { + git-read-tree -m "$1" "$2" && git-ls-files --stage +} + +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" +compare_change () { + sed -n >current \ + -e '/^--- /d; /^+++ /d; /^@@ /d;' \ + -e 's/^\([-+][0-7][0-7][0-7][0-7][0-7][0-7]\) '"$_x40"' /\1 X /p' \ + "$1" + diff -u expected current +} + +check_cache_at () { + clean_if_empty=`git-diff-files -- "$1"` + case "$clean_if_empty" in + '') echo "$1: clean" ;; + ?*) echo "$1: dirty" ;; + esac + case "$2,$clean_if_empty" in + clean,) : ;; + clean,?*) false ;; + dirty,) false ;; + dirty,?*) : ;; + esac +} + +cat >bozbar-old <<\EOF +This is a sample file used in two-way fast forward merge +tests. Its second line ends with a magic word bozbar +which will be modified by the merged head to gnusto. +It has some extra lines so that external tools can +successfully merge independent changes made to later +lines (such as this one), avoiding line conflicts. +EOF + +sed -e 's/bozbar/gnusto (earlier bozbar)/' bozbar-old >bozbar-new + +test_expect_success \ + setup \ + 'echo frotz >frotz && + echo nitfol >nitfol && + cat bozbar-old >bozbar && + echo rezrov >rezrov && + echo yomin >yomin && + git-update-index --add nitfol bozbar rezrov && + treeH=`git-write-tree` && + echo treeH $treeH && + git-ls-tree $treeH && + + cat bozbar-new >bozbar && + git-update-index --add frotz bozbar --force-remove rezrov && + git-ls-files --stage >M.out && + treeM=`git-write-tree` && + echo treeM $treeM && + git-ls-tree $treeM && + git-diff-tree $treeH $treeM' + +test_expect_success \ + '1, 2, 3 - no carry forward' \ + 'rm -f .git/index && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >1-3.out && + diff -u M.out 1-3.out && + check_cache_at bozbar dirty && + check_cache_at frotz dirty && + check_cache_at nitfol dirty' + +echo '+100644 X 0 yomin' >expected + +test_expect_success \ + '4 - carry forward local addition.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + git-update-index --add yomin && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >4.out || return 1 + diff -u M.out 4.out >4diff.out + compare_change 4diff.out expected && + check_cache_at yomin clean' + +test_expect_success \ + '5 - carry forward local addition.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo yomin >yomin && + git-update-index --add yomin && + echo yomin yomin >yomin && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >5.out || return 1 + diff -u M.out 5.out >5diff.out + compare_change 5diff.out expected && + check_cache_at yomin dirty' + +test_expect_success \ + '6 - local addition already has the same.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + git-update-index --add frotz && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >6.out && + diff -u M.out 6.out && + check_cache_at frotz clean' + +test_expect_success \ + '7 - local addition already has the same.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo frotz >frotz && + git-update-index --add frotz && + echo frotz frotz >frotz && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >7.out && + diff -u M.out 7.out && + check_cache_at frotz dirty' + +test_expect_success \ + '8 - conflicting addition.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo frotz frotz >frotz && + git-update-index --add frotz && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '9 - conflicting addition.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo frotz frotz >frotz && + git-update-index --add frotz && + echo frotz >frotz && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '10 - path removed.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo rezrov >rezrov && + git-update-index --add rezrov && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >10.out && + diff -u M.out 10.out' + +test_expect_success \ + '11 - dirty path removed.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo rezrov >rezrov && + git-update-index --add rezrov && + echo rezrov rezrov >rezrov && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '12 - unmatching local changes being removed.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo rezrov rezrov >rezrov && + git-update-index --add rezrov && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '13 - unmatching local changes being removed.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo rezrov rezrov >rezrov && + git-update-index --add rezrov && + echo rezrov >rezrov && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +cat >expected <<EOF +-100644 X 0 nitfol ++100644 X 0 nitfol +EOF + +test_expect_success \ + '14 - unchanged in two heads.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo nitfol nitfol >nitfol && + git-update-index --add nitfol && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >14.out || return 1 + diff -u M.out 14.out >14diff.out + compare_change 14diff.out expected && + check_cache_at nitfol clean' + +test_expect_success \ + '15 - unchanged in two heads.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo nitfol nitfol >nitfol && + git-update-index --add nitfol && + echo nitfol nitfol nitfol >nitfol && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >15.out || return 1 + diff -u M.out 15.out >15diff.out + compare_change 15diff.out expected && + check_cache_at nitfol dirty' + +test_expect_success \ + '16 - conflicting local change.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo bozbar bozbar >bozbar && + git-update-index --add bozbar && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '17 - conflicting local change.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + echo bozbar bozbar >bozbar && + git-update-index --add bozbar && + echo bozbar bozbar bozbar >bozbar && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '18 - local change already having a good result.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + cat bozbar-new >bozbar && + git-update-index --add bozbar && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >18.out && + diff -u M.out 18.out && + check_cache_at bozbar clean' + +test_expect_success \ + '19 - local change already having a good result, further modified.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + cat bozbar-new >bozbar && + git-update-index --add bozbar && + echo gnusto gnusto >bozbar && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >19.out && + diff -u M.out 19.out && + check_cache_at bozbar dirty' + +test_expect_success \ + '20 - no local change, use new tree.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + cat bozbar-old >bozbar && + git-update-index --add bozbar && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >20.out && + diff -u M.out 20.out && + check_cache_at bozbar dirty' + +test_expect_success \ + '21 - no local change, dirty cache.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + cat bozbar-old >bozbar && + git-update-index --add bozbar && + echo gnusto gnusto >bozbar && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +# This fails with straight two-way fast forward. +test_expect_success \ + '22 - local change cache updated.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-index -u -f -q -a && + sed -e "s/such as/SUCH AS/" bozbar-old >bozbar && + git-update-index --add bozbar && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +# Also make sure we did not break DF vs DF/DF case. +test_expect_success \ + 'DF vs DF/DF case setup.' \ + 'rm -f .git/index && + echo DF >DF && + git-update-index --add DF && + treeDF=`git-write-tree` && + echo treeDF $treeDF && + git-ls-tree $treeDF && + + rm -f DF && + mkdir DF && + echo DF/DF >DF/DF && + git-update-index --add --remove DF DF/DF && + treeDFDF=`git-write-tree` && + echo treeDFDF $treeDFDF && + git-ls-tree $treeDFDF && + git-ls-files --stage >DFDF.out' + +test_expect_success \ + 'DF vs DF/DF case test.' \ + 'rm -f .git/index && + rm -fr DF && + echo DF >DF && + git-update-index --add DF && + read_tree_twoway $treeDF $treeDFDF && + git-ls-files --stage >DFDFcheck.out && + diff -u DFDF.out DFDFcheck.out && + check_cache_at DF/DF dirty && + :' + +test_done diff --git a/t/t1002-read-tree-m-u-2way.sh b/t/t1002-read-tree-m-u-2way.sh new file mode 100755 index 0000000000..da3c81357b --- /dev/null +++ b/t/t1002-read-tree-m-u-2way.sh @@ -0,0 +1,344 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Two way merge with read-tree -m -u $H $M + +This is identical to t1001, but uses -u to update the work tree as well. + +' +. ./test-lib.sh + +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" +compare_change () { + sed >current \ + -e '/^--- /d; /^+++ /d; /^@@ /d;' \ + -e 's/^\(.[0-7][0-7][0-7][0-7][0-7][0-7]\) '"$_x40"' /\1 X /' "$1" + diff -u expected current +} + +check_cache_at () { + clean_if_empty=`git-diff-files -- "$1"` + case "$clean_if_empty" in + '') echo "$1: clean" ;; + ?*) echo "$1: dirty" ;; + esac + case "$2,$clean_if_empty" in + clean,) : ;; + clean,?*) false ;; + dirty,) false ;; + dirty,?*) : ;; + esac +} + +test_expect_success \ + setup \ + 'echo frotz >frotz && + echo nitfol >nitfol && + echo bozbar >bozbar && + echo rezrov >rezrov && + git-update-index --add nitfol bozbar rezrov && + treeH=`git-write-tree` && + echo treeH $treeH && + git-ls-tree $treeH && + + echo gnusto >bozbar && + git-update-index --add frotz bozbar --force-remove rezrov && + git-ls-files --stage >M.out && + treeM=`git-write-tree` && + echo treeM $treeM && + git-ls-tree $treeM && + sum bozbar frotz nitfol >M.sum && + git-diff-tree $treeH $treeM' + +test_expect_success \ + '1, 2, 3 - no carry forward' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >1-3.out && + cmp M.out 1-3.out && + sum bozbar frotz nitfol >actual3.sum && + cmp M.sum actual3.sum && + check_cache_at bozbar clean && + check_cache_at frotz clean && + check_cache_at nitfol clean' + +test_expect_success \ + '4 - carry forward local addition.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo "+100644 X 0 yomin" >expected && + echo yomin >yomin && + git-update-index --add yomin && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >4.out || return 1 + diff -U0 M.out 4.out >4diff.out + compare_change 4diff.out expected && + check_cache_at yomin clean && + sum bozbar frotz nitfol >actual4.sum && + cmp M.sum actual4.sum && + echo yomin >yomin1 && + diff yomin yomin1 && + rm -f yomin1' + +test_expect_success \ + '5 - carry forward local addition.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + git-read-tree -m -u $treeH && + echo yomin >yomin && + git-update-index --add yomin && + echo yomin yomin >yomin && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >5.out || return 1 + diff -U0 M.out 5.out >5diff.out + compare_change 5diff.out expected && + check_cache_at yomin dirty && + sum bozbar frotz nitfol >actual5.sum && + cmp M.sum actual5.sum && + : dirty index should have prevented -u from checking it out. && + echo yomin yomin >yomin1 && + diff yomin yomin1 && + rm -f yomin1' + +test_expect_success \ + '6 - local addition already has the same.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo frotz >frotz && + git-update-index --add frotz && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >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 >frotz1 && + diff frotz frotz1 && + rm -f frotz1' + +test_expect_success \ + '7 - local addition already has the same.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo frotz >frotz && + git-update-index --add frotz && + echo frotz frotz >frotz && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >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 && + : dirty index should have prevented -u from checking it out. && + echo frotz frotz >frotz1 && + diff frotz frotz1 && + rm -f frotz1' + +test_expect_success \ + '8 - conflicting addition.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo frotz frotz >frotz && + git-update-index --add frotz && + if git-read-tree -m -u $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '9 - conflicting addition.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo frotz frotz >frotz && + git-update-index --add frotz && + echo frotz >frotz && + if git-read-tree -m -u $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '10 - path removed.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo rezrov >rezrov && + git-update-index --add rezrov && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >10.out && + cmp M.out 10.out && + sum bozbar frotz nitfol >actual10.sum && + cmp M.sum actual10.sum' + +test_expect_success \ + '11 - dirty path removed.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo rezrov >rezrov && + git-update-index --add rezrov && + echo rezrov rezrov >rezrov && + if git-read-tree -m -u $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '12 - unmatching local changes being removed.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo rezrov rezrov >rezrov && + git-update-index --add rezrov && + if git-read-tree -m -u $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '13 - unmatching local changes being removed.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo rezrov rezrov >rezrov && + git-update-index --add rezrov && + echo rezrov >rezrov && + if git-read-tree -m -u $treeH $treeM; then false; else :; fi' + +cat >expected <<EOF +-100644 X 0 nitfol ++100644 X 0 nitfol +EOF + +test_expect_success \ + '14 - unchanged in two heads.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo nitfol nitfol >nitfol && + git-update-index --add nitfol && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >14.out || return 1 + 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 && + cmp expected14.sum actual14.sum && + sum bozbar frotz nitfol >actual14a.sum && + if cmp M.sum actual14a.sum; then false; else :; fi && + check_cache_at nitfol clean && + echo nitfol nitfol >nitfol1 && + diff nitfol nitfol1 && + rm -f nitfol1' + +test_expect_success \ + '15 - unchanged in two heads.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo nitfol nitfol >nitfol && + git-update-index --add nitfol && + echo nitfol nitfol nitfol >nitfol && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >15.out || return 1 + diff -U0 M.out 15.out >15diff.out + compare_change 15diff.out expected && + check_cache_at nitfol dirty && + sum bozbar frotz >actual15.sum && + grep -v nitfol M.sum > expected15.sum && + cmp expected15.sum actual15.sum && + sum bozbar frotz nitfol >actual15a.sum && + if cmp M.sum actual15a.sum; then false; else :; fi && + echo nitfol nitfol nitfol >nitfol1 && + diff nitfol nitfol1 && + rm -f nitfol1' + +test_expect_success \ + '16 - conflicting local change.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo bozbar bozbar >bozbar && + git-update-index --add bozbar && + if git-read-tree -m -u $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '17 - conflicting local change.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo bozbar bozbar >bozbar && + git-update-index --add bozbar && + echo bozbar bozbar bozbar >bozbar && + if git-read-tree -m -u $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '18 - local change already having a good result.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo gnusto >bozbar && + git-update-index --add bozbar && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >18.out && + diff -U0 M.out 18.out && + check_cache_at bozbar clean && + sum bozbar frotz nitfol >actual18.sum && + cmp M.sum actual18.sum' + +test_expect_success \ + '19 - local change already having a good result, further modified.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo gnusto >bozbar && + git-update-index --add bozbar && + echo gnusto gnusto >bozbar && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >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 && + cmp expected19.sum actual19.sum && + sum bozbar frotz nitfol >actual19a.sum && + if cmp M.sum actual19a.sum; then false; else :; fi && + echo gnusto gnusto >bozbar1 && + diff bozbar bozbar1 && + rm -f bozbar1' + +test_expect_success \ + '20 - no local change, use new tree.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo bozbar >bozbar && + git-update-index --add bozbar && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >20.out && + diff -U0 M.out 20.out && + check_cache_at bozbar clean && + sum bozbar frotz nitfol >actual20.sum && + cmp M.sum actual20.sum' + +test_expect_success \ + '21 - no local change, dirty cache.' \ + 'rm -f .git/index nitfol bozbar rezrov frotz && + git-read-tree --reset -u $treeH && + echo bozbar >bozbar && + git-update-index --add bozbar && + echo gnusto gnusto >bozbar && + if git-read-tree -m -u $treeH $treeM; then false; else :; fi' + +# Also make sure we did not break DF vs DF/DF case. +test_expect_success \ + 'DF vs DF/DF case setup.' \ + 'rm -f .git/index + echo DF >DF && + git-update-index --add DF && + treeDF=`git-write-tree` && + echo treeDF $treeDF && + git-ls-tree $treeDF && + + rm -f DF && + mkdir DF && + echo DF/DF >DF/DF && + git-update-index --add --remove DF DF/DF && + treeDFDF=`git-write-tree` && + echo treeDFDF $treeDFDF && + git-ls-tree $treeDFDF && + git-ls-files --stage >DFDF.out' + +test_expect_success \ + 'DF vs DF/DF case test.' \ + 'rm -f .git/index && + rm -fr DF && + echo DF >DF && + git-update-index --add DF && + git-read-tree -m -u $treeDF $treeDFDF && + git-ls-files --stage >DFDFcheck.out && + diff -U0 DFDF.out DFDFcheck.out && + check_cache_at DF/DF clean' + +test_done diff --git a/t/t1003-read-tree-prefix.sh b/t/t1003-read-tree-prefix.sh new file mode 100755 index 0000000000..48ab117d75 --- /dev/null +++ b/t/t1003-read-tree-prefix.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='git-read-tree --prefix test. +' + +. ./test-lib.sh + +test_expect_success setup ' + echo hello >one && + git-update-index --add one && + tree=`git-write-tree` && + echo tree is $tree +' + +echo 'one +two/one' >expect + +test_expect_success 'read-tree --prefix' ' + git-read-tree --prefix=two/ $tree && + git-ls-files >actual && + cmp expect actual +' + +test_done diff --git a/t/t1004-read-tree-m-u-wf.sh b/t/t1004-read-tree-m-u-wf.sh new file mode 100755 index 0000000000..c11420a8b6 --- /dev/null +++ b/t/t1004-read-tree-m-u-wf.sh @@ -0,0 +1,119 @@ +#!/bin/sh + +test_description='read-tree -m -u checks working tree files' + +. ./test-lib.sh + +# two-tree test + +test_expect_success 'two-way setup' ' + + mkdir subdir && + echo >file1 file one && + echo >file2 file two && + echo >subdir/file1 file one in subdirectory && + echo >subdir/file2 file two in subdirectory && + git update-index --add file1 file2 subdir/file1 subdir/file2 && + git commit -m initial && + + git branch side && + git tag -f branch-point && + + echo file2 is not tracked on the master anymore && + rm -f file2 subdir/file2 && + git update-index --remove file2 subdir/file2 && + git commit -a -m "master removes file2 and subdir/file2" +' + +test_expect_success 'two-way not clobbering' ' + + echo >file2 master creates untracked file2 && + echo >subdir/file2 master creates untracked subdir/file2 && + if err=`git read-tree -m -u master side 2>&1` + then + echo should have complained + false + else + echo "happy to see $err" + fi +' + +echo file2 >.gitignore + +test_expect_success 'two-way with incorrect --exclude-per-directory (1)' ' + + if err=`git read-tree -m --exclude-per-directory=.gitignore master side 2>&1` + then + echo should have complained + false + else + echo "happy to see $err" + fi +' + +test_expect_success 'two-way with incorrect --exclude-per-directory (2)' ' + + if err=`git read-tree -m -u --exclude-per-directory=foo --exclude-per-directory=.gitignore master side 2>&1` + then + echo should have complained + false + else + echo "happy to see $err" + fi +' + +test_expect_success 'two-way clobbering a ignored file' ' + + git read-tree -m -u --exclude-per-directory=.gitignore master side +' + +rm -f .gitignore + +# three-tree test + +test_expect_success 'three-way not complaining on an untracked path in both' ' + + rm -f file2 subdir/file2 && + git checkout side && + echo >file3 file three && + echo >subdir/file3 file three && + git update-index --add file3 subdir/file3 && + git commit -a -m "side adds file3 and removes file2" && + + git checkout master && + echo >file2 file two is untracked on the master side && + echo >subdir/file2 file two is untracked on the master side && + + git-read-tree -m -u branch-point master side +' + +test_expect_success 'three-way not clobbering a working tree file' ' + + git reset --hard && + rm -f file2 subdir/file2 file3 subdir/file3 && + git checkout master && + echo >file3 file three created in master, untracked && + echo >subdir/file3 file three created in master, untracked && + if err=`git read-tree -m -u branch-point master side 2>&1` + then + echo should have complained + false + else + echo "happy to see $err" + fi +' + +echo >.gitignore file3 + +test_expect_success 'three-way not complaining on an untracked file' ' + + git reset --hard && + rm -f file2 subdir/file2 file3 subdir/file3 && + git checkout master && + echo >file3 file three created in master, untracked && + echo >subdir/file3 file three created in master, untracked && + + git read-tree -m -u --exclude-per-directory=.gitignore branch-point master side +' + +test_done diff --git a/t/t1020-subdirectory.sh b/t/t1020-subdirectory.sh new file mode 100755 index 0000000000..1e8f9e59df --- /dev/null +++ b/t/t1020-subdirectory.sh @@ -0,0 +1,138 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='Try various core-level commands in subdirectory. +' + +. ./test-lib.sh + +test_expect_success setup ' + long="a b c d e f g h i j k l m n o p q r s t u v w x y z" && + for c in $long; do echo $c; done >one && + mkdir dir && + for c in x y z $long a b c; do echo $c; done >dir/two && + cp one original.one && + cp dir/two original.two +' +HERE=`pwd` +LF=' +' + +test_expect_success 'update-index and ls-files' ' + cd $HERE && + git-update-index --add one && + case "`git-ls-files`" in + one) echo ok one ;; + *) echo bad one; exit 1 ;; + esac && + cd dir && + git-update-index --add two && + case "`git-ls-files`" in + two) echo ok two ;; + *) echo bad two; exit 1 ;; + esac && + cd .. && + case "`git-ls-files`" in + dir/two"$LF"one) echo ok both ;; + *) echo bad; exit 1 ;; + esac +' + +test_expect_success 'cat-file' ' + cd $HERE && + two=`git-ls-files -s dir/two` && + two=`expr "$two" : "[0-7]* \\([0-9a-f]*\\)"` && + echo "$two" && + git-cat-file -p "$two" >actual && + cmp dir/two actual && + cd dir && + git-cat-file -p "$two" >actual && + cmp two actual +' +rm -f actual dir/actual + +test_expect_success 'diff-files' ' + cd $HERE && + echo a >>one && + echo d >>dir/two && + case "`git-diff-files --name-only`" in + dir/two"$LF"one) echo ok top ;; + *) echo bad top; exit 1 ;; + esac && + # diff should not omit leading paths + cd dir && + case "`git-diff-files --name-only`" in + dir/two"$LF"one) echo ok subdir ;; + *) echo bad subdir; exit 1 ;; + esac && + case "`git-diff-files --name-only .`" in + dir/two) echo ok subdir limited ;; + *) echo bad subdir limited; exit 1 ;; + esac +' + +test_expect_success 'write-tree' ' + cd $HERE && + top=`git-write-tree` && + echo $top && + cd dir && + sub=`git-write-tree` && + echo $sub && + test "z$top" = "z$sub" +' + +test_expect_success 'checkout-index' ' + cd $HERE && + git-checkout-index -f -u one && + cmp one original.one && + cd dir && + git-checkout-index -f -u two && + cmp two ../original.two +' + +test_expect_success 'read-tree' ' + cd $HERE && + rm -f one dir/two && + tree=`git-write-tree` && + git-read-tree --reset -u "$tree" && + cmp one original.one && + cmp dir/two original.two && + cd dir && + rm -f two && + git-read-tree --reset -u "$tree" && + cmp two ../original.two && + cmp ../one ../original.one +' + +test_expect_success 'no file/rev ambiguity check inside .git' ' + cd $HERE && + git commit -a -m 1 && + cd $HERE/.git && + git show -s HEAD +' + +test_expect_success 'no file/rev ambiguity check inside a bare repo' ' + cd $HERE && + git clone -s --bare .git foo.git && + cd foo.git && GIT_DIR=. git show -s HEAD +' + +# This still does not work as it should... +: test_expect_success 'no file/rev ambiguity check inside a bare repo' ' + cd $HERE && + git clone -s --bare .git foo.git && + cd foo.git && git show -s HEAD +' + +test_expect_success 'detection should not be fooled by a symlink' ' + cd $HERE && + rm -fr foo.git && + git clone -s .git another && + ln -s another yetanother && + cd yetanother/.git && + git show -s HEAD +' + +test_done diff --git a/t/t1100-commit-tree-options.sh b/t/t1100-commit-tree-options.sh new file mode 100755 index 0000000000..19a0ed4d20 --- /dev/null +++ b/t/t1100-commit-tree-options.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# +# Copyright (C) 2005 Rene Scharfe +# + +test_description='git-commit-tree options test + +This test checks that git-commit-tree can create a specific commit +object by defining all environment variables that it understands. +' + +. ./test-lib.sh + +cat >expected <<EOF +tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 +author Author Name <author@email> 1117148400 +0000 +committer Committer Name <committer@email> 1117150200 +0000 + +comment text +EOF + +test_expect_success \ + 'test preparation: write empty tree' \ + 'git-write-tree >treeid' + +test_expect_success \ + 'construct commit' \ + 'echo comment text | + GIT_AUTHOR_NAME="Author Name" \ + GIT_AUTHOR_EMAIL="author@email" \ + GIT_AUTHOR_DATE="2005-05-26 23:00" \ + GIT_COMMITTER_NAME="Committer Name" \ + GIT_COMMITTER_EMAIL="committer@email" \ + GIT_COMMITTER_DATE="2005-05-26 23:30" \ + TZ=GMT git-commit-tree `cat treeid` >commitid 2>/dev/null' + +test_expect_success \ + 'read commit' \ + 'git-cat-file commit `cat commitid` >commit' + +test_expect_success \ + 'compare commit' \ + 'diff expected commit' + +test_done diff --git a/t/t1200-tutorial.sh b/t/t1200-tutorial.sh new file mode 100755 index 0000000000..eebe643bda --- /dev/null +++ b/t/t1200-tutorial.sh @@ -0,0 +1,160 @@ +#!/bin/sh +# +# Copyright (c) 2005 Johannes Schindelin +# + +test_description='A simple turial in the form of a test case' + +. ./test-lib.sh + +echo "Hello World" > hello +echo "Silly example" > example + +git-update-index --add hello example + +test_expect_success 'blob' "test blob = \"$(git-cat-file -t 557db03)\"" + +test_expect_success 'blob 557db03' "test \"Hello World\" = \"$(git-cat-file blob 557db03)\"" + +echo "It's a new day for git" >>hello +cat > diff.expect << EOF +diff --git a/hello b/hello +index 557db03..263414f 100644 +--- a/hello ++++ b/hello +@@ -1 +1,2 @@ + Hello World ++It's a new day for git +EOF +git-diff-files -p > diff.output +test_expect_success 'git-diff-files -p' 'cmp diff.expect diff.output' +git diff > diff.output +test_expect_success 'git diff' 'cmp diff.expect diff.output' + +tree=$(git-write-tree 2>/dev/null) + +test_expect_success 'tree' "test 8988da15d077d4829fc51d8544c097def6644dbb = $tree" + +output="$(echo "Initial commit" | git-commit-tree $(git-write-tree) 2>&1 > .git/refs/heads/master)" + +git-diff-index -p HEAD > diff.output +test_expect_success 'git-diff-index -p HEAD' 'cmp diff.expect diff.output' + +git diff HEAD > diff.output +test_expect_success 'git diff HEAD' 'cmp diff.expect diff.output' + +#rm hello +#test_expect_success 'git-read-tree --reset HEAD' "git-read-tree --reset HEAD ; test \"hello: needs update\" = \"$(git-update-index --refresh)\"" + +cat > whatchanged.expect << EOF +commit VARIABLE +Author: VARIABLE +Date: VARIABLE + + Initial commit + +diff --git a/example b/example +new file mode 100644 +index 0000000..f24c74a +--- /dev/null ++++ b/example +@@ -0,0 +1 @@ ++Silly example +diff --git a/hello b/hello +new file mode 100644 +index 0000000..557db03 +--- /dev/null ++++ b/hello +@@ -0,0 +1 @@ ++Hello World +EOF + +git-whatchanged -p --root | \ + sed -e "1s/^\(.\{7\}\).\{40\}/\1VARIABLE/" \ + -e "2,3s/^\(.\{8\}\).*$/\1VARIABLE/" \ +> whatchanged.output +test_expect_success 'git-whatchanged -p --root' 'cmp whatchanged.expect whatchanged.output' + +git tag my-first-tag +test_expect_success 'git tag my-first-tag' 'cmp .git/refs/heads/master .git/refs/tags/my-first-tag' + +# TODO: test git-clone + +git checkout -b mybranch +test_expect_success 'git checkout -b mybranch' 'cmp .git/refs/heads/master .git/refs/heads/mybranch' + +cat > branch.expect <<EOF + master +* mybranch +EOF + +git branch > branch.output +test_expect_success 'git branch' 'cmp branch.expect branch.output' + +git checkout mybranch +echo "Work, work, work" >>hello +git commit -m 'Some work.' -i hello + +git checkout master + +echo "Play, play, play" >>hello +echo "Lots of fun" >>example +git commit -m 'Some fun.' -i hello example + +test_expect_failure 'git resolve now fails' 'git resolve HEAD mybranch "Merge work in mybranch"' + +cat > hello << EOF +Hello World +It's a new day for git +Play, play, play +Work, work, work +EOF + +git commit -m 'Merged "mybranch" changes.' -i hello + +test_done + +cat > show-branch.expect << EOF +* [master] Merged "mybranch" changes. + ! [mybranch] Some work. +-- +- [master] Merged "mybranch" changes. +*+ [mybranch] Some work. +EOF + +git show-branch --topo-order master mybranch > show-branch.output +test_expect_success 'git show-branch' 'cmp show-branch.expect show-branch.output' + +git checkout mybranch + +cat > resolve.expect << EOF +Updating from VARIABLE to VARIABLE + example | 1 + + hello | 1 + + 2 files changed, 2 insertions(+), 0 deletions(-) +EOF + +git resolve HEAD master "Merge upstream changes." | \ + sed -e "1s/[0-9a-f]\{40\}/VARIABLE/g" > resolve.output +test_expect_success 'git resolve' 'cmp resolve.expect resolve.output' + +cat > show-branch2.expect << EOF +! [master] Merged "mybranch" changes. + * [mybranch] Merged "mybranch" changes. +-- +-- [master] Merged "mybranch" changes. +EOF + +git show-branch --topo-order master mybranch > show-branch2.output +test_expect_success 'git show-branch' 'cmp show-branch2.expect show-branch2.output' + +# TODO: test git fetch + +# TODO: test git push + +test_expect_success 'git repack' 'git repack' +test_expect_success 'git prune-packed' 'git prune-packed' +test_expect_failure '-> only packed objects' 'find -type f .git/objects/[0-9a-f][0-9a-f]' + +test_done + diff --git a/t/t1300-repo-config.sh b/t/t1300-repo-config.sh new file mode 100755 index 0000000000..49b5666b33 --- /dev/null +++ b/t/t1300-repo-config.sh @@ -0,0 +1,448 @@ +#!/bin/sh +# +# Copyright (c) 2005 Johannes Schindelin +# + +test_description='Test git-config in different settings' + +. ./test-lib.sh + +test -f .git/config && rm .git/config + +git-config core.penguin "little blue" + +cat > expect << EOF +[core] + penguin = little blue +EOF + +test_expect_success 'initial' 'cmp .git/config expect' + +git-config Core.Movie BadPhysics + +cat > expect << EOF +[core] + penguin = little blue + Movie = BadPhysics +EOF + +test_expect_success 'mixed case' 'cmp .git/config expect' + +git-config Cores.WhatEver Second + +cat > expect << EOF +[core] + penguin = little blue + Movie = BadPhysics +[Cores] + WhatEver = Second +EOF + +test_expect_success 'similar section' 'cmp .git/config expect' + +git-config CORE.UPPERCASE true + +cat > expect << EOF +[core] + penguin = little blue + Movie = BadPhysics + UPPERCASE = true +[Cores] + WhatEver = Second +EOF + +test_expect_success 'similar section' 'cmp .git/config expect' + +test_expect_success 'replace with non-match' \ + 'git-config core.penguin kingpin !blue' + +test_expect_success 'replace with non-match (actually matching)' \ + 'git-config core.penguin "very blue" !kingpin' + +cat > expect << EOF +[core] + penguin = very blue + Movie = BadPhysics + UPPERCASE = true + penguin = kingpin +[Cores] + WhatEver = Second +EOF + +test_expect_success 'non-match result' 'cmp .git/config expect' + +cat > .git/config << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment + haha ="beta" # last silly comment +haha = hello + haha = bello +[nextSection] noNewline = ouch +EOF + +cp .git/config .git/config2 + +test_expect_success 'multiple unset' \ + 'git-config --unset-all beta.haha' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] noNewline = ouch +EOF + +test_expect_success 'multiple unset is correct' 'cmp .git/config expect' + +mv .git/config2 .git/config + +test_expect_success '--replace-all' \ + 'git-config --replace-all beta.haha gamma' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment + haha = gamma +[nextSection] noNewline = ouch +EOF + +test_expect_success 'all replaced' 'cmp .git/config expect' + +git-config beta.haha alpha + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment + haha = alpha +[nextSection] noNewline = ouch +EOF + +test_expect_success 'really mean test' 'cmp .git/config expect' + +git-config nextsection.nonewline wow + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment + haha = alpha +[nextSection] + nonewline = wow +EOF + +test_expect_success 'really really mean test' 'cmp .git/config expect' + +test_expect_success 'get value' 'test alpha = $(git-config beta.haha)' +git-config --unset beta.haha + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + nonewline = wow +EOF + +test_expect_success 'unset' 'cmp .git/config expect' + +git-config nextsection.NoNewLine "wow2 for me" "for me$" + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + nonewline = wow + NoNewLine = wow2 for me +EOF + +test_expect_success 'multivar' 'cmp .git/config expect' + +test_expect_success 'non-match' \ + 'git-config --get nextsection.nonewline !for' + +test_expect_success 'non-match value' \ + 'test wow = $(git-config --get nextsection.nonewline !for)' + +test_expect_failure 'ambiguous get' \ + 'git-config --get nextsection.nonewline' + +test_expect_success 'get multivar' \ + 'git-config --get-all nextsection.nonewline' + +git-config nextsection.nonewline "wow3" "wow$" + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + nonewline = wow3 + NoNewLine = wow2 for me +EOF + +test_expect_success 'multivar replace' 'cmp .git/config expect' + +test_expect_failure 'ambiguous value' 'git-config nextsection.nonewline' + +test_expect_failure 'ambiguous unset' \ + 'git-config --unset nextsection.nonewline' + +test_expect_failure 'invalid unset' \ + 'git-config --unset somesection.nonewline' + +git-config --unset nextsection.nonewline "wow3$" + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + NoNewLine = wow2 for me +EOF + +test_expect_success 'multivar unset' 'cmp .git/config expect' + +test_expect_failure 'invalid key' 'git-config inval.2key blabla' + +test_expect_success 'correct key' 'git-config 123456.a123 987' + +test_expect_success 'hierarchical section' \ + 'git-config Version.1.2.3eX.Alpha beta' + +cat > expect << EOF +[beta] ; silly comment # another comment +noIndent= sillyValue ; 'nother silly comment + +# empty line + ; comment +[nextSection] + NoNewLine = wow2 for me +[123456] + a123 = 987 +[Version "1.2.3eX"] + Alpha = beta +EOF + +test_expect_success 'hierarchical section value' 'cmp .git/config expect' + +cat > expect << EOF +beta.noindent=sillyValue +nextsection.nonewline=wow2 for me +123456.a123=987 +version.1.2.3eX.alpha=beta +EOF + +test_expect_success 'working --list' \ + 'git-config --list > output && cmp output expect' + +cat > expect << EOF +beta.noindent sillyValue +nextsection.nonewline wow2 for me +EOF + +test_expect_success '--get-regexp' \ + 'git-config --get-regexp in > output && cmp output expect' + +git-config --add nextsection.nonewline "wow4 for you" + +cat > expect << EOF +wow2 for me +wow4 for you +EOF + +test_expect_success '--add' \ + 'git-config --get-all nextsection.nonewline > output && cmp output expect' + +cat > .git/config << EOF +[novalue] + variable +EOF + +test_expect_success 'get variable with no value' \ + 'git-config --get novalue.variable ^$' + +git-config > output 2>&1 + +test_expect_success 'no arguments, but no crash' \ + "test $? = 129 && grep usage output" + +cat > .git/config << EOF +[a.b] + c = d +EOF + +git-config a.x y + +cat > expect << EOF +[a.b] + c = d +[a] + x = y +EOF + +test_expect_success 'new section is partial match of another' 'cmp .git/config expect' + +git-config b.x y +git-config a.b c + +cat > expect << EOF +[a.b] + c = d +[a] + x = y + b = c +[b] + x = y +EOF + +test_expect_success 'new variable inserts into proper section' 'cmp .git/config expect' + +cat > other-config << EOF +[ein] + bahn = strasse +EOF + +cat > expect << EOF +ein.bahn=strasse +EOF + +GIT_CONFIG=other-config git-config -l > output + +test_expect_success 'alternative GIT_CONFIG' 'cmp output expect' + +GIT_CONFIG=other-config git-config anwohner.park ausweis + +cat > expect << EOF +[ein] + bahn = strasse +[anwohner] + park = ausweis +EOF + +test_expect_success '--set in alternative GIT_CONFIG' 'cmp other-config expect' + +cat > .git/config << EOF +# Hallo + #Bello +[branch "eins"] + x = 1 +[branch.eins] + y = 1 + [branch "1 234 blabl/a"] +weird +EOF + +test_expect_success "rename section" \ + "git-config --rename-section branch.eins branch.zwei" + +cat > expect << EOF +# Hallo + #Bello +[branch "zwei"] + x = 1 +[branch "zwei"] + y = 1 + [branch "1 234 blabl/a"] +weird +EOF + +test_expect_success "rename succeeded" "diff -u expect .git/config" + +test_expect_failure "rename non-existing section" \ + 'git-config --rename-section branch."world domination" branch.drei' + +test_expect_success "rename succeeded" "diff -u expect .git/config" + +test_expect_success "rename another section" \ + 'git-config --rename-section branch."1 234 blabl/a" branch.drei' + +cat > expect << EOF +# Hallo + #Bello +[branch "zwei"] + x = 1 +[branch "zwei"] + y = 1 +[branch "drei"] +weird +EOF + +test_expect_success "rename succeeded" "diff -u expect .git/config" + +test_expect_success numbers ' + + git-config kilo.gram 1k && + git-config mega.ton 1m && + k=$(git-config --int --get kilo.gram) && + test z1024 = "z$k" && + m=$(git-config --int --get mega.ton) && + test z1048576 = "z$m" +' + +rm .git/config + +git-config quote.leading " test" +git-config quote.ending "test " +git-config quote.semicolon "test;test" +git-config quote.hash "test#test" + +cat > expect << EOF +[quote] + leading = " test" + ending = "test " + semicolon = "test;test" + hash = "test#test" +EOF + +test_expect_success 'quoting' 'cmp .git/config expect' + +test_expect_failure 'key with newline' 'git config key.with\\\ +newline 123' + +test_expect_success 'value with newline' 'git config key.sub value.with\\\ +newline' + +cat > .git/config <<\EOF +[section] + ; comment \ + continued = cont\ +inued + noncont = not continued ; \ + quotecont = "cont;\ +inued" +EOF + +cat > expect <<\EOF +section.continued=continued +section.noncont=not continued +section.quotecont=cont;inued +EOF + +git config --list > result + +test_expect_success 'value continued on next line' 'cmp result expect' + +test_done + diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh new file mode 100755 index 0000000000..d0aba2c2ae --- /dev/null +++ b/t/t1400-update-ref.sh @@ -0,0 +1,234 @@ +#!/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 +n_dir=refs/heads/gu +n=$n_dir/fixes + +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 \ + "fail to create $n" \ + "touch .git/$n_dir + git-update-ref $n $A >out 2>err"' + test $? != 0' +rm -f .git/$n_dir out err + +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 + +: a repository with working tree always has reflog these days... +: >.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-config core.logAllRefUpdates true && + test true = $(git-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 for '\'master\'' 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 for '\'master\'' 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 for '\''master'\'' 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) && + echo FIXED >F && + GIT_AUTHOR_DATE="2005-05-26 23:44" \ + GIT_COMMITTER_DATE="2005-05-26 23:44" git-commit --amend && + h_FIXED=$(git-rev-parse --verify HEAD) && + echo TEST+FIXED >F && + echo Merged initial commit and a later commit. >M && + echo $h_TEST >.git/MERGE_HEAD && + GIT_AUTHOR_DATE="2005-05-26 23:45" \ + GIT_COMMITTER_DATE="2005-05-26 23:45" git-commit -F M && + h_MERGED=$(git-rev-parse --verify HEAD) + rm -f M' + +cat >expect <<EOF +$Z $h_TEST $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000 commit (initial): add +$h_TEST $h_OTHER $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150860 +0000 commit: The other day this did not work. +$h_OTHER $h_FIXED $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117151040 +0000 commit (amend): The other day this did not work. +$h_FIXED $h_MERGED $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117151100 +0000 commit (merge): Merged initial commit and a later commit. +EOF +test_expect_success \ + 'git-commit logged updates' \ + "diff expect .git/logs/$m" +unset h_TEST h_OTHER h_FIXED h_MERGED + +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 diff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh new file mode 100755 index 0000000000..e5bbc384f7 --- /dev/null +++ b/t/t1410-reflog.sh @@ -0,0 +1,178 @@ +#!/bin/sh +# +# Copyright (c) 2007 Junio C Hamano +# + +test_description='Test prune and reflog expiration' +. ./test-lib.sh + +check_have () { + gaah= && + for N in "$@" + do + eval "o=\$$N" && git cat-file -t $o || { + echo Gaah $N + gaah=$N + break + } + done && + test -z "$gaah" +} + +check_fsck () { + output=$(git fsck --full) + case "$1" in + '') + test -z "$output" ;; + *) + echo "$output" | grep "$1" ;; + esac +} + +corrupt () { + aa=${1%??????????????????????????????????????} zz=${1#??} + mv .git/objects/$aa/$zz .git/$aa$zz +} + +recover () { + aa=${1%??????????????????????????????????????} zz=${1#??} + mkdir -p .git/objects/$aa + mv .git/$aa$zz .git/objects/$aa/$zz +} + +check_dont_have () { + gaah= && + for N in "$@" + do + eval "o=\$$N" + git cat-file -t $o && { + echo Gaah $N + gaah=$N + break + } + done + test -z "$gaah" +} + +test_expect_success setup ' + mkdir -p A/B && + echo rat >C && + echo ox >A/D && + echo tiger >A/B/E && + git add . && + + test_tick && git commit -m rabbit && + H=`git rev-parse --verify HEAD` && + A=`git rev-parse --verify HEAD:A` && + B=`git rev-parse --verify HEAD:A/B` && + C=`git rev-parse --verify HEAD:C` && + D=`git rev-parse --verify HEAD:A/D` && + E=`git rev-parse --verify HEAD:A/B/E` && + check_fsck && + + chmod +x C && + ( test "`git config --bool core.filemode`" != false || + echo executable >>C ) && + git add C && + test_tick && git commit -m dragon && + L=`git rev-parse --verify HEAD` && + check_fsck && + + rm -f C A/B/E && + echo snake >F && + echo horse >A/G && + git add F A/G && + test_tick && git commit -a -m sheep && + F=`git rev-parse --verify HEAD:F` && + G=`git rev-parse --verify HEAD:A/G` && + I=`git rev-parse --verify HEAD:A` && + J=`git rev-parse --verify HEAD` && + check_fsck && + + rm -f A/G && + test_tick && git commit -a -m monkey && + K=`git rev-parse --verify HEAD` && + check_fsck && + + check_have A B C D E F G H I J K L && + + git prune && + + check_have A B C D E F G H I J K L && + + check_fsck && + + loglen=$(wc -l <.git/logs/refs/heads/master) && + test $loglen = 4 +' + +test_expect_success rewind ' + test_tick && git reset --hard HEAD~2 && + test -f C && + test -f A/B/E && + ! test -f F && + ! test -f A/G && + + check_have A B C D E F G H I J K L && + + git prune && + + check_have A B C D E F G H I J K L && + + loglen=$(wc -l <.git/logs/refs/heads/master) && + test $loglen = 5 +' + +test_expect_success 'corrupt and check' ' + + corrupt $F && + check_fsck "missing blob $F" + +' + +test_expect_success 'reflog expire --dry-run should not touch reflog' ' + + git reflog expire --dry-run \ + --expire=$(($test_tick - 10000)) \ + --expire-unreachable=$(($test_tick - 10000)) \ + --stale-fix \ + --all && + + loglen=$(wc -l <.git/logs/refs/heads/master) && + test $loglen = 5 && + + check_fsck "missing blob $F" +' + +test_expect_success 'reflog expire' ' + + git reflog expire --verbose \ + --expire=$(($test_tick - 10000)) \ + --expire-unreachable=$(($test_tick - 10000)) \ + --stale-fix \ + --all && + + loglen=$(wc -l <.git/logs/refs/heads/master) && + test $loglen = 2 && + + check_fsck "dangling commit $K" +' + +test_expect_success 'prune and fsck' ' + + git prune && + check_fsck && + + check_have A B C D E H L && + check_dont_have F G I J K + +' + +test_expect_success 'recover and check' ' + + recover $F && + check_fsck "dangling blob $F" + +' + +test_done diff --git a/t/t2000-checkout-cache-clash.sh b/t/t2000-checkout-cache-clash.sh new file mode 100755 index 0000000000..03ea4dece4 --- /dev/null +++ b/t/t2000-checkout-cache-clash.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-checkout-index test. + +This test registers the following filesystem structure in the +cache: + + path0 - a file + path1/file1 - a file in a directory + +And then tries to checkout in a work tree that has the following: + + path0/file0 - a file in a directory + path1 - a file + +The git-checkout-index command should fail when attempting to checkout +path0, finding it is occupied by a directory, and path1/file1, finding +path1 is occupied by a non-directory. With "-f" flag, it should remove +the conflicting paths and succeed. +' +. ./test-lib.sh + +date >path0 +mkdir path1 +date >path1/file1 + +test_expect_success \ + 'git-update-index --add various paths.' \ + 'git-update-index --add path0 path1/file1' + +rm -fr path0 path1 +mkdir path0 +date >path0/file0 +date >path1 + +test_expect_failure \ + 'git-checkout-index without -f should fail on conflicting work tree.' \ + 'git-checkout-index -a' + +test_expect_success \ + 'git-checkout-index with -f should succeed.' \ + 'git-checkout-index -f -a' + +test_expect_success \ + 'git-checkout-index conflicting paths.' \ + 'test -f path0 && test -d path1 && test -f path1/file1' + +test_done + + diff --git a/t/t2001-checkout-cache-clash.sh b/t/t2001-checkout-cache-clash.sh new file mode 100755 index 0000000000..0dcab8f1de --- /dev/null +++ b/t/t2001-checkout-cache-clash.sh @@ -0,0 +1,87 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-checkout-index test. + +This test registers the following filesystem structure in the cache: + + path0/file0 - a file in a directory + path1/file1 - a file in a directory + +and attempts to check it out when the work tree has: + + path0/file0 - a file in a directory + path1 - a symlink pointing at "path0" + +Checkout cache should fail to extract path1/file1 because the leading +path path1 is occupied by a non-directory. With "-f" it should remove +the symlink path1 and create directory path1 and file path1/file1. +' +. ./test-lib.sh + +show_files() { + # show filesystem files, just [-dl] for type and name + find path? -ls | + sed -e 's/^[0-9]* * [0-9]* * \([-bcdl]\)[^ ]* *[0-9]* *[^ ]* *[^ ]* *[0-9]* [A-Z][a-z][a-z] [0-9][0-9] [^ ]* /fs: \1 /' + # what's in the cache, just mode and name + git-ls-files --stage | + sed -e 's/^\([0-9]*\) [0-9a-f]* [0-3] /ca: \1 /' + # what's in the tree, just mode and name. + git-ls-tree -r "$1" | + sed -e 's/^\([0-9]*\) [^ ]* [0-9a-f]* /tr: \1 /' +} + +mkdir path0 +date >path0/file0 +test_expect_success \ + 'git-update-index --add path0/file0' \ + 'git-update-index --add path0/file0' +test_expect_success \ + 'writing tree out with git-write-tree' \ + 'tree1=$(git-write-tree)' +test_debug 'show_files $tree1' + +mkdir path1 +date >path1/file1 +test_expect_success \ + 'git-update-index --add path1/file1' \ + 'git-update-index --add path1/file1' +test_expect_success \ + 'writing tree out with git-write-tree' \ + 'tree2=$(git-write-tree)' +test_debug 'show_files $tree2' + +rm -fr path1 +test_expect_success \ + 'read previously written tree and checkout.' \ + 'git-read-tree -m $tree1 && git-checkout-index -f -a' +test_debug 'show_files $tree1' + +ln -s path0 path1 +test_expect_success \ + 'git-update-index --add a symlink.' \ + 'git-update-index --add path1' +test_expect_success \ + 'writing tree out with git-write-tree' \ + 'tree3=$(git-write-tree)' +test_debug 'show_files $tree3' + +# Morten says "Got that?" here. +# Test begins. + +test_expect_success \ + 'read previously written tree and checkout.' \ + 'git-read-tree $tree2 && git-checkout-index -f -a' +test_debug 'show_files $tree2' + +test_expect_success \ + 'checking out conflicting path with -f' \ + 'test ! -h path0 && test -d path0 && + test ! -h path1 && test -d path1 && + test ! -h path0/file0 && test -f path0/file0 && + test ! -h path1/file1 && test -f path1/file1' + +test_done + diff --git a/t/t2002-checkout-cache-u.sh b/t/t2002-checkout-cache-u.sh new file mode 100755 index 0000000000..4352ddb1cb --- /dev/null +++ b/t/t2002-checkout-cache-u.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-checkout-index -u test. + +With -u flag, git-checkout-index internally runs the equivalent of +git-update-index --refresh on the checked out entry.' + +. ./test-lib.sh + +test_expect_success \ +'preparation' ' +echo frotz >path0 && +git-update-index --add path0 && +t=$(git-write-tree)' + +test_expect_failure \ +'without -u, git-checkout-index smudges stat information.' ' +rm -f path0 && +git-read-tree $t && +git-checkout-index -f -a && +git-diff-files | diff - /dev/null' + +test_expect_success \ +'with -u, git-checkout-index picks up stat information from new files.' ' +rm -f path0 && +git-read-tree $t && +git-checkout-index -u -f -a && +git-diff-files | diff - /dev/null' + +test_done diff --git a/t/t2003-checkout-cache-mkdir.sh b/t/t2003-checkout-cache-mkdir.sh new file mode 100755 index 0000000000..f9bc90aee4 --- /dev/null +++ b/t/t2003-checkout-cache-mkdir.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-checkout-index --prefix test. + +This test makes sure that --prefix option works as advertised, and +also verifies that such leading path may contain symlinks, unlike +the GIT controlled paths. +' + +. ./test-lib.sh + +test_expect_success \ + 'setup' \ + 'mkdir path1 && + echo frotz >path0 && + echo rezrov >path1/file1 && + git-update-index --add path0 path1/file1' + +test_expect_success \ + 'have symlink in place where dir is expected.' \ + 'rm -fr path0 path1 && + mkdir path2 && + ln -s path2 path1 && + git-checkout-index -f -a && + test ! -h path1 && test -d path1 && + test -f path1/file1 && test ! -f path2/file1' + +test_expect_success \ + 'use --prefix=path2/' \ + 'rm -fr path0 path1 path2 && + mkdir path2 && + git-checkout-index --prefix=path2/ -f -a && + test -f path2/path0 && + test -f path2/path1/file1 && + test ! -f path0 && + test ! -f path1/file1' + +test_expect_success \ + 'use --prefix=tmp-' \ + 'rm -fr path0 path1 path2 tmp* && + git-checkout-index --prefix=tmp- -f -a && + test -f tmp-path0 && + test -f tmp-path1/file1 && + test ! -f path0 && + test ! -f path1/file1' + +test_expect_success \ + 'use --prefix=tmp- but with a conflicting file and dir' \ + 'rm -fr path0 path1 path2 tmp* && + echo nitfol >tmp-path1 && + mkdir tmp-path0 && + git-checkout-index --prefix=tmp- -f -a && + test -f tmp-path0 && + test -f tmp-path1/file1 && + test ! -f path0 && + test ! -f path1/file1' + +# Linus fix #1 +test_expect_success \ + 'use --prefix=tmp/orary/ where tmp is a symlink' \ + 'rm -fr path0 path1 path2 tmp* && + mkdir tmp1 tmp1/orary && + ln -s tmp1 tmp && + git-checkout-index --prefix=tmp/orary/ -f -a && + test -d tmp1/orary && + test -f tmp1/orary/path0 && + test -f tmp1/orary/path1/file1 && + test -h tmp' + +# Linus fix #2 +test_expect_success \ + 'use --prefix=tmp/orary- where tmp is a symlink' \ + 'rm -fr path0 path1 path2 tmp* && + mkdir tmp1 && + ln -s tmp1 tmp && + git-checkout-index --prefix=tmp/orary- -f -a && + test -f tmp1/orary-path0 && + test -f tmp1/orary-path1/file1 && + test -h tmp' + +# Linus fix #3 +test_expect_success \ + 'use --prefix=tmp- where tmp-path1 is a symlink' \ + 'rm -fr path0 path1 path2 tmp* && + mkdir tmp1 && + ln -s tmp1 tmp-path1 && + git-checkout-index --prefix=tmp- -f -a && + test -f tmp-path0 && + test ! -h tmp-path1 && + test -d tmp-path1 && + test -f tmp-path1/file1' + +test_done diff --git a/t/t2004-checkout-cache-temp.sh b/t/t2004-checkout-cache-temp.sh new file mode 100755 index 0000000000..c100959cad --- /dev/null +++ b/t/t2004-checkout-cache-temp.sh @@ -0,0 +1,212 @@ +#!/bin/sh +# +# Copyright (c) 2006 Shawn Pearce +# + +test_description='git-checkout-index --temp test. + +With --temp flag, git-checkout-index writes to temporary merge files +rather than the tracked path.' + +. ./test-lib.sh + +test_expect_success \ +'preparation' ' +mkdir asubdir && +echo tree1path0 >path0 && +echo tree1path1 >path1 && +echo tree1path3 >path3 && +echo tree1path4 >path4 && +echo tree1asubdir/path5 >asubdir/path5 && +git-update-index --add path0 path1 path3 path4 asubdir/path5 && +t1=$(git-write-tree) && +rm -f path* .merge_* out .git/index && +echo tree2path0 >path0 && +echo tree2path1 >path1 && +echo tree2path2 >path2 && +echo tree2path4 >path4 && +git-update-index --add path0 path1 path2 path4 && +t2=$(git-write-tree) && +rm -f path* .merge_* out .git/index && +echo tree2path0 >path0 && +echo tree3path1 >path1 && +echo tree3path2 >path2 && +echo tree3path3 >path3 && +git-update-index --add path0 path1 path2 path3 && +t3=$(git-write-tree)' + +test_expect_success \ +'checkout one stage 0 to temporary file' ' +rm -f path* .merge_* out .git/index && +git-read-tree $t1 && +git-checkout-index --temp -- path1 >out && +test $(wc -l <out) = 1 && +test $(cut "-d " -f2 out) = path1 && +p=$(cut "-d " -f1 out) && +test -f $p && +test $(cat $p) = tree1path1' + +test_expect_success \ +'checkout all stage 0 to temporary files' ' +rm -f path* .merge_* out .git/index && +git-read-tree $t1 && +git-checkout-index -a --temp >out && +test $(wc -l <out) = 5 && +for f in path0 path1 path3 path4 asubdir/path5 +do + test $(grep $f out | cut "-d " -f2) = $f && + p=$(grep $f out | cut "-d " -f1) && + test -f $p && + test $(cat $p) = tree1$f +done' + +test_expect_success \ +'prepare 3-way merge' ' +rm -f path* .merge_* out .git/index && +git-read-tree -m $t1 $t2 $t3' + +test_expect_success \ +'checkout one stage 2 to temporary file' ' +rm -f path* .merge_* out && +git-checkout-index --stage=2 --temp -- path1 >out && +test $(wc -l <out) = 1 && +test $(cut "-d " -f2 out) = path1 && +p=$(cut "-d " -f1 out) && +test -f $p && +test $(cat $p) = tree2path1' + +test_expect_success \ +'checkout all stage 2 to temporary files' ' +rm -f path* .merge_* out && +git-checkout-index --all --stage=2 --temp >out && +test $(wc -l <out) = 3 && +for f in path1 path2 path4 +do + test $(grep $f out | cut "-d " -f2) = $f && + p=$(grep $f out | cut "-d " -f1) && + test -f $p && + test $(cat $p) = tree2$f +done' + +test_expect_success \ +'checkout all stages/one file to nothing' ' +rm -f path* .merge_* out && +git-checkout-index --stage=all --temp -- path0 >out && +test $(wc -l <out) = 0' + +test_expect_success \ +'checkout all stages/one file to temporary files' ' +rm -f path* .merge_* out && +git-checkout-index --stage=all --temp -- path1 >out && +test $(wc -l <out) = 1 && +test $(cut "-d " -f2 out) = path1 && +cut "-d " -f1 out | (read s1 s2 s3 && +test -f $s1 && +test -f $s2 && +test -f $s3 && +test $(cat $s1) = tree1path1 && +test $(cat $s2) = tree2path1 && +test $(cat $s3) = tree3path1)' + +test_expect_success \ +'checkout some stages/one file to temporary files' ' +rm -f path* .merge_* out && +git-checkout-index --stage=all --temp -- path2 >out && +test $(wc -l <out) = 1 && +test $(cut "-d " -f2 out) = path2 && +cut "-d " -f1 out | (read s1 s2 s3 && +test $s1 = . && +test -f $s2 && +test -f $s3 && +test $(cat $s2) = tree2path2 && +test $(cat $s3) = tree3path2)' + +test_expect_success \ +'checkout all stages/all files to temporary files' ' +rm -f path* .merge_* out && +git-checkout-index -a --stage=all --temp >out && +test $(wc -l <out) = 5' + +test_expect_success \ +'-- path0: no entry' ' +test x$(grep path0 out | cut "-d " -f2) = x' + +test_expect_success \ +'-- path1: all 3 stages' ' +test $(grep path1 out | cut "-d " -f2) = path1 && +grep path1 out | cut "-d " -f1 | (read s1 s2 s3 && +test -f $s1 && +test -f $s2 && +test -f $s3 && +test $(cat $s1) = tree1path1 && +test $(cat $s2) = tree2path1 && +test $(cat $s3) = tree3path1)' + +test_expect_success \ +'-- path2: no stage 1, have stage 2 and 3' ' +test $(grep path2 out | cut "-d " -f2) = path2 && +grep path2 out | cut "-d " -f1 | (read s1 s2 s3 && +test $s1 = . && +test -f $s2 && +test -f $s3 && +test $(cat $s2) = tree2path2 && +test $(cat $s3) = tree3path2)' + +test_expect_success \ +'-- path3: no stage 2, have stage 1 and 3' ' +test $(grep path3 out | cut "-d " -f2) = path3 && +grep path3 out | cut "-d " -f1 | (read s1 s2 s3 && +test -f $s1 && +test $s2 = . && +test -f $s3 && +test $(cat $s1) = tree1path3 && +test $(cat $s3) = tree3path3)' + +test_expect_success \ +'-- path4: no stage 3, have stage 1 and 3' ' +test $(grep path4 out | cut "-d " -f2) = path4 && +grep path4 out | cut "-d " -f1 | (read s1 s2 s3 && +test -f $s1 && +test -f $s2 && +test $s3 = . && +test $(cat $s1) = tree1path4 && +test $(cat $s2) = tree2path4)' + +test_expect_success \ +'-- asubdir/path5: no stage 2 and 3 have stage 1' ' +test $(grep asubdir/path5 out | cut "-d " -f2) = asubdir/path5 && +grep asubdir/path5 out | cut "-d " -f1 | (read s1 s2 s3 && +test -f $s1 && +test $s2 = . && +test $s3 = . && +test $(cat $s1) = tree1asubdir/path5)' + +test_expect_success \ +'checkout --temp within subdir' ' +(cd asubdir && + git-checkout-index -a --stage=all >out && + test $(wc -l <out) = 1 && + test $(grep path5 out | cut "-d " -f2) = path5 && + grep path5 out | cut "-d " -f1 | (read s1 s2 s3 && + test -f ../$s1 && + test $s2 = . && + test $s3 = . && + test $(cat ../$s1) = tree1asubdir/path5) +)' + +test_expect_success \ +'checkout --temp symlink' ' +rm -f path* .merge_* out .git/index && +ln -s b a && +git-update-index --add a && +t4=$(git-write-tree) && +rm -f .git/index && +git-read-tree $t4 && +git-checkout-index --temp -a >out && +test $(wc -l <out) = 1 && +test $(cut "-d " -f2 out) = a && +p=$(cut "-d " -f1 out) && +test -f $p && +test $(cat $p) = b' + +test_done diff --git a/t/t2100-update-cache-badpath.sh b/t/t2100-update-cache-badpath.sh new file mode 100755 index 0000000000..5bc0a3bed3 --- /dev/null +++ b/t/t2100-update-cache-badpath.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-update-index nonsense-path test. + +This test creates the following structure in the cache: + + path0 - a file + path1 - a symlink + path2/file2 - a file in a directory + path3/file3 - a file in a directory + +and tries to git-update-index --add the following: + + path0/file0 - a file in a directory + path1/file1 - a file in a directory + path2 - a file + path3 - a symlink + +All of the attempts should fail. +' + +. ./test-lib.sh + +mkdir path2 path3 +date >path0 +ln -s xyzzy path1 +date >path2/file2 +date >path3/file3 + +test_expect_success \ + 'git-update-index --add to add various paths.' \ + 'git-update-index --add -- path0 path1 path2/file2 path3/file3' + +rm -fr path? + +mkdir path0 path1 +date >path2 +ln -s frotz path3 +date >path0/file0 +date >path1/file1 + +for p in path0/file0 path1/file1 path2 path3 +do + test_expect_failure \ + "git-update-index to add conflicting path $p should fail." \ + "git-update-index --add -- $p" +done +test_done diff --git a/t/t2101-update-index-reupdate.sh b/t/t2101-update-index-reupdate.sh new file mode 100755 index 0000000000..a78ea7f0b0 --- /dev/null +++ b/t/t2101-update-index-reupdate.sh @@ -0,0 +1,84 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='git-update-index --again test. +' + +. ./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 expected' + +test_expect_success 'update-index --again' \ + 'rm -f file1 && + echo hello everybody >file2 && + if git-update-index --again + then + echo should have refused to remove file1 + exit 1 + else + echo happy - failed as expected + fi && + git-ls-files -s >current && + 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 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 goodbye people >file2 && + git-update-index --add file2 dir1/file3 && + echo hello everybody >file2 + echo happy >dir1/file3 && + git-update-index --again && + git-ls-files -s >current && + 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 && + cat ../file2 >file3 && + git-update-index --again && + cd .. && + git-ls-files -s >current && + 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 expected' + +test_done diff --git a/t/t3000-ls-files-others.sh b/t/t3000-ls-files-others.sh new file mode 100755 index 0000000000..adcbe03d56 --- /dev/null +++ b/t/t3000-ls-files-others.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-ls-files test (--others should pick up symlinks). + +This test runs git-ls-files --others with the following on the +filesystem. + + path0 - a file + path1 - a symlink + path2/file2 - a file in a directory + path3-junk - a file to confuse things + path3/file3 - a file in a directory +' +. ./test-lib.sh + +date >path0 +ln -s xyzzy path1 +mkdir path2 path3 +date >path2/file2 +date >path2-junk +date >path3/file3 +date >path3-junk +git-update-index --add path3-junk path3/file3 + +cat >expected1 <<EOF +expected1 +expected2 +output +path0 +path1 +path2-junk +path2/file2 +EOF +sed -e 's|path2/file2|path2/|' <expected1 >expected2 + +test_expect_success \ + 'git-ls-files --others to show output.' \ + 'git-ls-files --others >output' + +test_expect_success \ + 'git-ls-files --others should pick up symlinks.' \ + 'diff output expected1' + +test_expect_success \ + 'git-ls-files --others --directory to show output.' \ + 'git-ls-files --others --directory >output' + + +test_expect_success \ + 'git-ls-files --others --directory should not get confused.' \ + 'diff output expected2' + +test_done diff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh new file mode 100755 index 0000000000..6979b7c1c0 --- /dev/null +++ b/t/t3001-ls-files-others-exclude.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-ls-files --others --exclude + +This test runs git-ls-files --others and tests --exclude patterns. +' + +. ./test-lib.sh + +rm -fr one three +for dir in . one one/two three +do + mkdir -p $dir && + for i in 1 2 3 4 5 6 7 8 + do + >$dir/a.$i + done +done + +cat >expect <<EOF +a.2 +a.4 +a.5 +a.8 +one/a.3 +one/a.4 +one/a.5 +one/a.7 +one/two/a.2 +one/two/a.3 +one/two/a.5 +one/two/a.7 +one/two/a.8 +three/a.2 +three/a.3 +three/a.4 +three/a.5 +three/a.8 +EOF + +echo '.gitignore +output +expect +.gitignore +*.7 +!*.8' >.git/ignore + +echo '*.1 +/*.3 +!*.6' >.gitignore +echo '*.2 +two/*.4 +!*.7 +*.8' >one/.gitignore +echo '!*.2 +!*.8' >one/two/.gitignore + +test_expect_success \ + 'git-ls-files --others with various exclude options.' \ + 'git-ls-files --others \ + --exclude=\*.6 \ + --exclude-per-directory=.gitignore \ + --exclude-from=.git/ignore \ + >output && + diff -u expect output' + +# Test \r\n (MSDOS-like systems) +printf '*.1\r\n/*.3\r\n!*.6\r\n' >.gitignore + +test_expect_success \ + 'git-ls-files --others with \r\n line endings.' \ + 'git-ls-files --others \ + --exclude=\*.6 \ + --exclude-per-directory=.gitignore \ + --exclude-from=.git/ignore \ + >output && + diff -u expect output' + +test_done diff --git a/t/t3002-ls-files-dashpath.sh b/t/t3002-ls-files-dashpath.sh new file mode 100755 index 0000000000..b42f1382bc --- /dev/null +++ b/t/t3002-ls-files-dashpath.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-ls-files test (-- to terminate the path list). + +This test runs git-ls-files --others with the following on the +filesystem. + + path0 - a file + -foo - a file with a funny name. + -- - another file with a funny name. +' +. ./test-lib.sh + +test_expect_success \ + setup \ + 'echo frotz >path0 && + echo frotz >./-foo && + echo frotz >./--' + +test_expect_success \ + 'git-ls-files without path restriction.' \ + 'git-ls-files --others >output && + diff -u output - <<EOF +-- +-foo +output +path0 +EOF +' + +test_expect_success \ + 'git-ls-files with path restriction.' \ + 'git-ls-files --others path0 >output && + diff -u output - <<EOF +path0 +EOF +' + +test_expect_success \ + 'git-ls-files with path restriction with --.' \ + 'git-ls-files --others -- path0 >output && + diff -u output - <<EOF +path0 +EOF +' + +test_expect_success \ + 'git-ls-files with path restriction with -- --.' \ + 'git-ls-files --others -- -- >output && + diff -u output - <<EOF +-- +EOF +' + +test_expect_success \ + 'git-ls-files with no path restriction.' \ + 'git-ls-files --others -- >output && + diff -u output - <<EOF +-- +-foo +output +path0 +EOF +' + +test_done diff --git a/t/t3010-ls-files-killed-modified.sh b/t/t3010-ls-files-killed-modified.sh new file mode 100755 index 0000000000..5fc1976711 --- /dev/null +++ b/t/t3010-ls-files-killed-modified.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-ls-files -k and -m flags test. + +This test prepares the following in the cache: + + path0 - a file + path1 - a symlink + path2/file2 - a file in a directory + path3/file3 - a file in a directory + +and the following on the filesystem: + + path0/file0 - a file in a directory + path1/file1 - a file in a directory + path2 - a file + path3 - a symlink + path4 - a file + path5 - a symlink + path6/file6 - a file in a directory + +git-ls-files -k should report that existing filesystem +objects except path4, path5 and path6/file6 to be killed. + +Also for modification test, the cache and working tree have: + + path7 - an empty file, modified to a non-empty file. + path8 - a non-empty file, modified to an empty file. + path9 - an empty file, cache dirtied. + path10 - a non-empty file, cache dirtied. + +We should report path0, path1, path2/file2, path3/file3, path7 and path8 +modified without reporting path9 and path10. +' +. ./test-lib.sh + +date >path0 +ln -s xyzzy path1 +mkdir path2 path3 +date >path2/file2 +date >path3/file3 +: >path7 +date >path8 +: >path9 +date >path10 +test_expect_success \ + 'git-update-index --add to add various paths.' \ + "git-update-index --add -- path0 path1 path?/file? path7 path8 path9 path10" + +rm -fr path? ;# leave path10 alone +date >path2 +ln -s frotz path3 +ln -s nitfol path5 +mkdir path0 path1 path6 +date >path0/file0 +date >path1/file1 +date >path6/file6 +date >path7 +: >path8 +: >path9 +touch path10 + +test_expect_success \ + 'git-ls-files -k to show killed files.' \ + 'git-ls-files -k >.output' +cat >.expected <<EOF +path0/file0 +path1/file1 +path2 +path3 +EOF + +test_expect_success \ + 'validate git-ls-files -k output.' \ + 'diff .output .expected' + +test_expect_success \ + 'git-ls-files -m to show modified files.' \ + 'git-ls-files -m >.output' +cat >.expected <<EOF +path0 +path1 +path2/file2 +path3/file3 +path7 +path8 +EOF + +test_expect_success \ + 'validate git-ls-files -m output.' \ + 'diff .output .expected' + +test_done diff --git a/t/t3020-ls-files-error-unmatch.sh b/t/t3020-ls-files-error-unmatch.sh new file mode 100755 index 0000000000..d55559e553 --- /dev/null +++ b/t/t3020-ls-files-error-unmatch.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# +# Copyright (c) 2006 Carl D. Worth +# + +test_description='git-ls-files test for --error-unmatch option + +This test runs git-ls-files --error-unmatch to ensure it correctly +returns an error when a non-existent path is provided on the command +line. +' +. ./test-lib.sh + +touch foo bar +git-update-index --add foo bar +git-commit -m "add foo bar" + +test_expect_failure \ + 'git-ls-files --error-unmatch should fail with unmatched path.' \ + 'git-ls-files --error-unmatch foo bar-does-not-match' + +test_expect_success \ + 'git-ls-files --error-unmatch should succeed eith matched paths.' \ + 'git-ls-files --error-unmatch foo bar' + +test_done +1 diff --git a/t/t3100-ls-tree-restrict.sh b/t/t3100-ls-tree-restrict.sh new file mode 100755 index 0000000000..2ec06d3d39 --- /dev/null +++ b/t/t3100-ls-tree-restrict.sh @@ -0,0 +1,158 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-ls-tree test. + +This test runs git-ls-tree with the following in a tree. + + path0 - a file + path1 - a symlink + path2/foo - a file in a directory + path2/bazbo - a symlink in a directory + path2/baz/b - a file in a directory in a directory + +The new path restriction code should do the right thing for path2 and +path2/baz. Also path0/ should snow nothing. +' +. ./test-lib.sh + +test_expect_success \ + 'setup' \ + 'mkdir path2 path2/baz && + echo Hi >path0 && + ln -s path0 path1 && + echo Lo >path2/foo && + ln -s ../path1 path2/bazbo && + echo Mi >path2/baz/b && + find path? \( -type f -o -type l \) -print | + xargs git-update-index --add && + tree=`git-write-tree` && + echo $tree' + +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" +test_output () { + sed -e "s/ $_x40 / X /" <current >check + diff -u expected check +} + +test_expect_success \ + 'ls-tree plain' \ + 'git-ls-tree $tree >current && + cat >expected <<\EOF && +100644 blob X path0 +120000 blob X path1 +040000 tree X path2 +EOF + test_output' + +test_expect_success \ + 'ls-tree recursive' \ + 'git-ls-tree -r $tree >current && + cat >expected <<\EOF && +100644 blob X path0 +120000 blob X path1 +100644 blob X path2/baz/b +120000 blob X path2/bazbo +100644 blob X path2/foo +EOF + test_output' + +test_expect_success \ + 'ls-tree recursive with -t' \ + 'git-ls-tree -r -t $tree >current && + cat >expected <<\EOF && +100644 blob X path0 +120000 blob X path1 +040000 tree X path2 +040000 tree X path2/baz +100644 blob X path2/baz/b +120000 blob X path2/bazbo +100644 blob X path2/foo +EOF + test_output' + +test_expect_success \ + 'ls-tree recursive with -d' \ + 'git-ls-tree -r -d $tree >current && + cat >expected <<\EOF && +040000 tree X path2 +040000 tree X path2/baz +EOF + test_output' + +test_expect_success \ + 'ls-tree filtered with path' \ + 'git-ls-tree $tree path >current && + cat >expected <<\EOF && +EOF + test_output' + + +# it used to be path1 and then path0, but with pathspec semantics +# they are shown in canonical order. +test_expect_success \ + 'ls-tree filtered with path1 path0' \ + 'git-ls-tree $tree path1 path0 >current && + cat >expected <<\EOF && +100644 blob X path0 +120000 blob X path1 +EOF + test_output' + +test_expect_success \ + 'ls-tree filtered with path0/' \ + 'git-ls-tree $tree path0/ >current && + cat >expected <<\EOF && +EOF + test_output' + +# It used to show path2 and its immediate children but +# with pathspec semantics it shows only path2 +test_expect_success \ + 'ls-tree filtered with path2' \ + 'git-ls-tree $tree path2 >current && + cat >expected <<\EOF && +040000 tree X path2 +EOF + test_output' + +# ... and path2/ shows the children. +test_expect_success \ + 'ls-tree filtered with path2/' \ + 'git-ls-tree $tree path2/ >current && + cat >expected <<\EOF && +040000 tree X path2/baz +120000 blob X path2/bazbo +100644 blob X path2/foo +EOF + test_output' + +# The same change -- exact match does not show children of +# path2/baz +test_expect_success \ + 'ls-tree filtered with path2/baz' \ + 'git-ls-tree $tree path2/baz >current && + cat >expected <<\EOF && +040000 tree X path2/baz +EOF + test_output' + +test_expect_success \ + 'ls-tree filtered with path2/bak' \ + 'git-ls-tree $tree path2/bak >current && + cat >expected <<\EOF && +EOF + test_output' + +test_expect_success \ + 'ls-tree -t filtered with path2/bak' \ + 'git-ls-tree -t $tree path2/bak >current && + cat >expected <<\EOF && +040000 tree X path2 +EOF + test_output' + +test_done diff --git a/t/t3101-ls-tree-dirname.sh b/t/t3101-ls-tree-dirname.sh new file mode 100755 index 0000000000..d78deb1e71 --- /dev/null +++ b/t/t3101-ls-tree-dirname.sh @@ -0,0 +1,138 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# Copyright (c) 2005 Robert Fitzsimons +# + +test_description='git-ls-tree directory and filenames handling. + +This test runs git-ls-tree with the following in a tree. + + 1.txt - a file + 2.txt - a file + path0/a/b/c/1.txt - a file in a directory + path1/b/c/1.txt - a file in a directory + path2/1.txt - a file in a directory + path3/1.txt - a file in a directory + path3/2.txt - a file in a directory + +Test the handling of mulitple directories which have matching file +entries. Also test odd filename and missing entries handling. +' +. ./test-lib.sh + +test_expect_success \ + 'setup' \ + 'echo 111 >1.txt && + echo 222 >2.txt && + mkdir path0 path0/a path0/a/b path0/a/b/c && + echo 111 >path0/a/b/c/1.txt && + mkdir path1 path1/b path1/b/c && + echo 111 >path1/b/c/1.txt && + mkdir path2 && + echo 111 >path2/1.txt && + mkdir path3 && + echo 111 >path3/1.txt && + echo 222 >path3/2.txt && + find *.txt path* \( -type f -o -type l \) -print | + xargs git-update-index --add && + tree=`git-write-tree` && + echo $tree' + +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" +test_output () { + sed -e "s/ $_x40 / X /" <current >check + diff -u expected check +} + +test_expect_success \ + 'ls-tree plain' \ + 'git-ls-tree $tree >current && + cat >expected <<\EOF && +100644 blob X 1.txt +100644 blob X 2.txt +040000 tree X path0 +040000 tree X path1 +040000 tree X path2 +040000 tree X path3 +EOF + test_output' + +# Recursive does not show tree nodes anymore... +test_expect_success \ + 'ls-tree recursive' \ + 'git-ls-tree -r $tree >current && + cat >expected <<\EOF && +100644 blob X 1.txt +100644 blob X 2.txt +100644 blob X path0/a/b/c/1.txt +100644 blob X path1/b/c/1.txt +100644 blob X path2/1.txt +100644 blob X path3/1.txt +100644 blob X path3/2.txt +EOF + test_output' + +test_expect_success \ + 'ls-tree filter 1.txt' \ + 'git-ls-tree $tree 1.txt >current && + cat >expected <<\EOF && +100644 blob X 1.txt +EOF + test_output' + +test_expect_success \ + 'ls-tree filter path1/b/c/1.txt' \ + 'git-ls-tree $tree path1/b/c/1.txt >current && + cat >expected <<\EOF && +100644 blob X path1/b/c/1.txt +EOF + test_output' + +test_expect_success \ + 'ls-tree filter all 1.txt files' \ + 'git-ls-tree $tree 1.txt path0/a/b/c/1.txt path1/b/c/1.txt path2/1.txt path3/1.txt >current && + cat >expected <<\EOF && +100644 blob X 1.txt +100644 blob X path0/a/b/c/1.txt +100644 blob X path1/b/c/1.txt +100644 blob X path2/1.txt +100644 blob X path3/1.txt +EOF + test_output' + +# I am not so sure about this one after ls-tree doing pathspec match. +# Having both path0/a and path0/a/b/c makes path0/a redundant, and +# it behaves as if path0/a/b/c, path1/b/c, path2 and path3 are specified. +test_expect_success \ + 'ls-tree filter directories' \ + 'git-ls-tree $tree path3 path2 path0/a/b/c path1/b/c path0/a >current && + cat >expected <<\EOF && +040000 tree X path0/a/b/c +040000 tree X path1/b/c +040000 tree X path2 +040000 tree X path3 +EOF + test_output' + +# Again, duplicates are filtered away so this is equivalent to +# having 1.txt and path3 +test_expect_success \ + 'ls-tree filter odd names' \ + 'git-ls-tree $tree 1.txt /1.txt //1.txt path3/1.txt /path3/1.txt //path3//1.txt path3 /path3/ path3// >current && + cat >expected <<\EOF && +100644 blob X 1.txt +100644 blob X path3/1.txt +100644 blob X path3/2.txt +EOF + test_output' + +test_expect_success \ + 'ls-tree filter missing files and extra slashes' \ + 'git-ls-tree $tree 1.txt/ abc.txt path3//23.txt path3/2.txt/// >current && + cat >expected <<\EOF && +EOF + test_output' + +test_done diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh new file mode 100755 index 0000000000..5565c27033 --- /dev/null +++ b/t/t3200-branch.sh @@ -0,0 +1,120 @@ +#!/bin/sh +# +# Copyright (c) 2005 Amos Waterland +# + +test_description='git branch --foo should not create bogus branch + +This test runs git branch --help and checks that the argument is properly +handled. Specifically, that a bogus branch is not created. +' +. ./test-lib.sh + +test_expect_success \ + 'prepare an trivial repository' \ + 'echo Hello > A && + git-update-index --add A && + git-commit -m "Initial commit." && + HEAD=$(git-rev-parse --verify HEAD)' + +test_expect_failure \ + 'git branch --help should not have created a bogus branch' \ + 'git-branch --help </dev/null >/dev/null 2>/dev/null || : + test -f .git/refs/heads/--help' + +test_expect_success \ + 'git branch abc should create a branch' \ + 'git-branch abc && test -f .git/refs/heads/abc' + +test_expect_success \ + '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 master +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 +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_expect_success \ + 'git branch j/k should work after branch j has been deleted' \ + 'git-branch j && + git-branch -d j && + git-branch j/k' + +test_expect_success \ + 'git branch l should work after branch l/m has been deleted' \ + 'git-branch l/m && + git-branch -d l/m && + git-branch l' + +test_expect_success \ + 'git branch -m m m/m should work' \ + 'git-branch -l m && + git-branch -m m m/m && + test -f .git/logs/refs/heads/m/m' + +test_expect_success \ + 'git branch -m n/n n should work' \ + 'git-branch -l n/n && + git-branch -m n/n n + test -f .git/logs/refs/heads/n' + +test_expect_failure \ + 'git branch -m o/o o should fail when o/p exists' \ + 'git-branch o/o && + git-branch o/p && + git-branch -m o/o o' + +test_expect_failure \ + 'git branch -m q r/q should fail when r exists' \ + 'git-branch q && + git-branch r && + git-branch -m q r/q' + +git-config branch.s/s.dummy Hello + +test_expect_success \ + 'git branch -m s/s s should work when s/t is deleted' \ + 'git-branch -l s/s && + test -f .git/logs/refs/heads/s/s && + git-branch -l s/t && + test -f .git/logs/refs/heads/s/t && + git-branch -d s/t && + git-branch -m s/s s && + test -f .git/logs/refs/heads/s' + +test_expect_success 'config information was renamed, too' \ + "test $(git-config branch.s.dummy) = Hello && + ! git-config branch.s/s/dummy" + +test_expect_failure \ + 'git-branch -m u v should fail when the reflog for u is a symlink' \ + 'git-branch -l u && + mv .git/logs/refs/heads/u real-u && + ln -s real-u .git/logs/refs/heads/u && + git-branch -m u v' + +test_done diff --git a/t/t3210-pack-refs.sh b/t/t3210-pack-refs.sh new file mode 100755 index 0000000000..f0c7e22b36 --- /dev/null +++ b/t/t3210-pack-refs.sh @@ -0,0 +1,108 @@ +#!/bin/sh +# +# Copyright (c) 2005 Amos Waterland +# Copyright (c) 2006 Christian Couder +# + +test_description='git pack-refs should not change the branch semantic + +This test runs git pack-refs and git show-ref and checks that the branch +semantic is still the same. +' +. ./test-lib.sh + +echo '[core] logallrefupdates = true' >>.git/config + +test_expect_success \ + 'prepare a trivial repository' \ + 'echo Hello > A && + git-update-index --add A && + git-commit -m "Initial commit." && + HEAD=$(git-rev-parse --verify HEAD)' + +SHA1= + +test_expect_success \ + 'see if git show-ref works as expected' \ + 'git-branch a && + SHA1=`cat .git/refs/heads/a` && + echo "$SHA1 refs/heads/a" >expect && + git-show-ref a >result && + diff expect result' + +test_expect_success \ + 'see if a branch still exists when packed' \ + 'git-branch b && + git-pack-refs --all && + rm -f .git/refs/heads/b && + echo "$SHA1 refs/heads/b" >expect && + git-show-ref b >result && + diff expect result' + +test_expect_failure \ + 'git branch c/d should barf if branch c exists' \ + 'git-branch c && + git-pack-refs --all && + rm .git/refs/heads/c && + git-branch c/d' + +test_expect_success \ + 'see if a branch still exists after git pack-refs --prune' \ + 'git-branch e && + git-pack-refs --all --prune && + echo "$SHA1 refs/heads/e" >expect && + git-show-ref e >result && + diff expect result' + +test_expect_failure \ + 'see if git pack-refs --prune remove ref files' \ + 'git-branch f && + git-pack-refs --all --prune && + ls .git/refs/heads/f' + +test_expect_success \ + 'git branch g should work when git branch g/h has been deleted' \ + 'git-branch g/h && + git-pack-refs --all --prune && + git-branch -d g/h && + git-branch g && + git-pack-refs --all && + git-branch -d g' + +test_expect_failure \ + 'git branch i/j/k should barf if branch i exists' \ + 'git-branch i && + git-pack-refs --all --prune && + git-branch i/j/k' + +test_expect_success \ + 'test git branch k after branch k/l/m and k/lm have been deleted' \ + 'git-branch k/l && + git-branch k/lm && + git-branch -d k/l && + git-branch k/l/m && + git-branch -d k/l/m && + git-branch -d k/lm && + git-branch k' + +test_expect_success \ + 'test git branch n after some branch deletion and pruning' \ + 'git-branch n/o && + git-branch n/op && + git-branch -d n/o && + git-branch n/o/p && + git-branch -d n/op && + git-pack-refs --all --prune && + git-branch -d n/o/p && + git-branch n' + +test_expect_success 'pack, prune and repack' ' + git-tag foo && + git-pack-refs --all --prune && + git-show-ref >all-of-them && + git-pack-refs && + git-show-ref >again && + diff all-of-them again +' + +test_done diff --git a/t/t3300-funny-names.sh b/t/t3300-funny-names.sh new file mode 100755 index 0000000000..c12270efab --- /dev/null +++ b/t/t3300-funny-names.sh @@ -0,0 +1,160 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Pathnames with funny characters. + +This test tries pathnames with funny characters in the working +tree, index, and tree objects. +' + +. ./test-lib.sh + +p0='no-funny' +p1='tabs ," (dq) and spaces' +p2='just space' + +cat >"$p0" <<\EOF +1. A quick brown fox jumps over the lazy cat, oops dog. +2. A quick brown fox jumps over the lazy cat, oops dog. +3. A quick brown fox jumps over the lazy cat, oops dog. +EOF + +cat >"$p1" "$p0" +echo 'Foo Bar Baz' >"$p2" + +test -f "$p1" && cmp "$p0" "$p1" || { + # since FAT/NTFS does not allow tabs in filenames, skip this test + say 'Your filesystem does not allow tabs in filenames, test skipped.' + test_done +} + +echo 'just space +no-funny' >expected +test_expect_success 'git-ls-files no-funny' \ + 'git-update-index --add "$p0" "$p2" && + git-ls-files >current && + diff -u expected current' + +t0=`git-write-tree` +echo "$t0" >t0 + +cat > expected <<\EOF +just space +no-funny +"tabs\t,\" (dq) and spaces" +EOF +test_expect_success 'git-ls-files with-funny' \ + 'git-update-index --add "$p1" && + git-ls-files >current && + diff -u expected current' + +echo 'just space +no-funny +tabs ," (dq) and spaces' >expected +test_expect_success 'git-ls-files -z with-funny' \ + 'git-ls-files -z | tr \\0 \\012 >current && + diff -u expected current' + +t1=`git-write-tree` +echo "$t1" >t1 + +cat > expected <<\EOF +just space +no-funny +"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' + +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' + +test_expect_success 'git-diff-tree with-funny' \ + 'git-diff-tree --name-status $t0 $t1 >current && + diff -u expected current' + +echo 'A +tabs ," (dq) and spaces' >expected +test_expect_success 'git-diff-index -z with-funny' \ + 'git-diff-index -z --name-status $t0 | tr \\0 \\012 >current && + diff -u expected current' + +test_expect_success 'git-diff-tree -z with-funny' \ + 'git-diff-tree -z --name-status $t0 $t1 | tr \\0 \\012 >current && + diff -u expected current' + +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' + +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' + +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" +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" +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" +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' + +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' + +cat > expected <<\EOF + no-funny + "tabs\t,\" (dq) and spaces" + 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 && + diff -u expected current' + +test_expect_success 'git-apply non-git diff' \ + 'git-diff-index -p $t0 | + sed -ne "/^[-+@]/p" | + git-apply --stat | sed -e "s/|.*//" -e "s/ *\$//" >current && + diff -u expected current' + +test_done diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh new file mode 100755 index 0000000000..b9d3131cc2 --- /dev/null +++ b/t/t3400-rebase.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# +# Copyright (c) 2005 Amos Waterland +# + +test_description='git rebase should not destroy author information + +This test runs git rebase and checks that the author information is not lost. +' +. ./test-lib.sh + +export GIT_AUTHOR_EMAIL=bogus_email_address + +test_expect_success \ + 'prepare repository with topic branch, then rebase against master' \ + 'echo First > A && + git-update-index --add A && + git-commit -m "Add A." && + git checkout -b my-topic-branch && + echo Second > B && + git-update-index --add B && + git-commit -m "Add B." && + git checkout -f master && + echo Third >> A && + git-update-index A && + git-commit -m "Modify A." && + git checkout -f my-topic-branch && + git rebase master' + +test_expect_failure \ + 'the rebase operation should not have destroyed author information' \ + 'git log | grep "Author:" | grep "<>"' + +test_done diff --git a/t/t3401-rebase-partial.sh b/t/t3401-rebase-partial.sh new file mode 100755 index 0000000000..8b19d3ccea --- /dev/null +++ b/t/t3401-rebase-partial.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# +# Copyright (c) 2006 Yann Dirson, based on t3400 by Amos Waterland +# + +test_description='git rebase should detect patches integrated upstream + +This test cherry-picks one local change of two into master branch, and +checks that git rebase succeeds with only the second patch in the +local branch. +' +. ./test-lib.sh + +test_expect_success \ + 'prepare repository with topic branch' \ + 'echo First > A && + git-update-index --add A && + git-commit -m "Add A." && + + git-checkout -b my-topic-branch && + + echo Second > B && + git-update-index --add B && + git-commit -m "Add B." && + + echo AnotherSecond > C && + git-update-index --add C && + git-commit -m "Add C." && + + git-checkout -f master && + + echo Third >> A && + git-update-index A && + git-commit -m "Modify A." +' + +test_expect_success \ + 'pick top patch from topic branch into master' \ + 'git-cherry-pick my-topic-branch^0 && + git-checkout -f my-topic-branch && + git-branch master-merge master && + git-branch my-topic-branch-merge my-topic-branch +' + +test_debug \ + 'git-cherry master && + git-format-patch -k --stdout --full-index master >/dev/null && + gitk --all & sleep 1 +' + +test_expect_success \ + 'rebase topic branch against new master and check git-am did not get halted' \ + 'git-rebase master && test ! -d .dotest' + +test_expect_success \ + 'rebase --merge topic branch that was partially merged upstream' \ + 'git-checkout -f my-topic-branch-merge && + git-rebase --merge master-merge && + test ! -d .git/.dotest-merge' + +test_done diff --git a/t/t3402-rebase-merge.sh b/t/t3402-rebase-merge.sh new file mode 100755 index 0000000000..0779aaa9ab --- /dev/null +++ b/t/t3402-rebase-merge.sh @@ -0,0 +1,106 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='git rebase --merge test' + +. ./test-lib.sh + +T="A quick brown fox +jumps over the lazy dog." +for i in 1 2 3 4 5 6 7 8 9 10 +do + echo "$i $T" +done >original + +test_expect_success setup ' + git add original && + git commit -m"initial" && + git branch side && + echo "11 $T" >>original && + git commit -a -m"master updates a bit." && + + echo "12 $T" >>original && + git commit -a -m"master updates a bit more." && + + git checkout side && + (echo "0 $T" ; cat original) >renamed && + git add renamed && + git update-index --force-remove original && + git commit -a -m"side renames and edits." && + + tr "[a-z]" "[A-Z]" <original >newfile && + git add newfile && + git commit -a -m"side edits further." && + + tr "[a-m]" "[A-M]" <original >newfile && + rm -f original && + git commit -a -m"side edits once again." && + + git branch test-rebase side && + git branch test-rebase-pick side && + git branch test-reference-pick side && + git checkout -b test-merge side +' + +test_expect_success 'reference merge' ' + git merge -s recursive "reference merge" HEAD master +' + +test_expect_success rebase ' + git checkout test-rebase && + git rebase --merge master +' + +test_expect_success 'merge and rebase should match' ' + git diff-tree -r test-rebase test-merge >difference && + if test -s difference + then + cat difference + (exit 1) + else + echo happy + fi +' + +test_expect_success 'rebase the other way' ' + git reset --hard master && + git rebase --merge side +' + +test_expect_success 'merge and rebase should match' ' + git diff-tree -r test-rebase test-merge >difference && + if test -s difference + then + cat difference + (exit 1) + else + echo happy + fi +' + +test_expect_success 'picking rebase' ' + git reset --hard side && + git rebase --merge --onto master side^^ && + mb=$(git merge-base master HEAD) && + if test "$mb" = "$(git rev-parse master)" + then + echo happy + else + git show-branch + (exit 1) + fi && + f=$(git diff-tree --name-only HEAD^ HEAD) && + g=$(git diff-tree --name-only HEAD^^ HEAD^) && + case "$f,$g" in + newfile,newfile) + echo happy ;; + *) + echo "$f" + echo "$g" + (exit 1) + esac +' + +test_done diff --git a/t/t3403-rebase-skip.sh b/t/t3403-rebase-skip.sh new file mode 100755 index 0000000000..977c498f00 --- /dev/null +++ b/t/t3403-rebase-skip.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# +# Copyright (c) 2006 Eric Wong +# + +test_description='git rebase --merge --skip tests' + +. ./test-lib.sh + +# we assume the default git-am -3 --skip strategy is tested independently +# and always works :) + +test_expect_success setup ' + echo hello > hello && + git add hello && + git commit -m "hello" && + git branch skip-reference && + + echo world >> hello && + git commit -a -m "hello world" && + echo goodbye >> hello && + git commit -a -m "goodbye" && + + git checkout -f skip-reference && + echo moo > hello && + git commit -a -m "we should skip this" && + echo moo > cow && + git add cow && + git commit -m "this should not be skipped" && + git branch pre-rebase skip-reference && + git branch skip-merge skip-reference + ' + +test_expect_failure 'rebase with git am -3 (default)' ' + git rebase master +' + +test_expect_success 'rebase --skip with am -3' ' + git reset --hard HEAD && + git rebase --skip + ' +test_expect_success 'checkout skip-merge' 'git checkout -f skip-merge' + +test_expect_failure 'rebase with --merge' 'git rebase --merge master' + +test_expect_success 'rebase --skip with --merge' ' + git reset --hard HEAD && + git rebase --skip + ' + +test_expect_success 'merge and reference trees equal' \ + 'test -z "`git-diff-tree skip-merge skip-reference`"' + +test_debug 'gitk --all & sleep 1' + +test_done + diff --git a/t/t3500-cherry.sh b/t/t3500-cherry.sh new file mode 100755 index 0000000000..e83bbee074 --- /dev/null +++ b/t/t3500-cherry.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# +# Copyright (c) 2006 Yann Dirson, based on t3400 by Amos Waterland +# + +test_description='git-cherry should detect patches integrated upstream + +This test cherry-picks one local change of two into master branch, and +checks that git-cherry only returns the second patch in the local branch +' +. ./test-lib.sh + +export GIT_AUTHOR_EMAIL=bogus_email_address + +test_expect_success \ + 'prepare repository with topic branch, and check cherry finds the 2 patches from there' \ + 'echo First > A && + git-update-index --add A && + git-commit -m "Add A." && + + git-checkout -b my-topic-branch && + + echo Second > B && + git-update-index --add B && + git-commit -m "Add B." && + + sleep 2 && + echo AnotherSecond > C && + git-update-index --add C && + git-commit -m "Add C." && + + git-checkout -f master && + rm -f B C && + + echo Third >> A && + git-update-index A && + git-commit -m "Modify A." && + + expr "$(echo $(git-cherry master my-topic-branch) )" : "+ [^ ]* + .*" +' + +test_expect_success \ + 'check that cherry with limit returns only the top patch'\ + 'expr "$(echo $(git-cherry master my-topic-branch my-topic-branch^1) )" : "+ [^ ]*" +' + +test_expect_success \ + 'cherry-pick one of the 2 patches, and check cherry recognized one and only one as new' \ + 'git-cherry-pick my-topic-branch^0 && + echo $(git-cherry master my-topic-branch) && + expr "$(echo $(git-cherry master my-topic-branch) )" : "+ [^ ]* - .*" +' + +test_done diff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh new file mode 100755 index 0000000000..552af1c4d2 --- /dev/null +++ b/t/t3501-revert-cherry-pick.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +test_description='test cherry-pick and revert with renames + + -- + + rename2: renames oops to opos + + rename1: renames oops to spoo + + added: adds extra line to oops + ++ initial: has lines in oops + +' + +. ./test-lib.sh + +test_expect_success setup ' + + for l in a b c d e f g h i j k l m n o + do + echo $l$l$l$l$l$l$l$l$l + done >oops && + + test_tick && + git add oops && + git commit -m initial && + git tag initial && + + test_tick && + echo "Add extra line at the end" >>oops && + git commit -a -m added && + git tag added && + + test_tick && + git mv oops spoo && + git commit -m rename1 && + git tag rename1 && + + test_tick && + git checkout -b side initial && + git mv oops opos && + git commit -m rename2 && + git tag rename2 +' + +test_expect_success 'cherry-pick after renaming branch' ' + + git checkout rename2 && + EDITOR=: VISUAL=: git cherry-pick added && + test -f opos && + grep "Add extra line at the end" opos + +' + +test_expect_success 'revert after renaming branch' ' + + git checkout rename1 && + EDITOR=: VISUAL=: git revert added && + test -f spoo && + ! grep "Add extra line at the end" spoo + +' + +test_done diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh new file mode 100755 index 0000000000..e31cf93a00 --- /dev/null +++ b/t/t3600-rm.sh @@ -0,0 +1,157 @@ +#!/bin/sh +# +# Copyright (c) 2006 Carl D. Worth +# + +test_description='Test of the various options to git-rm.' + +. ./test-lib.sh + +# Setup some files to be removed, some with funny characters +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' + 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_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' \ + '[ -f foo ] && git-ls-files --error-unmatch foo' + +test_expect_success \ + 'Test that git-rm foo succeeds' \ + 'git-rm --cached foo' + +test_expect_success \ + 'Post-check that foo exists but is not in index after git-rm foo' \ + '[ -f foo ] && ! git-ls-files --error-unmatch foo' + +test_expect_success \ + 'Pre-check that bar exists and is in index before "git-rm bar"' \ + '[ -f bar ] && git-ls-files --error-unmatch bar' + +test_expect_success \ + 'Test that "git-rm bar" succeeds' \ + 'git-rm bar' + +test_expect_success \ + 'Post-check that bar does not exist and is not in index after "git-rm -f bar"' \ + '! [ -f bar ] && ! git-ls-files --error-unmatch bar' + +test_expect_success \ + 'Test that "git-rm -- -q" succeeds (remove a file that looks like an option)' \ + 'git-rm -- -q' + +test "$test_tabs" = y && test_expect_success \ + "Test that \"git-rm -f\" succeeds with embedded space, tab, or newline characters." \ + "git-rm -f 'space embedded' 'tab embedded' 'newline +embedded'" + +if test "$test_failed_remove" = y; then +chmod a-w . +test_expect_failure \ + 'Test that "git-rm -f" fails if its rm fails' \ + 'git-rm -f baz' +chmod 775 . +else + test_expect_success 'skipping removal failure (perhaps running as root?)' : +fi + +test_expect_success \ + 'When the rm in "git-rm -f" fails, it should not remove the file from the index' \ + 'git-ls-files --error-unmatch baz' + +# Now, failure cases. +test_expect_success 'Re-add foo and baz' ' + git add foo baz && + git ls-files --error-unmatch foo baz +' + +test_expect_success 'Modify foo -- rm should refuse' ' + echo >>foo && + ! git rm foo baz && + test -f foo && + test -f baz && + git ls-files --error-unmatch foo baz +' + +test_expect_success 'Modified foo -- rm -f should work' ' + git rm -f foo baz && + test ! -f foo && + test ! -f baz && + ! git ls-files --error-unmatch foo && + ! git ls-files --error-unmatch bar +' + +test_expect_success 'Re-add foo and baz for HEAD tests' ' + echo frotz >foo && + git checkout HEAD -- baz && + git add foo baz && + git ls-files --error-unmatch foo baz +' + +test_expect_success 'foo is different in index from HEAD -- rm should refuse' ' + ! git rm foo baz && + test -f foo && + test -f baz && + git ls-files --error-unmatch foo baz +' + +test_expect_success 'but with -f it should work.' ' + git rm -f foo baz && + test ! -f foo && + test ! -f baz && + ! git ls-files --error-unmatch foo + ! git ls-files --error-unmatch baz +' + +test_expect_success 'Recursive test setup' ' + mkdir -p frotz && + echo qfwfq >frotz/nitfol && + git add frotz && + git commit -m "subdir test" +' + +test_expect_success 'Recursive without -r fails' ' + ! git rm frotz && + test -d frotz && + test -f frotz/nitfol +' + +test_expect_success 'Recursive with -r but dirty' ' + echo qfwfq >>frotz/nitfol + ! git rm -r frotz && + test -d frotz && + test -f frotz/nitfol +' + +test_expect_success 'Recursive with -r -f' ' + git rm -f -r frotz && + ! test -f frotz/nitfol && + ! test -d frotz +' + +test_done diff --git a/t/t3700-add.sh b/t/t3700-add.sh new file mode 100755 index 0000000000..caaab26c2f --- /dev/null +++ b/t/t3700-add.sh @@ -0,0 +1,87 @@ +#!/bin/sh +# +# Copyright (c) 2006 Carl D. Worth +# + +test_description='Test of git-add, including the -- option.' + +. ./test-lib.sh + +test_expect_success \ + 'Test of git-add' \ + 'touch foo && git-add foo' + +test_expect_success \ + 'Post-check that foo is in the index' \ + 'git-ls-files foo | grep foo' + +test_expect_success \ + 'Test that "git-add -- -q" works' \ + 'touch -- -q && git-add -- -q' + +test_expect_success \ + 'git-add: Test that executable bit is not used if core.filemode=0' \ + 'git config core.filemode 0 && + echo foo >xfoo1 && + chmod 755 xfoo1 && + git-add xfoo1 && + case "`git-ls-files --stage xfoo1`" in + 100644" "*xfoo1) echo ok;; + *) echo fail; git-ls-files --stage xfoo1; (exit 1);; + esac' + +test_expect_success \ + 'git-update-index --add: Test that executable bit is not used...' \ + 'git config core.filemode 0 && + echo foo >xfoo2 && + chmod 755 xfoo2 && + git-update-index --add xfoo2 && + case "`git-ls-files --stage xfoo2`" in + 100644" "*xfoo2) echo ok;; + *) echo fail; git-ls-files --stage xfoo2; (exit 1);; + esac' + +test_expect_success \ + 'git-update-index --add: Test that executable bit is not used...' \ + 'git config core.filemode 0 && + ln -s xfoo2 xfoo3 && + git-update-index --add xfoo3 && + case "`git-ls-files --stage xfoo3`" in + 120000" "*xfoo3) echo ok;; + *) echo fail; git-ls-files --stage xfoo3; (exit 1);; + esac' + +test_expect_success '.gitignore test setup' ' + echo "*.ig" >.gitignore && + mkdir c.if d.ig && + >a.ig && >b.if && + >c.if/c.if && >c.if/c.ig && + >d.ig/d.if && >d.ig/d.ig +' + +test_expect_success '.gitignore is honored' ' + git-add . && + ! git-ls-files | grep "\\.ig" +' + +test_expect_success 'error out when attempting to add ignored ones without -f' ' + ! git-add a.?? && + ! git-ls-files | grep "\\.ig" +' + +test_expect_success 'error out when attempting to add ignored ones without -f' ' + ! git-add d.?? && + ! git-ls-files | grep "\\.ig" +' + +test_expect_success 'add ignored ones with -f' ' + git-add -f a.?? && + git-ls-files --error-unmatch a.ig +' + +test_expect_success 'add ignored ones with -f' ' + git-add -f d.??/* && + git-ls-files --error-unmatch d.ig/d.if d.ig/d.ig +' + +test_done diff --git a/t/t3800-mktag.sh b/t/t3800-mktag.sh new file mode 100755 index 0000000000..7c7e4335d6 --- /dev/null +++ b/t/t3800-mktag.sh @@ -0,0 +1,227 @@ +#!/bin/sh +# +# + +test_description='git-mktag: tag object verify test' + +. ./test-lib.sh + +########################################################### +# check the tag.sig file, expecting verify_tag() to fail, +# and checking that the error message matches the pattern +# given in the expect.pat file. + +check_verify_failure () { + test_expect_success \ + "$1" \ + 'git-mktag <tag.sig 2>message || + egrep -q -f expect.pat message' +} + +########################################################### +# first create a commit, so we have a valid object/type +# for the tag. +echo Hello >A +git-update-index --add A +git-commit -m "Initial commit" +head=$(git-rev-parse --verify HEAD) + +############################################################ +# 1. length check + +cat >tag.sig <<EOF +too short for a tag +EOF + +cat >expect.pat <<EOF +^error: .*size wrong.*$ +EOF + +check_verify_failure 'Tag object length check' + +############################################################ +# 2. object line label check + +cat >tag.sig <<EOF +xxxxxx 139e9b33986b1c2670fff52c5067603117b3e895 +type tag +tag mytag +EOF + +cat >expect.pat <<EOF +^error: char0: .*"object "$ +EOF + +check_verify_failure '"object" line label check' + +############################################################ +# 3. object line SHA1 check + +cat >tag.sig <<EOF +object zz9e9b33986b1c2670fff52c5067603117b3e895 +type tag +tag mytag +EOF + +cat >expect.pat <<EOF +^error: char7: .*SHA1 hash$ +EOF + +check_verify_failure '"object" line SHA1 check' + +############################################################ +# 4. type line label check + +cat >tag.sig <<EOF +object 779e9b33986b1c2670fff52c5067603117b3e895 +xxxx tag +tag mytag +EOF + +cat >expect.pat <<EOF +^error: char47: .*"[\]ntype "$ +EOF + +check_verify_failure '"type" line label check' + +############################################################ +# 5. type line eol check + +echo "object 779e9b33986b1c2670fff52c5067603117b3e895" >tag.sig +printf "type tagsssssssssssssssssssssssssssssss" >>tag.sig + +cat >expect.pat <<EOF +^error: char48: .*"[\]n"$ +EOF + +check_verify_failure '"type" line eol check' + +############################################################ +# 6. tag line label check #1 + +cat >tag.sig <<EOF +object 779e9b33986b1c2670fff52c5067603117b3e895 +type tag +xxx mytag +EOF + +cat >expect.pat <<EOF +^error: char57: no "tag " found$ +EOF + +check_verify_failure '"tag" line label check #1' + +############################################################ +# 7. tag line label check #2 + +cat >tag.sig <<EOF +object 779e9b33986b1c2670fff52c5067603117b3e895 +type taggggggggggggggggggggggggggggggg +tag +EOF + +cat >expect.pat <<EOF +^error: char87: no "tag " found$ +EOF + +check_verify_failure '"tag" line label check #2' + +############################################################ +# 8. type line type-name length check + +cat >tag.sig <<EOF +object 779e9b33986b1c2670fff52c5067603117b3e895 +type taggggggggggggggggggggggggggggggg +tag mytag +EOF + +cat >expect.pat <<EOF +^error: char53: type too long$ +EOF + +check_verify_failure '"type" line type-name length check' + +############################################################ +# 9. verify object (SHA1/type) check + +cat >tag.sig <<EOF +object 779e9b33986b1c2670fff52c5067603117b3e895 +type tagggg +tag mytag +EOF + +cat >expect.pat <<EOF +^error: char7: could not verify object.*$ +EOF + +check_verify_failure 'verify object (SHA1/type) check' + +############################################################ +# 10. verify tag-name check + +cat >tag.sig <<EOF +object $head +type commit +tag my tag +EOF + +cat >expect.pat <<EOF +^error: char67: could not verify tag name$ +EOF + +check_verify_failure 'verify tag-name check' + +############################################################ +# 11. tagger line label check #1 + +cat >tag.sig <<EOF +object $head +type commit +tag mytag +EOF + +cat >expect.pat <<EOF +^error: char70: could not find "tagger"$ +EOF + +check_verify_failure '"tagger" line label check #1' + +############################################################ +# 12. tagger line label check #2 + +cat >tag.sig <<EOF +object $head +type commit +tag mytag +tagger +EOF + +cat >expect.pat <<EOF +^error: char70: could not find "tagger"$ +EOF + +check_verify_failure '"tagger" line label check #2' + +############################################################ +# 13. create valid tag + +cat >tag.sig <<EOF +object $head +type commit +tag mytag +tagger another@example.com +EOF + +test_expect_success \ + 'create valid tag' \ + 'git-mktag <tag.sig >.git/refs/tags/mytag 2>message' + +############################################################ +# 14. check mytag + +test_expect_success \ + 'check mytag' \ + 'git-tag -l | grep mytag' + + +test_done diff --git a/t/t3900-i18n-commit.sh b/t/t3900-i18n-commit.sh new file mode 100755 index 0000000000..e54fe0f401 --- /dev/null +++ b/t/t3900-i18n-commit.sh @@ -0,0 +1,122 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='commit and log output encodings' + +. ./test-lib.sh + +compare_with () { + git-show -s $1 | sed -e '1,/^$/d' -e 's/^ //' -e '$d' >current && + diff -u current "$2" +} + +test_expect_success setup ' + : >F && + git-add F && + T=$(git-write-tree) && + C=$(git-commit-tree $T <../t3900/1-UTF-8.txt) && + git-update-ref HEAD $C && + git-tag C0 +' + +test_expect_success 'no encoding header for base case' ' + E=$(git-cat-file commit C0 | sed -ne "s/^encoding //p") && + test z = "z$E" +' + +for H in ISO-8859-1 EUCJP ISO-2022-JP +do + test_expect_success "$H setup" ' + git-config i18n.commitencoding $H && + git-checkout -b $H C0 && + echo $H >F && + git-commit -a -F ../t3900/$H.txt + ' +done + +for H in ISO-8859-1 EUCJP ISO-2022-JP +do + test_expect_success "check encoding header for $H" ' + E=$(git-cat-file commit '$H' | sed -ne "s/^encoding //p") && + test "z$E" = "z'$H'" + ' +done + +test_expect_success 'config to remove customization' ' + git-config --unset-all i18n.commitencoding && + if Z=$(git-config --get-all i18n.commitencoding) + then + echo Oops, should have failed. + false + else + test z = "z$Z" + fi && + git-config i18n.commitencoding utf-8 +' + +test_expect_success 'ISO-8859-1 should be shown in UTF-8 now' ' + compare_with ISO-8859-1 ../t3900/1-UTF-8.txt +' + +for H in EUCJP ISO-2022-JP +do + test_expect_success "$H should be shown in UTF-8 now" ' + compare_with '$H' ../t3900/2-UTF-8.txt + ' +done + +test_expect_success 'config to add customization' ' + git-config --unset-all i18n.commitencoding && + if Z=$(git-config --get-all i18n.commitencoding) + then + echo Oops, should have failed. + false + else + test z = "z$Z" + fi +' + +for H in ISO-8859-1 EUCJP ISO-2022-JP +do + test_expect_success "$H should be shown in itself now" ' + git-config i18n.commitencoding '$H' && + compare_with '$H' ../t3900/'$H'.txt + ' +done + +test_expect_success 'config to tweak customization' ' + git-config i18n.logoutputencoding utf-8 +' + +test_expect_success 'ISO-8859-1 should be shown in UTF-8 now' ' + compare_with ISO-8859-1 ../t3900/1-UTF-8.txt +' + +for H in EUCJP ISO-2022-JP +do + test_expect_success "$H should be shown in UTF-8 now" ' + compare_with '$H' ../t3900/2-UTF-8.txt + ' +done + +for J in EUCJP ISO-2022-JP +do + git-config i18n.logoutputencoding $J + for H in EUCJP ISO-2022-JP + do + test_expect_success "$H should be shown in $J now" ' + compare_with '$H' ../t3900/'$J'.txt + ' + done +done + +for H in ISO-8859-1 EUCJP ISO-2022-JP +do + test_expect_success "No conversion with $H" ' + compare_with "--encoding=none '$H'" ../t3900/'$H'.txt + ' +done + +test_done diff --git a/t/t3900/1-UTF-8.txt b/t/t3900/1-UTF-8.txt new file mode 100644 index 0000000000..ee31e19738 --- /dev/null +++ b/t/t3900/1-UTF-8.txt @@ -0,0 +1,3 @@ +ÄËÑÃÖ + +Ãbçdèfg diff --git a/t/t3900/2-UTF-8.txt b/t/t3900/2-UTF-8.txt new file mode 100644 index 0000000000..63f4f8f121 --- /dev/null +++ b/t/t3900/2-UTF-8.txt @@ -0,0 +1,4 @@ +ã¯ã‚Œã²ã»ãµ + +ã—ã¦ã„ã‚‹ã®ãŒã€ã„ã‚‹ã®ã§ã€‚ +濱浜ã»ã‚Œã·ã‚Šã½ã‚Œã¾ã³ãりã‚ã¸ã€‚ diff --git a/t/t3900/EUCJP.txt b/t/t3900/EUCJP.txt new file mode 100644 index 0000000000..546f2aac01 --- /dev/null +++ b/t/t3900/EUCJP.txt @@ -0,0 +1,4 @@ +¤Ï¤ì¤Ò¤Û¤Õ + +¤·¤Æ¤¤¤ë¤Î¤¬¡¢¤¤¤ë¤Î¤Ç¡£ +ßÀÉͤۤì¤×¤ê¤Ý¤ì¤Þ¤Ó¤°¤ê¤í¤Ø¡£ diff --git a/t/t3900/ISO-2022-JP.txt b/t/t3900/ISO-2022-JP.txt new file mode 100644 index 0000000000..74b533042f --- /dev/null +++ b/t/t3900/ISO-2022-JP.txt @@ -0,0 +1,4 @@ +$B$O$l$R$[$U(B + +$B$7$F$$$k$N$,!"$$$k$N$G!#(B +$B_@IM$[$l$W$j$]$l$^$S$0$j$m$X!#(B diff --git a/t/t3900/ISO-8859-1.txt b/t/t3900/ISO-8859-1.txt new file mode 100644 index 0000000000..7cbef0ee6f --- /dev/null +++ b/t/t3900/ISO-8859-1.txt @@ -0,0 +1,3 @@ +ÄËÑÏÖ + +Ábçdèfg diff --git a/t/t3901-8859-1.txt b/t/t3901-8859-1.txt new file mode 100755 index 0000000000..38c21a6a7f --- /dev/null +++ b/t/t3901-8859-1.txt @@ -0,0 +1,4 @@ +: to be sourced in t3901 -- this is latin-1 +GIT_AUTHOR_NAME="Áéí óú" && +GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME && +export GIT_AUTHOR_NAME GIT_COMMITTER_NAME diff --git a/t/t3901-i18n-patch.sh b/t/t3901-i18n-patch.sh new file mode 100755 index 0000000000..a881797bc7 --- /dev/null +++ b/t/t3901-i18n-patch.sh @@ -0,0 +1,255 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='i18n settings and format-patch | am pipe' + +. ./test-lib.sh + +check_encoding () { + # Make sure characters are not corrupted + cnt="$1" header="$2" i=1 j=0 bad=0 + while test "$i" -le $cnt + do + git format-patch --encoding=UTF-8 --stdout HEAD~$i..HEAD~$j | + grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD_=C3=B3=C3=BA?=" && + git-cat-file commit HEAD~$j | + case "$header" in + 8859) + grep "^encoding ISO-8859-1" ;; + *) + ! grep "^encoding ISO-8859-1" ;; + esac || { + bad=1 + break + } + j=$i + i=$(($i+1)) + done + (exit $bad) +} + +test_expect_success setup ' + git-config i18n.commitencoding UTF-8 && + + # use UTF-8 in author and committer name to match the + # i18n.commitencoding settings + . ../t3901-utf8.txt && + + test_tick && + echo "$GIT_AUTHOR_NAME" >mine && + git add mine && + git commit -s -m "Initial commit" && + + test_tick && + echo Hello world >mine && + git add mine && + git commit -s -m "Second on main" && + + # the first commit on the side branch is UTF-8 + test_tick && + git checkout -b side master^ && + echo Another file >yours && + git add yours && + git commit -s -m "Second on side" && + + # the second one on the side branch is ISO-8859-1 + git-config i18n.commitencoding ISO-8859-1 && + # use author and committer name in ISO-8859-1 to match it. + . ../t3901-8859-1.txt && + test_tick && + echo Yet another >theirs && + git add theirs && + git commit -s -m "Third on side" && + + # Back to default + git-config i18n.commitencoding UTF-8 +' + +test_expect_success 'format-patch output (ISO-8859-1)' ' + git-config i18n.logoutputencoding ISO-8859-1 && + + git format-patch --stdout master..HEAD^ >out-l1 && + git format-patch --stdout HEAD^ >out-l2 && + grep "^Content-Type: text/plain; charset=ISO-8859-1" out-l1 && + grep "^From: =?ISO-8859-1?q?=C1=E9=ED_=F3=FA?=" out-l1 && + grep "^Content-Type: text/plain; charset=ISO-8859-1" out-l2 && + grep "^From: =?ISO-8859-1?q?=C1=E9=ED_=F3=FA?=" out-l2 +' + +test_expect_success 'format-patch output (UTF-8)' ' + git config i18n.logoutputencoding UTF-8 && + + git format-patch --stdout master..HEAD^ >out-u1 && + git format-patch --stdout HEAD^ >out-u2 && + grep "^Content-Type: text/plain; charset=UTF-8" out-u1 && + grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD_=C3=B3=C3=BA?=" out-u1 && + grep "^Content-Type: text/plain; charset=UTF-8" out-u2 && + grep "^From: =?UTF-8?q?=C3=81=C3=A9=C3=AD_=C3=B3=C3=BA?=" out-u2 +' + +test_expect_success 'rebase (U/U)' ' + # We want the result of rebase in UTF-8 + git-config i18n.commitencoding UTF-8 && + + # The test is about logoutputencoding not affecting the + # final outcome -- it is used internally to generate the + # patch and the log. + + git config i18n.logoutputencoding UTF-8 && + + # The result will be committed by GIT_COMMITTER_NAME -- + # we want UTF-8 encoded name. + . ../t3901-utf8.txt && + git checkout -b test && + git-rebase master && + + check_encoding 2 +' + +test_expect_success 'rebase (U/L)' ' + git-config i18n.commitencoding UTF-8 && + git config i18n.logoutputencoding ISO-8859-1 && + . ../t3901-utf8.txt && + + git reset --hard side && + git-rebase master && + + check_encoding 2 +' + +test_expect_success 'rebase (L/L)' ' + # In this test we want ISO-8859-1 encoded commits as the result + git-config i18n.commitencoding ISO-8859-1 && + git config i18n.logoutputencoding ISO-8859-1 && + . ../t3901-8859-1.txt && + + git reset --hard side && + git-rebase master && + + check_encoding 2 8859 +' + +test_expect_success 'rebase (L/U)' ' + # This is pathological -- use UTF-8 as intermediate form + # to get ISO-8859-1 results. + git-config i18n.commitencoding ISO-8859-1 && + git config i18n.logoutputencoding UTF-8 && + . ../t3901-8859-1.txt && + + git reset --hard side && + git-rebase master && + + check_encoding 2 8859 +' + +test_expect_success 'cherry-pick(U/U)' ' + # Both the commitencoding and logoutputencoding is set to UTF-8. + + git-config i18n.commitencoding UTF-8 && + git config i18n.logoutputencoding UTF-8 && + . ../t3901-utf8.txt && + + git reset --hard master && + git cherry-pick side^ && + git cherry-pick side && + EDITOR=: VISUAL=: git revert HEAD && + + check_encoding 3 +' + +test_expect_success 'cherry-pick(L/L)' ' + # Both the commitencoding and logoutputencoding is set to ISO-8859-1 + + git-config i18n.commitencoding ISO-8859-1 && + git config i18n.logoutputencoding ISO-8859-1 && + . ../t3901-8859-1.txt && + + git reset --hard master && + git cherry-pick side^ && + git cherry-pick side && + EDITOR=: VISUAL=: git revert HEAD && + + check_encoding 3 8859 +' + +test_expect_success 'cherry-pick(U/L)' ' + # Commitencoding is set to UTF-8 but logoutputencoding is ISO-8859-1 + + git-config i18n.commitencoding UTF-8 && + git config i18n.logoutputencoding ISO-8859-1 && + . ../t3901-utf8.txt && + + git reset --hard master && + git cherry-pick side^ && + git cherry-pick side && + EDITOR=: VISUAL=: git revert HEAD && + + check_encoding 3 +' + +test_expect_success 'cherry-pick(L/U)' ' + # Again, the commitencoding is set to ISO-8859-1 but + # logoutputencoding is set to UTF-8. + + git-config i18n.commitencoding ISO-8859-1 && + git config i18n.logoutputencoding UTF-8 && + . ../t3901-8859-1.txt && + + git reset --hard master && + git cherry-pick side^ && + git cherry-pick side && + EDITOR=: VISUAL=: git revert HEAD && + + check_encoding 3 8859 +' + +test_expect_success 'rebase --merge (U/U)' ' + git-config i18n.commitencoding UTF-8 && + git config i18n.logoutputencoding UTF-8 && + . ../t3901-utf8.txt && + + git reset --hard side && + git-rebase --merge master && + + check_encoding 2 +' + +test_expect_success 'rebase --merge (U/L)' ' + git-config i18n.commitencoding UTF-8 && + git config i18n.logoutputencoding ISO-8859-1 && + . ../t3901-utf8.txt && + + git reset --hard side && + git-rebase --merge master && + + check_encoding 2 +' + +test_expect_success 'rebase --merge (L/L)' ' + # In this test we want ISO-8859-1 encoded commits as the result + git-config i18n.commitencoding ISO-8859-1 && + git config i18n.logoutputencoding ISO-8859-1 && + . ../t3901-8859-1.txt && + + git reset --hard side && + git-rebase --merge master && + + check_encoding 2 8859 +' + +test_expect_success 'rebase --merge (L/U)' ' + # This is pathological -- use UTF-8 as intermediate form + # to get ISO-8859-1 results. + git-config i18n.commitencoding ISO-8859-1 && + git config i18n.logoutputencoding UTF-8 && + . ../t3901-8859-1.txt && + + git reset --hard side && + git-rebase --merge master && + + check_encoding 2 8859 +' + +test_done diff --git a/t/t3901-utf8.txt b/t/t3901-utf8.txt new file mode 100755 index 0000000000..5f5205cd02 --- /dev/null +++ b/t/t3901-utf8.txt @@ -0,0 +1,4 @@ +: to be sourced in t3901 -- this is utf8 +GIT_AUTHOR_NAME="Ãéà óú" && +GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME && +export GIT_AUTHOR_NAME GIT_COMMITTER_NAME diff --git a/t/t4000-diff-format.sh b/t/t4000-diff-format.sh new file mode 100755 index 0000000000..9c58d77cc2 --- /dev/null +++ b/t/t4000-diff-format.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Test built-in diff output engine. + +' +. ./test-lib.sh +. ../diff-lib.sh + +echo >path0 'Line 1 +Line 2 +line 3' +cat path0 >path1 +chmod +x path1 + +test_expect_success \ + 'update-cache --add two files with and without +x.' \ + 'git-update-index --add path0 path1' + +mv path0 path0- +sed -e 's/line/Line/' <path0- >path0 +chmod +x path0 +rm -f path1 +test_expect_success \ + 'git-diff-files -p after editing work tree.' \ + 'git-diff-files -p >current' + +# that's as far as it comes +if [ "$(git config --get core.filemode)" = false ] +then + say 'filemode disabled on the filesystem' + test_done +fi + +cat >expected <<\EOF +diff --git a/path0 b/path0 +old mode 100644 +new mode 100755 +--- a/path0 ++++ b/path0 +@@ -1,3 +1,3 @@ + Line 1 + Line 2 +-line 3 ++Line 3 +diff --git a/path1 b/path1 +deleted file mode 100755 +--- a/path1 ++++ /dev/null +@@ -1,3 +0,0 @@ +-Line 1 +-Line 2 +-line 3 +EOF + +test_expect_success \ + 'validate git-diff-files -p output.' \ + 'compare_diff_patch current expected' + +test_done diff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh new file mode 100755 index 0000000000..2e3c20d6b9 --- /dev/null +++ b/t/t4001-diff-rename.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Test rename detection in diff engine. + +' +. ./test-lib.sh +. ../diff-lib.sh + +echo >path0 'Line 1 +Line 2 +Line 3 +Line 4 +Line 5 +Line 6 +Line 7 +Line 8 +Line 9 +Line 10 +line 11 +Line 12 +Line 13 +Line 14 +Line 15 +' + +test_expect_success \ + 'update-cache --add a file.' \ + 'git-update-index --add path0' + +test_expect_success \ + 'write that tree.' \ + 'tree=$(git-write-tree) && echo $tree' + +sed -e 's/line/Line/' <path0 >path1 +rm -f path0 +test_expect_success \ + 'renamed and edited the file.' \ + 'git-update-index --add --remove path0 path1' + +test_expect_success \ + 'git-diff-index -p -M after rename and editing.' \ + 'git-diff-index -p -M $tree >current' +cat >expected <<\EOF +diff --git a/path0 b/path1 +rename from path0 +rename to path1 +--- a/path0 ++++ b/path1 +@@ -8,7 +8,7 @@ Line 7 + Line 8 + Line 9 + Line 10 +-line 11 ++Line 11 + Line 12 + Line 13 + Line 14 +EOF + +test_expect_success \ + 'validate the output.' \ + 'compare_diff_patch current expected' + +test_done diff --git a/t/t4002-diff-basic.sh b/t/t4002-diff-basic.sh new file mode 100755 index 0000000000..56eda63fc2 --- /dev/null +++ b/t/t4002-diff-basic.sh @@ -0,0 +1,247 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Test diff raw-output. + +' +. ./test-lib.sh +. ../lib-read-tree-m-3way.sh + +cat >.test-plain-OA <<\EOF +:000000 100644 0000000000000000000000000000000000000000 ccba72ad3888a3520b39efcf780b9ee64167535d A AA +:000000 100644 0000000000000000000000000000000000000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 A AN +:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D DD +:000000 040000 0000000000000000000000000000000000000000 6d50f65d3bdab91c63444294d38f08aeff328e42 A DF +:100644 000000 141c1f1642328e4bc46a7d801a71da392e66791e 0000000000000000000000000000000000000000 D DM +:100644 000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 0000000000000000000000000000000000000000 D DN +:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 A LL +:100644 100644 03f24c8c4700babccfd28b654e7e8eac402ad6cd 103d9f89b50b9aad03054b579be5e7aa665f2d57 M MD +:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 M MM +:100644 100644 bd084b0c27c7b6cc34f11d6d0509a29be3caf970 a716d58de4a570e0038f5c307bd8db34daea021f M MN +:100644 100644 40c959f984c8b89a2b02520d17f00d717f024397 2ac547ae9614a00d1b28275de608131f7a0e259f M SS +:100644 100644 4ac13458899ab908ef3b1128fa378daefc88d356 4c86f9a85fbc5e6804ee2e17a797538fbe785bca M TT +:040000 040000 7d670fdcdb9929f6c7dac196ff78689cd1c566a1 5e5f22072bb39f6e12cf663a57cb634c76eefb49 M Z +EOF + +cat >.test-recursive-OA <<\EOF +:000000 100644 0000000000000000000000000000000000000000 ccba72ad3888a3520b39efcf780b9ee64167535d A AA +:000000 100644 0000000000000000000000000000000000000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 A AN +:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D DD +:000000 100644 0000000000000000000000000000000000000000 68a6d8b91da11045cf4aa3a5ab9f2a781c701249 A DF/DF +:100644 000000 141c1f1642328e4bc46a7d801a71da392e66791e 0000000000000000000000000000000000000000 D DM +:100644 000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 0000000000000000000000000000000000000000 D DN +:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 A LL +:100644 100644 03f24c8c4700babccfd28b654e7e8eac402ad6cd 103d9f89b50b9aad03054b579be5e7aa665f2d57 M MD +:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 M MM +:100644 100644 bd084b0c27c7b6cc34f11d6d0509a29be3caf970 a716d58de4a570e0038f5c307bd8db34daea021f M MN +:100644 100644 40c959f984c8b89a2b02520d17f00d717f024397 2ac547ae9614a00d1b28275de608131f7a0e259f M SS +:100644 100644 4ac13458899ab908ef3b1128fa378daefc88d356 4c86f9a85fbc5e6804ee2e17a797538fbe785bca M TT +:000000 100644 0000000000000000000000000000000000000000 8acb8e9750e3f644bf323fcf3d338849db106c77 A Z/AA +:000000 100644 0000000000000000000000000000000000000000 087494262084cefee7ed484d20c8dc0580791272 A Z/AN +:100644 000000 879007efae624d2b1307214b24a956f0a8d686a8 0000000000000000000000000000000000000000 D Z/DD +:100644 000000 9b541b2275c06e3a7b13f28badf5294e2ae63df4 0000000000000000000000000000000000000000 D Z/DM +:100644 000000 beb5d38c55283d280685ea21a0e50cfcc0ca064a 0000000000000000000000000000000000000000 D Z/DN +:100644 100644 d41fda41b7ec4de46b43cb7ea42a45001ae393d5 a79ac3be9377639e1c7d1edf1ae1b3a5f0ccd8a9 M Z/MD +:100644 100644 4ca22bae2527d3d9e1676498a0fba3b355bd1278 61422ba9c2c873416061a88cd40a59a35b576474 M Z/MM +:100644 100644 b16d7b25b869f2beb124efa53467d8a1550ad694 a5c544c21cfcb07eb80a4d89a5b7d1570002edfd M Z/MN +EOF +cat >.test-plain-OB <<\EOF +:000000 100644 0000000000000000000000000000000000000000 6aa2b5335b16431a0ef71e5c0a28be69183cf6a2 A AA +:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D DD +:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 A DF +:100644 100644 141c1f1642328e4bc46a7d801a71da392e66791e 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 M DM +:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 A LL +:100644 000000 03f24c8c4700babccfd28b654e7e8eac402ad6cd 0000000000000000000000000000000000000000 D MD +:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 19989d4559aae417fedee240ccf2ba315ea4dc2b M MM +:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 A NA +:100644 000000 a4e179e4291e5536a5e1c82e091052772d2c5a93 0000000000000000000000000000000000000000 D ND +:100644 100644 c8f25781e8f1792e3e40b74225e20553041b5226 cdb9a8c3da571502ac30225e9c17beccb8387983 M NM +:100644 100644 40c959f984c8b89a2b02520d17f00d717f024397 2ac547ae9614a00d1b28275de608131f7a0e259f M SS +:100644 100644 4ac13458899ab908ef3b1128fa378daefc88d356 c4e4a12231b9fa79a0053cb6077fcb21bb5b135a M TT +:040000 040000 7d670fdcdb9929f6c7dac196ff78689cd1c566a1 1ba523955d5160681af65cb776411f574c1e8155 M Z +EOF +cat >.test-recursive-OB <<\EOF +:000000 100644 0000000000000000000000000000000000000000 6aa2b5335b16431a0ef71e5c0a28be69183cf6a2 A AA +:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D DD +:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 A DF +:100644 100644 141c1f1642328e4bc46a7d801a71da392e66791e 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 M DM +:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 A LL +:100644 000000 03f24c8c4700babccfd28b654e7e8eac402ad6cd 0000000000000000000000000000000000000000 D MD +:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 19989d4559aae417fedee240ccf2ba315ea4dc2b M MM +:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 A NA +:100644 000000 a4e179e4291e5536a5e1c82e091052772d2c5a93 0000000000000000000000000000000000000000 D ND +:100644 100644 c8f25781e8f1792e3e40b74225e20553041b5226 cdb9a8c3da571502ac30225e9c17beccb8387983 M NM +:100644 100644 40c959f984c8b89a2b02520d17f00d717f024397 2ac547ae9614a00d1b28275de608131f7a0e259f M SS +:100644 100644 4ac13458899ab908ef3b1128fa378daefc88d356 c4e4a12231b9fa79a0053cb6077fcb21bb5b135a M TT +:000000 100644 0000000000000000000000000000000000000000 6c0b99286d0bce551ac4a7b3dff8b706edff3715 A Z/AA +:100644 000000 879007efae624d2b1307214b24a956f0a8d686a8 0000000000000000000000000000000000000000 D Z/DD +:100644 100644 9b541b2275c06e3a7b13f28badf5294e2ae63df4 d77371d15817fcaa57eeec27f770c505ba974ec1 M Z/DM +:100644 000000 d41fda41b7ec4de46b43cb7ea42a45001ae393d5 0000000000000000000000000000000000000000 D Z/MD +:100644 100644 4ca22bae2527d3d9e1676498a0fba3b355bd1278 697aad7715a1e7306ca76290a3dd4208fbaeddfa M Z/MM +:000000 100644 0000000000000000000000000000000000000000 d12979c22fff69c59ca9409e7a8fe3ee25eaee80 A Z/NA +:100644 000000 a18393c636b98e9bd7296b8b437ea4992b72440c 0000000000000000000000000000000000000000 D Z/ND +:100644 100644 3fdbe17fd013303a2e981e1ca1c6cd6e72789087 7e09d6a3a14bd630913e8c75693cea32157b606d M Z/NM +EOF +cat >.test-plain-AB <<\EOF +:100644 100644 ccba72ad3888a3520b39efcf780b9ee64167535d 6aa2b5335b16431a0ef71e5c0a28be69183cf6a2 M AA +:100644 000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 0000000000000000000000000000000000000000 D AN +:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 A DF +:040000 000000 6d50f65d3bdab91c63444294d38f08aeff328e42 0000000000000000000000000000000000000000 D DF +:000000 100644 0000000000000000000000000000000000000000 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 A DM +:000000 100644 0000000000000000000000000000000000000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 A DN +:100644 000000 103d9f89b50b9aad03054b579be5e7aa665f2d57 0000000000000000000000000000000000000000 D MD +:100644 100644 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 19989d4559aae417fedee240ccf2ba315ea4dc2b M MM +:100644 100644 a716d58de4a570e0038f5c307bd8db34daea021f bd084b0c27c7b6cc34f11d6d0509a29be3caf970 M MN +:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 A NA +:100644 000000 a4e179e4291e5536a5e1c82e091052772d2c5a93 0000000000000000000000000000000000000000 D ND +:100644 100644 c8f25781e8f1792e3e40b74225e20553041b5226 cdb9a8c3da571502ac30225e9c17beccb8387983 M NM +:100644 100644 4c86f9a85fbc5e6804ee2e17a797538fbe785bca c4e4a12231b9fa79a0053cb6077fcb21bb5b135a M TT +:040000 040000 5e5f22072bb39f6e12cf663a57cb634c76eefb49 1ba523955d5160681af65cb776411f574c1e8155 M Z +EOF +cat >.test-recursive-AB <<\EOF +:100644 100644 ccba72ad3888a3520b39efcf780b9ee64167535d 6aa2b5335b16431a0ef71e5c0a28be69183cf6a2 M AA +:100644 000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 0000000000000000000000000000000000000000 D AN +:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 A DF +:100644 000000 68a6d8b91da11045cf4aa3a5ab9f2a781c701249 0000000000000000000000000000000000000000 D DF/DF +:000000 100644 0000000000000000000000000000000000000000 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 A DM +:000000 100644 0000000000000000000000000000000000000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 A DN +:100644 000000 103d9f89b50b9aad03054b579be5e7aa665f2d57 0000000000000000000000000000000000000000 D MD +:100644 100644 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 19989d4559aae417fedee240ccf2ba315ea4dc2b M MM +:100644 100644 a716d58de4a570e0038f5c307bd8db34daea021f bd084b0c27c7b6cc34f11d6d0509a29be3caf970 M MN +:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 A NA +:100644 000000 a4e179e4291e5536a5e1c82e091052772d2c5a93 0000000000000000000000000000000000000000 D ND +:100644 100644 c8f25781e8f1792e3e40b74225e20553041b5226 cdb9a8c3da571502ac30225e9c17beccb8387983 M NM +:100644 100644 4c86f9a85fbc5e6804ee2e17a797538fbe785bca c4e4a12231b9fa79a0053cb6077fcb21bb5b135a M TT +:100644 100644 8acb8e9750e3f644bf323fcf3d338849db106c77 6c0b99286d0bce551ac4a7b3dff8b706edff3715 M Z/AA +:100644 000000 087494262084cefee7ed484d20c8dc0580791272 0000000000000000000000000000000000000000 D Z/AN +:000000 100644 0000000000000000000000000000000000000000 d77371d15817fcaa57eeec27f770c505ba974ec1 A Z/DM +:000000 100644 0000000000000000000000000000000000000000 beb5d38c55283d280685ea21a0e50cfcc0ca064a A Z/DN +:100644 000000 a79ac3be9377639e1c7d1edf1ae1b3a5f0ccd8a9 0000000000000000000000000000000000000000 D Z/MD +:100644 100644 61422ba9c2c873416061a88cd40a59a35b576474 697aad7715a1e7306ca76290a3dd4208fbaeddfa M Z/MM +:100644 100644 a5c544c21cfcb07eb80a4d89a5b7d1570002edfd b16d7b25b869f2beb124efa53467d8a1550ad694 M Z/MN +:000000 100644 0000000000000000000000000000000000000000 d12979c22fff69c59ca9409e7a8fe3ee25eaee80 A Z/NA +:100644 000000 a18393c636b98e9bd7296b8b437ea4992b72440c 0000000000000000000000000000000000000000 D Z/ND +:100644 100644 3fdbe17fd013303a2e981e1ca1c6cd6e72789087 7e09d6a3a14bd630913e8c75693cea32157b606d M Z/NM +EOF + +x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +x40="$x40$x40$x40$x40$x40$x40$x40$x40" +z40='0000000000000000000000000000000000000000' +cmp_diff_files_output () { + # diff-files never reports additions. Also it does not fill in the + # object ID for the changed files because it wants you to look at the + # filesystem. + sed <"$2" >.test-tmp \ + -e '/^:000000 /d;s/'$x40'\( [MCRNDU][0-9]*\) /'$z40'\1 /' && + diff "$1" .test-tmp +} + +test_expect_success \ + 'diff-tree of known trees.' \ + 'git-diff-tree $tree_O $tree_A >.test-a && + cmp -s .test-a .test-plain-OA' + +test_expect_success \ + 'diff-tree of known trees.' \ + 'git-diff-tree -r $tree_O $tree_A >.test-a && + cmp -s .test-a .test-recursive-OA' + +test_expect_success \ + 'diff-tree of known trees.' \ + 'git-diff-tree $tree_O $tree_B >.test-a && + cmp -s .test-a .test-plain-OB' + +test_expect_success \ + 'diff-tree of known trees.' \ + 'git-diff-tree -r $tree_O $tree_B >.test-a && + cmp -s .test-a .test-recursive-OB' + +test_expect_success \ + 'diff-tree of known trees.' \ + 'git-diff-tree $tree_A $tree_B >.test-a && + cmp -s .test-a .test-plain-AB' + +test_expect_success \ + 'diff-tree of known trees.' \ + 'git-diff-tree -r $tree_A $tree_B >.test-a && + cmp -s .test-a .test-recursive-AB' + +test_expect_success \ + 'diff-cache O with A in cache' \ + 'git-read-tree $tree_A && + git-diff-index --cached $tree_O >.test-a && + cmp -s .test-a .test-recursive-OA' + +test_expect_success \ + 'diff-cache O with B in cache' \ + 'git-read-tree $tree_B && + git-diff-index --cached $tree_O >.test-a && + cmp -s .test-a .test-recursive-OB' + +test_expect_success \ + 'diff-cache A with B in cache' \ + 'git-read-tree $tree_B && + git-diff-index --cached $tree_A >.test-a && + cmp -s .test-a .test-recursive-AB' + +test_expect_success \ + 'diff-files with O in cache and A checked out' \ + 'rm -fr Z [A-Z][A-Z] && + git-read-tree $tree_A && + git-checkout-index -f -a && + git-read-tree --reset $tree_O || return 1 + git-update-index --refresh >/dev/null ;# this can exit non-zero + git-diff-files >.test-a && + cmp_diff_files_output .test-a .test-recursive-OA' + +test_expect_success \ + 'diff-files with O in cache and B checked out' \ + 'rm -fr Z [A-Z][A-Z] && + git-read-tree $tree_B && + git-checkout-index -f -a && + git-read-tree --reset $tree_O || return 1 + git-update-index --refresh >/dev/null ;# this can exit non-zero + git-diff-files >.test-a && + cmp_diff_files_output .test-a .test-recursive-OB' + +test_expect_success \ + 'diff-files with A in cache and B checked out' \ + 'rm -fr Z [A-Z][A-Z] && + git-read-tree $tree_B && + git-checkout-index -f -a && + git-read-tree --reset $tree_A || return 1 + git-update-index --refresh >/dev/null ;# this can exit non-zero + git-diff-files >.test-a && + cmp_diff_files_output .test-a .test-recursive-AB' + +################################################################ +# Now we have established the baseline, we do not have to +# rely on individual object ID values that much. + +test_expect_success \ + 'diff-tree O A == diff-tree -R A O' \ + 'git-diff-tree $tree_O $tree_A >.test-a && + git-diff-tree -R $tree_A $tree_O >.test-b && + cmp -s .test-a .test-b' + +test_expect_success \ + 'diff-tree -r O A == diff-tree -r -R A O' \ + 'git-diff-tree -r $tree_O $tree_A >.test-a && + git-diff-tree -r -R $tree_A $tree_O >.test-b && + cmp -s .test-a .test-b' + +test_expect_success \ + 'diff-tree B A == diff-tree -R A B' \ + 'git-diff-tree $tree_B $tree_A >.test-a && + git-diff-tree -R $tree_A $tree_B >.test-b && + cmp -s .test-a .test-b' + +test_expect_success \ + 'diff-tree -r B A == diff-tree -r -R A B' \ + 'git-diff-tree -r $tree_B $tree_A >.test-a && + git-diff-tree -r -R $tree_A $tree_B >.test-b && + cmp -s .test-a .test-b' + +test_done diff --git a/t/t4003-diff-rename-1.sh b/t/t4003-diff-rename-1.sh new file mode 100755 index 0000000000..27519704d4 --- /dev/null +++ b/t/t4003-diff-rename-1.sh @@ -0,0 +1,128 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='More rename detection + +' +. ./test-lib.sh +. ../diff-lib.sh ;# test-lib chdir's into trash + +test_expect_success \ + 'prepare reference tree' \ + 'cat ../../COPYING >COPYING && + echo frotz >rezrov && + git-update-index --add COPYING rezrov && + tree=$(git-write-tree) && + echo $tree' + +test_expect_success \ + 'prepare work tree' \ + 'sed -e 's/HOWEVER/However/' <COPYING >COPYING.1 && + sed -e 's/GPL/G.P.L/g' <COPYING >COPYING.2 && + rm -f COPYING && + git-update-index --add --remove COPYING COPYING.?' + +# tree has COPYING and rezrov. work tree has COPYING.1 and COPYING.2, +# both are slightly edited, and unchanged rezrov. So we say you +# copy-and-edit one, and rename-and-edit the other. We do not say +# anything about rezrov. + +GIT_DIFF_OPTS=--unified=0 git-diff-index -M -p $tree >current +cat >expected <<\EOF +diff --git a/COPYING b/COPYING.1 +copy from COPYING +copy to COPYING.1 +--- a/COPYING ++++ b/COPYING.1 +@@ -6 +6 @@ +- HOWEVER, in order to allow a migration to GPLv3 if that seems like ++ However, in order to allow a migration to GPLv3 if that seems like +diff --git a/COPYING b/COPYING.2 +rename from COPYING +rename to COPYING.2 +--- a/COPYING ++++ b/COPYING.2 +@@ -2 +2 @@ +- Note that the only valid version of the GPL as far as this project ++ Note that the only valid version of the G.P.L as far as this project +@@ -6 +6 @@ +- HOWEVER, in order to allow a migration to GPLv3 if that seems like ++ HOWEVER, in order to allow a migration to G.P.Lv3 if that seems like +@@ -12 +12 @@ +- This file is licensed under the GPL v2, or a later version ++ This file is licensed under the G.P.L v2, or a later version +EOF + +test_expect_success \ + 'validate output from rename/copy detection (#1)' \ + 'compare_diff_patch current expected' + +test_expect_success \ + 'prepare work tree again' \ + 'mv COPYING.2 COPYING && + git-update-index --add --remove COPYING COPYING.1 COPYING.2' + +# tree has COPYING and rezrov. work tree has COPYING and COPYING.1, +# both are slightly edited, and unchanged rezrov. So we say you +# edited one, and copy-and-edit the other. We do not say +# anything about rezrov. + +GIT_DIFF_OPTS=--unified=0 git-diff-index -C -p $tree >current +cat >expected <<\EOF +diff --git a/COPYING b/COPYING +--- a/COPYING ++++ b/COPYING +@@ -2 +2 @@ +- Note that the only valid version of the GPL as far as this project ++ Note that the only valid version of the G.P.L as far as this project +@@ -6 +6 @@ +- HOWEVER, in order to allow a migration to GPLv3 if that seems like ++ HOWEVER, in order to allow a migration to G.P.Lv3 if that seems like +@@ -12 +12 @@ +- This file is licensed under the GPL v2, or a later version ++ This file is licensed under the G.P.L v2, or a later version +diff --git a/COPYING b/COPYING.1 +copy from COPYING +copy to COPYING.1 +--- a/COPYING ++++ b/COPYING.1 +@@ -6 +6 @@ +- HOWEVER, in order to allow a migration to GPLv3 if that seems like ++ However, in order to allow a migration to GPLv3 if that seems like +EOF + +test_expect_success \ + 'validate output from rename/copy detection (#2)' \ + 'compare_diff_patch current expected' + +test_expect_success \ + 'prepare work tree once again' \ + 'cat ../../COPYING >COPYING && + git-update-index --add --remove COPYING COPYING.1' + +# tree has COPYING and rezrov. work tree has COPYING and COPYING.1, +# but COPYING is not edited. We say you copy-and-edit COPYING.1; this +# is only possible because -C mode now reports the unmodified file to +# the diff-core. Unchanged rezrov, although being fed to +# git-diff-index as well, should not be mentioned. + +GIT_DIFF_OPTS=--unified=0 \ + git-diff-index -C --find-copies-harder -p $tree >current +cat >expected <<\EOF +diff --git a/COPYING b/COPYING.1 +copy from COPYING +copy to COPYING.1 +--- a/COPYING ++++ b/COPYING.1 +@@ -6 +6 @@ +- HOWEVER, in order to allow a migration to GPLv3 if that seems like ++ However, in order to allow a migration to GPLv3 if that seems like +EOF + +test_expect_success \ + 'validate output from rename/copy detection (#3)' \ + 'compare_diff_patch current expected' + +test_done diff --git a/t/t4004-diff-rename-symlink.sh b/t/t4004-diff-rename-symlink.sh new file mode 100755 index 0000000000..a23aaa0a94 --- /dev/null +++ b/t/t4004-diff-rename-symlink.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='More rename detection tests. + +The rename detection logic should be able to detect pure rename or +copy of symbolic links, but should not produce rename/copy followed +by an edit for them. +' +. ./test-lib.sh +. ../diff-lib.sh + +test_expect_success \ + 'prepare reference tree' \ + 'echo xyzzy | tr -d '\\\\'012 >yomin && + ln -s xyzzy frotz && + git-update-index --add frotz yomin && + tree=$(git-write-tree) && + echo $tree' + +test_expect_success \ + 'prepare work tree' \ + 'mv frotz rezrov && + rm -f yomin && + ln -s xyzzy nitfol && + ln -s xzzzy bozbar && + git-update-index --add --remove frotz rezrov nitfol bozbar yomin' + +# tree has frotz pointing at xyzzy, and yomin that contains xyzzy to +# confuse things. work tree has rezrov (xyzzy) nitfol (xyzzy) and +# bozbar (xzzzy). +# rezrov and nitfol are rename/copy of frotz and bozbar should be +# a new creation. + +GIT_DIFF_OPTS=--unified=0 git-diff-index -M -p $tree >current +cat >expected <<\EOF +diff --git a/bozbar b/bozbar +new file mode 120000 +--- /dev/null ++++ b/bozbar +@@ -0,0 +1 @@ ++xzzzy +\ No newline at end of file +diff --git a/frotz b/nitfol +similarity index 100% +copy from frotz +copy to nitfol +diff --git a/frotz b/rezrov +similarity index 100% +rename from frotz +rename to rezrov +diff --git a/yomin b/yomin +deleted file mode 100644 +--- a/yomin ++++ /dev/null +@@ -1 +0,0 @@ +-xyzzy +\ No newline at end of file +EOF + +test_expect_success \ + 'validate diff output' \ + 'compare_diff_patch current expected' + +test_done diff --git a/t/t4005-diff-rename-2.sh b/t/t4005-diff-rename-2.sh new file mode 100755 index 0000000000..684fd23a41 --- /dev/null +++ b/t/t4005-diff-rename-2.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Same rename detection as t4003 but testing diff-raw. + +' +. ./test-lib.sh +. ../diff-lib.sh ;# test-lib chdir's into trash + +test_expect_success \ + 'prepare reference tree' \ + 'cat ../../COPYING >COPYING && + echo frotz >rezrov && + git-update-index --add COPYING rezrov && + tree=$(git-write-tree) && + echo $tree' + +test_expect_success \ + 'prepare work tree' \ + 'sed -e 's/HOWEVER/However/' <COPYING >COPYING.1 && + sed -e 's/GPL/G.P.L/g' <COPYING >COPYING.2 && + rm -f COPYING && + git-update-index --add --remove COPYING COPYING.?' + +# tree has COPYING and rezrov. work tree has COPYING.1 and COPYING.2, +# both are slightly edited, and unchanged rezrov. We say COPYING.1 +# and COPYING.2 are based on COPYING, and do not say anything about +# rezrov. + +git-diff-index -M $tree >current + +cat >expected <<\EOF +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234 COPYING COPYING.1 +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 06c67961bbaed34a127f76d261f4c0bf73eda471 R1234 COPYING COPYING.2 +EOF + +test_expect_success \ + 'validate output from rename/copy detection (#1)' \ + 'compare_diff_raw current expected' + +################################################################ + +test_expect_success \ + 'prepare work tree again' \ + 'mv COPYING.2 COPYING && + git-update-index --add --remove COPYING COPYING.1 COPYING.2' + +# tree has COPYING and rezrov. work tree has COPYING and COPYING.1, +# both are slightly edited, and unchanged rezrov. We say COPYING.1 +# is based on COPYING and COPYING is still there, and do not say anything +# about rezrov. + +git-diff-index -C $tree >current +cat >expected <<\EOF +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 06c67961bbaed34a127f76d261f4c0bf73eda471 M COPYING +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234 COPYING COPYING.1 +EOF + +test_expect_success \ + 'validate output from rename/copy detection (#2)' \ + 'compare_diff_raw current expected' + +################################################################ + +# tree has COPYING and rezrov. work tree has the same COPYING and +# copy-edited COPYING.1, and unchanged rezrov. We should not say +# anything about rezrov nor COPYING, since the revised again diff-raw +# nows how to say Copy. + +test_expect_success \ + 'prepare work tree once again' \ + 'cat ../../COPYING >COPYING && + git-update-index --add --remove COPYING COPYING.1' + +git-diff-index -C --find-copies-harder $tree >current +cat >expected <<\EOF +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234 COPYING COPYING.1 +EOF + +test_expect_success \ + 'validate output from rename/copy detection (#3)' \ + 'compare_diff_raw current expected' + +test_done diff --git a/t/t4006-diff-mode.sh b/t/t4006-diff-mode.sh new file mode 100755 index 0000000000..ca342f48a1 --- /dev/null +++ b/t/t4006-diff-mode.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Test mode change diffs. + +' +. ./test-lib.sh + +test_expect_success \ + 'setup' \ + 'echo frotz >rezrov && + git-update-index --add rezrov && + tree=`git-write-tree` && + echo $tree' + +if [ "$(git config --get core.filemode)" = false ] +then + say 'filemode disabled on the filesystem, using update-index --chmod=+x' + test_expect_success \ + 'git-update-index --chmod=+x' \ + 'git-update-index rezrov && + git-update-index --chmod=+x rezrov && + git-diff-index $tree >current' +else + test_expect_success \ + 'chmod' \ + 'chmod +x rezrov && + git-update-index rezrov && + git-diff-index $tree >current' +fi + +_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" +sed -e 's/\(:100644 100755\) \('"$_x40"'\) \2 /\1 X X /' <current >check +echo ":100644 100755 X X M rezrov" >expected + +test_expect_success \ + 'verify' \ + 'diff -u expected check' + +test_done + diff --git a/t/t4007-rename-3.sh b/t/t4007-rename-3.sh new file mode 100755 index 0000000000..bb6ba69258 --- /dev/null +++ b/t/t4007-rename-3.sh @@ -0,0 +1,90 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Rename interaction with pathspec. + +' +. ./test-lib.sh +. ../diff-lib.sh ;# test-lib chdir's into trash + +test_expect_success \ + 'prepare reference tree' \ + 'mkdir path0 path1 && + cp ../../COPYING path0/COPYING && + git-update-index --add path0/COPYING && + tree=$(git-write-tree) && + echo $tree' + +test_expect_success \ + 'prepare work tree' \ + 'cp path0/COPYING path1/COPYING && + git-update-index --add --remove path0/COPYING path1/COPYING' + +# In the tree, there is only path0/COPYING. In the cache, path0 and +# path1 both have COPYING and the latter is a copy of path0/COPYING. +# Comparing the full tree with cache should tell us so. + +git-diff-index -C --find-copies-harder $tree >current + +cat >expected <<\EOF +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 6ff87c4664981e4397625791c8ea3bbb5f2279a3 C100 path0/COPYING path1/COPYING +EOF + +test_expect_success \ + 'validate the result (#1)' \ + 'compare_diff_raw current expected' + +# In the tree, there is only path0/COPYING. In the cache, path0 and +# path1 both have COPYING and the latter is a copy of path0/COPYING. +# However when we say we care only about path1, we should just see +# path1/COPYING suddenly appearing from nowhere, not detected as +# a copy from path0/COPYING. + +git-diff-index -C $tree path1 >current + +cat >expected <<\EOF +:000000 100644 0000000000000000000000000000000000000000 6ff87c4664981e4397625791c8ea3bbb5f2279a3 A path1/COPYING +EOF + +test_expect_success \ + 'validate the result (#2)' \ + 'compare_diff_raw current expected' + +test_expect_success \ + 'tweak work tree' \ + 'rm -f path0/COPYING && + git-update-index --remove path0/COPYING' + +# In the tree, there is only path0/COPYING. In the cache, path0 does +# not have COPYING anymore and path1 has COPYING which is a copy of +# path0/COPYING. Showing the full tree with cache should tell us about +# the rename. + +git-diff-index -C $tree >current + +cat >expected <<\EOF +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 6ff87c4664981e4397625791c8ea3bbb5f2279a3 R100 path0/COPYING path1/COPYING +EOF + +test_expect_success \ + 'validate the result (#3)' \ + 'compare_diff_raw current expected' + +# In the tree, there is only path0/COPYING. In the cache, path0 does +# not have COPYING anymore and path1 has COPYING which is a copy of +# path0/COPYING. When we say we care only about path1, we should just +# see path1/COPYING appearing from nowhere. + +git-diff-index -C $tree path1 >current + +cat >expected <<\EOF +:000000 100644 0000000000000000000000000000000000000000 6ff87c4664981e4397625791c8ea3bbb5f2279a3 A path1/COPYING +EOF + +test_expect_success \ + 'validate the result (#4)' \ + 'compare_diff_raw current expected' + +test_done diff --git a/t/t4008-diff-break-rewrite.sh b/t/t4008-diff-break-rewrite.sh new file mode 100755 index 0000000000..263ac1ebf7 --- /dev/null +++ b/t/t4008-diff-break-rewrite.sh @@ -0,0 +1,188 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Break and then rename + +We have two very different files, file0 and file1, registered in a tree. + +We update file1 so drastically that it is more similar to file0, and +then remove file0. With -B, changes to file1 should be broken into +separate delete and create, resulting in removal of file0, removal of +original file1 and creation of completely rewritten file1. + +Further, with -B and -M together, these three modifications should +turn into rename-edit of file0 into file1. + +Starting from the same two files in the tree, we swap file0 and file1. +With -B, this should be detected as two complete rewrites, resulting in +four changes in total. + +Further, with -B and -M together, these should turn into two renames. +' +. ./test-lib.sh +. ../diff-lib.sh ;# test-lib chdir's into trash + +test_expect_success \ + setup \ + 'cat ../../README >file0 && + cat ../../COPYING >file1 && + git-update-index --add file0 file1 && + tree=$(git-write-tree) && + echo "$tree"' + +test_expect_success \ + 'change file1 with copy-edit of file0 and remove file0' \ + 'sed -e "s/git/GIT/" file0 >file1 && + rm -f file0 && + git-update-index --remove file0 file1' + +test_expect_success \ + 'run diff with -B' \ + 'git-diff-index -B --cached "$tree" >current' + +cat >expected <<\EOF +:100644 000000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 0000000000000000000000000000000000000000 D file0 +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 11e331465a89c394dc25c780de230043750c1ec8 M100 file1 +EOF + +test_expect_success \ + 'validate result of -B (#1)' \ + 'compare_diff_raw expected current' + +test_expect_success \ + 'run diff with -B and -M' \ + 'git-diff-index -B -M "$tree" >current' + +cat >expected <<\EOF +:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 08bb2fb671deff4c03a4d4a0a1315dff98d5732c R100 file0 file1 +EOF + +test_expect_success \ + 'validate result of -B -M (#2)' \ + 'compare_diff_raw expected current' + +test_expect_success \ + 'swap file0 and file1' \ + 'rm -f file0 file1 && + git-read-tree -m $tree && + git-checkout-index -f -u -a && + mv file0 tmp && + mv file1 file0 && + mv tmp file1 && + git-update-index file0 file1' + +test_expect_success \ + 'run diff with -B' \ + 'git-diff-index -B "$tree" >current' + +cat >expected <<\EOF +:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 6ff87c4664981e4397625791c8ea3bbb5f2279a3 M100 file0 +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 M100 file1 +EOF + +test_expect_success \ + 'validate result of -B (#3)' \ + 'compare_diff_raw expected current' + +test_expect_success \ + 'run diff with -B and -M' \ + 'git-diff-index -B -M "$tree" >current' + +cat >expected <<\EOF +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 6ff87c4664981e4397625791c8ea3bbb5f2279a3 R100 file1 file0 +:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 R100 file0 file1 +EOF + +test_expect_success \ + 'validate result of -B -M (#4)' \ + 'compare_diff_raw expected current' + +test_expect_success \ + 'make file0 into something completely different' \ + 'rm -f file0 && + ln -s frotz file0 && + git-update-index file0 file1' + +test_expect_success \ + 'run diff with -B' \ + 'git-diff-index -B "$tree" >current' + +cat >expected <<\EOF +:100644 120000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 67be421f88824578857624f7b3dc75e99a8a1481 T file0 +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 M100 file1 +EOF + +test_expect_success \ + 'validate result of -B (#5)' \ + 'compare_diff_raw expected current' + +test_expect_success \ + 'run diff with -B' \ + 'git-diff-index -B -M "$tree" >current' + +# This should not mistake file0 as the copy source of new file1 +# due to type differences. +cat >expected <<\EOF +:100644 120000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 67be421f88824578857624f7b3dc75e99a8a1481 T file0 +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 M100 file1 +EOF + +test_expect_success \ + 'validate result of -B -M (#6)' \ + 'compare_diff_raw expected current' + +test_expect_success \ + 'run diff with -M' \ + 'git-diff-index -M "$tree" >current' + +# This should not mistake file0 as the copy source of new file1 +# due to type differences. +cat >expected <<\EOF +:100644 120000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 67be421f88824578857624f7b3dc75e99a8a1481 T file0 +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 M file1 +EOF + +test_expect_success \ + 'validate result of -M (#7)' \ + 'compare_diff_raw expected current' + +test_expect_success \ + 'file1 edited to look like file0 and file0 rename-edited to file2' \ + 'rm -f file0 file1 && + git-read-tree -m $tree && + git-checkout-index -f -u -a && + sed -e "s/git/GIT/" file0 >file1 && + sed -e "s/git/GET/" file0 >file2 && + rm -f file0 + git-update-index --add --remove file0 file1 file2' + +test_expect_success \ + 'run diff with -B' \ + 'git-diff-index -B "$tree" >current' + +cat >expected <<\EOF +:100644 000000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 0000000000000000000000000000000000000000 D file0 +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 08bb2fb671deff4c03a4d4a0a1315dff98d5732c M100 file1 +:000000 100644 0000000000000000000000000000000000000000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 A file2 +EOF + +test_expect_success \ + 'validate result of -B (#8)' \ + 'compare_diff_raw expected current' + +test_expect_success \ + 'run diff with -B -M' \ + 'git-diff-index -B -M "$tree" >current' + +cat >expected <<\EOF +:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 08bb2fb671deff4c03a4d4a0a1315dff98d5732c C095 file0 file1 +:100644 100644 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 59f832e5c8b3f7e486be15ad0cd3e95ba9af8998 R095 file0 file2 +EOF + +test_expect_success \ + 'validate result of -B -M (#9)' \ + 'compare_diff_raw expected current' + +test_done diff --git a/t/t4009-diff-rename-4.sh b/t/t4009-diff-rename-4.sh new file mode 100755 index 0000000000..2f2f8b1216 --- /dev/null +++ b/t/t4009-diff-rename-4.sh @@ -0,0 +1,95 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Same rename detection as t4003 but testing diff-raw -z. + +' +. ./test-lib.sh +. ../diff-lib.sh ;# test-lib chdir's into trash + +test_expect_success \ + 'prepare reference tree' \ + 'cat ../../COPYING >COPYING && + echo frotz >rezrov && + git-update-index --add COPYING rezrov && + tree=$(git-write-tree) && + echo $tree' + +test_expect_success \ + 'prepare work tree' \ + 'sed -e 's/HOWEVER/However/' <COPYING >COPYING.1 && + sed -e 's/GPL/G.P.L/g' <COPYING >COPYING.2 && + rm -f COPYING && + git-update-index --add --remove COPYING COPYING.?' + +# tree has COPYING and rezrov. work tree has COPYING.1 and COPYING.2, +# both are slightly edited, and unchanged rezrov. We say COPYING.1 +# and COPYING.2 are based on COPYING, and do not say anything about +# rezrov. + +git-diff-index -z -M $tree >current + +cat >expected <<\EOF +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234 +COPYING +COPYING.1 +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 06c67961bbaed34a127f76d261f4c0bf73eda471 R1234 +COPYING +COPYING.2 +EOF + +test_expect_success \ + 'validate output from rename/copy detection (#1)' \ + 'compare_diff_raw_z current expected' + +################################################################ + +test_expect_success \ + 'prepare work tree again' \ + 'mv COPYING.2 COPYING && + git-update-index --add --remove COPYING COPYING.1 COPYING.2' + +# tree has COPYING and rezrov. work tree has COPYING and COPYING.1, +# both are slightly edited, and unchanged rezrov. We say COPYING.1 +# is based on COPYING and COPYING is still there, and do not say anything +# about rezrov. + +git-diff-index -z -C $tree >current +cat >expected <<\EOF +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 06c67961bbaed34a127f76d261f4c0bf73eda471 M +COPYING +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234 +COPYING +COPYING.1 +EOF + +test_expect_success \ + 'validate output from rename/copy detection (#2)' \ + 'compare_diff_raw_z current expected' + +################################################################ + +# tree has COPYING and rezrov. work tree has the same COPYING and +# copy-edited COPYING.1, and unchanged rezrov. We should not say +# anything about rezrov nor COPYING, since the revised again diff-raw +# nows how to say Copy. + +test_expect_success \ + 'prepare work tree once again' \ + 'cat ../../COPYING >COPYING && + git-update-index --add --remove COPYING COPYING.1' + +git-diff-index -z -C --find-copies-harder $tree >current +cat >expected <<\EOF +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0603b3238a076dc6c8022aedc6648fa523a17178 C1234 +COPYING +COPYING.1 +EOF + +test_expect_success \ + 'validate output from rename/copy detection (#3)' \ + 'compare_diff_raw_z current expected' + +test_done diff --git a/t/t4010-diff-pathspec.sh b/t/t4010-diff-pathspec.sh new file mode 100755 index 0000000000..9e1544df9d --- /dev/null +++ b/t/t4010-diff-pathspec.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Pathspec restrictions + +Prepare: + file0 + path1/file1 +' +. ./test-lib.sh +. ../diff-lib.sh ;# test-lib chdir's into trash + +test_expect_success \ + setup \ + 'echo frotz >file0 && + mkdir path1 && + echo rezrov >path1/file1 && + git-update-index --add file0 path1/file1 && + tree=`git-write-tree` && + echo "$tree" && + echo nitfol >file0 && + echo yomin >path1/file1 && + git-update-index file0 path1/file1' + +cat >expected <<\EOF +EOF +test_expect_success \ + 'limit to path should show nothing' \ + 'git-diff-index --cached $tree -- path >current && + compare_diff_raw current expected' + +cat >expected <<\EOF +:100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M path1/file1 +EOF +test_expect_success \ + 'limit to path1 should show path1/file1' \ + 'git-diff-index --cached $tree -- path1 >current && + compare_diff_raw current expected' + +cat >expected <<\EOF +:100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M path1/file1 +EOF +test_expect_success \ + 'limit to path1/ should show path1/file1' \ + 'git-diff-index --cached $tree -- path1/ >current && + compare_diff_raw current expected' + +cat >expected <<\EOF +:100644 100644 766498d93a4b06057a8e49d23f4068f1170ff38f 0a41e115ab61be0328a19b29f18cdcb49338d516 M file0 +EOF +test_expect_success \ + 'limit to file0 should show file0' \ + 'git-diff-index --cached $tree -- file0 >current && + compare_diff_raw current expected' + +cat >expected <<\EOF +EOF +test_expect_success \ + 'limit to file0/ should emit nothing.' \ + 'git-diff-index --cached $tree -- file0/ >current && + compare_diff_raw current expected' + +test_done diff --git a/t/t4011-diff-symlink.sh b/t/t4011-diff-symlink.sh new file mode 100755 index 0000000000..379a831f0b --- /dev/null +++ b/t/t4011-diff-symlink.sh @@ -0,0 +1,85 @@ +#!/bin/sh +# +# Copyright (c) 2005 Johannes Schindelin +# + +test_description='Test diff of symlinks. + +' +. ./test-lib.sh +. ../diff-lib.sh + +cat > expected << EOF +diff --git a/frotz b/frotz +new file mode 120000 +index 0000000..7c465af +--- /dev/null ++++ b/frotz +@@ -0,0 +1 @@ ++xyzzy +\ No newline at end of file +EOF + +test_expect_success \ + 'diff new symlink' \ + 'ln -s xyzzy frotz && + git-update-index && + tree=$(git-write-tree) && + git-update-index --add frotz && + GIT_DIFF_OPTS=--unified=0 git-diff-index -M -p $tree > current && + compare_diff_patch current expected' + +test_expect_success \ + 'diff unchanged symlink' \ + 'tree=$(git-write-tree) && + git-update-index frotz && + test -z "$(git-diff-index --name-only $tree)"' + +cat > expected << EOF +diff --git a/frotz b/frotz +deleted file mode 120000 +index 7c465af..0000000 +--- a/frotz ++++ /dev/null +@@ -1 +0,0 @@ +-xyzzy +\ No newline at end of file +EOF + +test_expect_success \ + 'diff removed symlink' \ + 'rm frotz && + git-diff-index -M -p $tree > current && + compare_diff_patch current expected' + +cat > expected << EOF +diff --git a/frotz b/frotz +EOF + +test_expect_success \ + 'diff identical, but newly created symlink' \ + 'sleep 3 && + ln -s xyzzy frotz && + git-diff-index -M -p $tree > current && + compare_diff_patch current expected' + +cat > expected << EOF +diff --git a/frotz b/frotz +index 7c465af..df1db54 120000 +--- a/frotz ++++ b/frotz +@@ -1 +1 @@ +-xyzzy +\ No newline at end of file ++yxyyz +\ No newline at end of file +EOF + +test_expect_success \ + 'diff different symlink' \ + 'rm frotz && + ln -s yxyyz frotz && + git-diff-index -M -p $tree > current && + compare_diff_patch current expected' + +test_done diff --git a/t/t4012-diff-binary.sh b/t/t4012-diff-binary.sh new file mode 100755 index 0000000000..323606c65c --- /dev/null +++ b/t/t4012-diff-binary.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='Binary diff and apply +' + +. ./test-lib.sh + +test_expect_success 'prepare repository' \ + 'echo AIT >a && echo BIT >b && echo CIT >c && echo DIT >d && + git-update-index --add a b c d && + echo git >a && + cat ../test4012.png >b && + echo git >c && + cat b b >d' + +cat > expected <<\EOF + a | 2 +- + b | Bin + c | 2 +- + d | Bin + 4 files changed, 2 insertions(+), 2 deletions(-) +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 expected' + +# apply needs to be able to skip the binary material correctly +# in order to report the line number of a corrupt patch. +test_expect_success 'apply detecting corrupt patch correctly' \ + 'git-diff | sed -e 's/-CIT/xCIT/' >broken && + if git-apply --stat --summary broken 2>detected + then + echo unhappy - should have detected an error + (exit 1) + else + echo happy + fi && + detected=`cat detected` && + detected=`expr "$detected" : "fatal.*at line \\([0-9]*\\)\$"` && + detected=`sed -ne "${detected}p" broken` && + test "$detected" = xCIT' + +test_expect_success 'apply detecting corrupt patch correctly' \ + 'git-diff --binary | sed -e 's/-CIT/xCIT/' >broken && + if git-apply --stat --summary broken 2>detected + then + echo unhappy - should have detected an error + (exit 1) + else + echo happy + fi && + detected=`cat detected` && + detected=`expr "$detected" : "fatal.*at line \\([0-9]*\\)\$"` && + detected=`sed -ne "${detected}p" broken` && + test "$detected" = xCIT' + +test_expect_success 'initial commit' 'git-commit -a -m initial' + +# Try removal (b), modification (d), and creation (e). +test_expect_success 'diff-index with --binary' \ + 'echo AIT >a && mv b e && echo CIT >c && cat e >d && + git-update-index --add --remove a b c d e && + tree0=`git-write-tree` && + git-diff --cached --binary >current && + git-apply --stat --summary current' + +test_expect_success 'apply binary patch' \ + 'git-reset --hard && + git-apply --binary --index <current && + tree1=`git-write-tree` && + test "$tree1" = "$tree0"' + +test_done diff --git a/t/t4013-diff-various.sh b/t/t4013-diff-various.sh new file mode 100755 index 0000000000..3d85ceaae9 --- /dev/null +++ b/t/t4013-diff-various.sh @@ -0,0 +1,253 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='Various diff formatting options' + +. ./test-lib.sh + +LF=' +' + +test_expect_success setup ' + + GIT_AUTHOR_DATE="2006-06-26 00:00:00 +0000" && + GIT_COMMITTER_DATE="2006-06-26 00:00:00 +0000" && + export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && + + mkdir dir && + for i in 1 2 3; do echo $i; done >file0 && + for i in A B; do echo $i; done >dir/sub && + cat file0 >file2 && + git add file0 file2 dir/sub && + git commit -m Initial && + + git branch initial && + git branch side && + + GIT_AUTHOR_DATE="2006-06-26 00:01:00 +0000" && + GIT_COMMITTER_DATE="2006-06-26 00:01:00 +0000" && + export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && + + for i in 4 5 6; do echo $i; done >>file0 && + for i in C D; do echo $i; done >>dir/sub && + rm -f file2 && + git update-index --remove file0 file2 dir/sub && + git commit -m "Second${LF}${LF}This is the second commit." && + + GIT_AUTHOR_DATE="2006-06-26 00:02:00 +0000" && + GIT_COMMITTER_DATE="2006-06-26 00:02:00 +0000" && + export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && + + for i in A B C; do echo $i; done >file1 && + git add file1 && + for i in E F; do echo $i; done >>dir/sub && + git update-index dir/sub && + git commit -m Third && + + GIT_AUTHOR_DATE="2006-06-26 00:03:00 +0000" && + GIT_COMMITTER_DATE="2006-06-26 00:03:00 +0000" && + export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && + + git checkout side && + for i in A B C; do echo $i; done >>file0 && + for i in 1 2; do echo $i; done >>dir/sub && + cat dir/sub >file3 && + git add file3 && + git update-index file0 dir/sub && + git commit -m Side && + + GIT_AUTHOR_DATE="2006-06-26 00:04:00 +0000" && + GIT_COMMITTER_DATE="2006-06-26 00:04:00 +0000" && + export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && + + git checkout master && + git pull -s ours . side && + + GIT_AUTHOR_DATE="2006-06-26 00:05:00 +0000" && + GIT_COMMITTER_DATE="2006-06-26 00:05:00 +0000" && + export GIT_AUTHOR_DATE GIT_COMMITTER_DATE && + + for i in A B C; do echo $i; done >>file0 && + for i in 1 2; do echo $i; done >>dir/sub && + git update-index file0 dir/sub && + + git config log.showroot false && + git commit --amend && + git show-branch +' + +: <<\EOF +! [initial] Initial + * [master] Merge branch 'side' + ! [side] Side +--- + - [master] Merge branch 'side' + *+ [side] Side + * [master^] Second ++*+ [initial] Initial +EOF + +V=`git version | sed -e 's/^git version //' -e 's/\./\\./g'` +while read cmd +do + case "$cmd" in + '' | '#'*) continue ;; + esac + test=`echo "$cmd" | sed -e 's|[/ ][/ ]*|_|g'` + cnt=`expr $test_count + 1` + pfx=`printf "%04d" $cnt` + expect="../t4013/diff.$test" + actual="$pfx-diff.$test" + + test_expect_success "git $cmd" ' + { + echo "\$ git $cmd" + git $cmd | + sed -e "s/^\\(-*\\)$V\\(-*\\)\$/\\1g-i-t--v-e-r-s-i-o-n\2/" \ + -e "s/^\\( *boundary=\"-*\\)$V\\(-*\\)\"\$/\\1g-i-t--v-e-r-s-i-o-n\2\"/" + echo "\$" + } >"$actual" && + if test -f "$expect" + then + diff -u "$expect" "$actual" && + rm -f "$actual" + else + # this is to help developing new tests. + cp "$actual" "$expect" + false + fi + ' +done <<\EOF +diff-tree initial +diff-tree -r initial +diff-tree -r --abbrev initial +diff-tree -r --abbrev=4 initial +diff-tree --root initial +diff-tree --root --abbrev initial +diff-tree --root -r initial +diff-tree --root -r --abbrev initial +diff-tree --root -r --abbrev=4 initial +diff-tree -p initial +diff-tree --root -p initial +diff-tree --patch-with-stat initial +diff-tree --root --patch-with-stat initial +diff-tree --patch-with-raw initial +diff-tree --root --patch-with-raw initial + +diff-tree --pretty initial +diff-tree --pretty --root initial +diff-tree --pretty -p initial +diff-tree --pretty --stat initial +diff-tree --pretty --summary initial +diff-tree --pretty --stat --summary initial +diff-tree --pretty --root -p initial +diff-tree --pretty --root --stat initial +# improved by Timo's patch +diff-tree --pretty --root --summary initial +# improved by Timo's patch +diff-tree --pretty --root --summary -r initial +diff-tree --pretty --root --stat --summary initial +diff-tree --pretty --patch-with-stat initial +diff-tree --pretty --root --patch-with-stat initial +diff-tree --pretty --patch-with-raw initial +diff-tree --pretty --root --patch-with-raw initial + +diff-tree --pretty=oneline initial +diff-tree --pretty=oneline --root initial +diff-tree --pretty=oneline -p initial +diff-tree --pretty=oneline --root -p initial +diff-tree --pretty=oneline --patch-with-stat initial +# improved by Timo's patch +diff-tree --pretty=oneline --root --patch-with-stat initial +diff-tree --pretty=oneline --patch-with-raw initial +diff-tree --pretty=oneline --root --patch-with-raw initial + +diff-tree --pretty side +diff-tree --pretty -p side +diff-tree --pretty --patch-with-stat side + +diff-tree master +diff-tree -p master +diff-tree -p -m master +diff-tree -c master +diff-tree -c --abbrev master +diff-tree --cc master +# stat only should show the diffstat with the first parent +diff-tree -c --stat master +diff-tree --cc --stat master +diff-tree -c --stat --summary master +diff-tree --cc --stat --summary master +# stat summary should show the diffstat and summary with the first parent +diff-tree -c --stat --summary side +diff-tree --cc --stat --summary side +# improved by Timo's patch +diff-tree --cc --patch-with-stat master +# improved by Timo's patch +diff-tree --cc --patch-with-stat --summary master +# this is correct +diff-tree --cc --patch-with-stat --summary side + +log master +log -p master +log --root master +log --root -p master +log --patch-with-stat master +log --root --patch-with-stat master +log --root --patch-with-stat --summary master +# improved by Timo's patch +log --root -c --patch-with-stat --summary master +# improved by Timo's patch +log --root --cc --patch-with-stat --summary master +log -SF master +log -SF -p master + +whatchanged master +whatchanged -p master +whatchanged --root master +whatchanged --root -p master +whatchanged --patch-with-stat master +whatchanged --root --patch-with-stat master +whatchanged --root --patch-with-stat --summary master +# improved by Timo's patch +whatchanged --root -c --patch-with-stat --summary master +# improved by Timo's patch +whatchanged --root --cc --patch-with-stat --summary master +whatchanged -SF master +whatchanged -SF -p master + +log --patch-with-stat master -- dir/ +whatchanged --patch-with-stat master -- dir/ +log --patch-with-stat --summary master -- dir/ +whatchanged --patch-with-stat --summary master -- dir/ + +show initial +show --root initial +show side +show master +show --stat side +show --stat --summary side +show --patch-with-stat side +show --patch-with-raw side +show --patch-with-stat --summary side + +format-patch --stdout initial..side +format-patch --stdout initial..master^ +format-patch --stdout initial..master +format-patch --attach --stdout initial..side +format-patch --attach --stdout initial..master^ +format-patch --attach --stdout initial..master + +diff --abbrev initial..side +diff -r initial..side +diff --stat initial..side +diff -r --stat initial..side +diff initial..side +diff --patch-with-stat initial..side +diff --patch-with-raw initial..side +diff --patch-with-stat -r initial..side +diff --patch-with-raw -r initial..side +EOF + +test_done diff --git a/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_master b/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_master new file mode 100644 index 0000000000..3a9f78a09d --- /dev/null +++ b/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_master @@ -0,0 +1,34 @@ +$ git diff-tree --cc --patch-with-stat --summary master +59d314ad6f356dd08601a4cd5e530381da3e3c64 + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --cc dir/sub +index cead32e,7289e35..992913c +--- a/dir/sub ++++ b/dir/sub +@@@ -1,6 -1,4 +1,8 @@@ + A + B + +C + +D + +E + +F ++ 1 ++ 2 +diff --cc file0 +index b414108,f4615da..10a8a9f +--- a/file0 ++++ b/file0 +@@@ -1,6 -1,6 +1,9 @@@ + 1 + 2 + 3 + +4 + +5 + +6 ++ A ++ B ++ C +$ diff --git a/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_side b/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_side new file mode 100644 index 0000000000..a61ad8cb13 --- /dev/null +++ b/t/t4013/diff.diff-tree_--cc_--patch-with-stat_--summary_side @@ -0,0 +1,39 @@ +$ git diff-tree --cc --patch-with-stat --summary side +c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.diff-tree_--cc_--patch-with-stat_master b/t/t4013/diff.diff-tree_--cc_--patch-with-stat_master new file mode 100644 index 0000000000..49f23b9215 --- /dev/null +++ b/t/t4013/diff.diff-tree_--cc_--patch-with-stat_master @@ -0,0 +1,34 @@ +$ git diff-tree --cc --patch-with-stat master +59d314ad6f356dd08601a4cd5e530381da3e3c64 + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --cc dir/sub +index cead32e,7289e35..992913c +--- a/dir/sub ++++ b/dir/sub +@@@ -1,6 -1,4 +1,8 @@@ + A + B + +C + +D + +E + +F ++ 1 ++ 2 +diff --cc file0 +index b414108,f4615da..10a8a9f +--- a/file0 ++++ b/file0 +@@@ -1,6 -1,6 +1,9 @@@ + 1 + 2 + 3 + +4 + +5 + +6 ++ A ++ B ++ C +$ diff --git a/t/t4013/diff.diff-tree_--cc_--stat_--summary_master b/t/t4013/diff.diff-tree_--cc_--stat_--summary_master new file mode 100644 index 0000000000..cc6eb3b3d5 --- /dev/null +++ b/t/t4013/diff.diff-tree_--cc_--stat_--summary_master @@ -0,0 +1,6 @@ +$ git diff-tree --cc --stat --summary master +59d314ad6f356dd08601a4cd5e530381da3e3c64 + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) +$ diff --git a/t/t4013/diff.diff-tree_--cc_--stat_--summary_side b/t/t4013/diff.diff-tree_--cc_--stat_--summary_side new file mode 100644 index 0000000000..50362be7bf --- /dev/null +++ b/t/t4013/diff.diff-tree_--cc_--stat_--summary_side @@ -0,0 +1,8 @@ +$ git diff-tree --cc --stat --summary side +c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 +$ diff --git a/t/t4013/diff.diff-tree_--cc_--stat_master b/t/t4013/diff.diff-tree_--cc_--stat_master new file mode 100644 index 0000000000..fae7f33255 --- /dev/null +++ b/t/t4013/diff.diff-tree_--cc_--stat_master @@ -0,0 +1,6 @@ +$ git diff-tree --cc --stat master +59d314ad6f356dd08601a4cd5e530381da3e3c64 + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) +$ diff --git a/t/t4013/diff.diff-tree_--cc_master b/t/t4013/diff.diff-tree_--cc_master new file mode 100644 index 0000000000..5ecb4e14ae --- /dev/null +++ b/t/t4013/diff.diff-tree_--cc_master @@ -0,0 +1,30 @@ +$ git diff-tree --cc master +59d314ad6f356dd08601a4cd5e530381da3e3c64 +diff --cc dir/sub +index cead32e,7289e35..992913c +--- a/dir/sub ++++ b/dir/sub +@@@ -1,6 -1,4 +1,8 @@@ + A + B + +C + +D + +E + +F ++ 1 ++ 2 +diff --cc file0 +index b414108,f4615da..10a8a9f +--- a/file0 ++++ b/file0 +@@@ -1,6 -1,6 +1,9 @@@ + 1 + 2 + 3 + +4 + +5 + +6 ++ A ++ B ++ C +$ diff --git a/t/t4013/diff.diff-tree_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--patch-with-raw_initial new file mode 100644 index 0000000000..fc177ab3f2 --- /dev/null +++ b/t/t4013/diff.diff-tree_--patch-with-raw_initial @@ -0,0 +1,2 @@ +$ git diff-tree --patch-with-raw initial +$ diff --git a/t/t4013/diff.diff-tree_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--patch-with-stat_initial new file mode 100644 index 0000000000..bd905b1c57 --- /dev/null +++ b/t/t4013/diff.diff-tree_--patch-with-stat_initial @@ -0,0 +1,2 @@ +$ git diff-tree --patch-with-stat initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-raw_initial new file mode 100644 index 0000000000..7bb8b45e3e --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-raw_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty=oneline --patch-with-raw initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-stat_initial new file mode 100644 index 0000000000..cbdde4f400 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty=oneline_--patch-with-stat_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty=oneline --patch-with-stat initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-raw_initial new file mode 100644 index 0000000000..cd79f1a0ff --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-raw_initial @@ -0,0 +1,33 @@ +$ git diff-tree --pretty=oneline --root --patch-with-raw initial +444ac553ac7612cc88969031b02b3767fb8a353a Initial +:000000 100644 0000000000000000000000000000000000000000 35d242ba79ae89ac695e26b3d4c27a8e6f028f9e A dir/sub +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file0 +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file2 + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-stat_initial new file mode 100644 index 0000000000..d5c333a378 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty=oneline_--root_--patch-with-stat_initial @@ -0,0 +1,34 @@ +$ git diff-tree --pretty=oneline --root --patch-with-stat initial +444ac553ac7612cc88969031b02b3767fb8a353a Initial + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--root_-p_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--root_-p_initial new file mode 100644 index 0000000000..3c5092c699 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty=oneline_--root_-p_initial @@ -0,0 +1,29 @@ +$ git diff-tree --pretty=oneline --root -p initial +444ac553ac7612cc88969031b02b3767fb8a353a Initial +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_--root_initial b/t/t4013/diff.diff-tree_--pretty=oneline_--root_initial new file mode 100644 index 0000000000..08920ac658 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty=oneline_--root_initial @@ -0,0 +1,6 @@ +$ git diff-tree --pretty=oneline --root initial +444ac553ac7612cc88969031b02b3767fb8a353a Initial +:000000 040000 0000000000000000000000000000000000000000 da7a33fa77d8066d6698643940ce5860fe2d7fb3 A dir +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file0 +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file2 +$ diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_-p_initial b/t/t4013/diff.diff-tree_--pretty=oneline_-p_initial new file mode 100644 index 0000000000..94b76bfef1 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty=oneline_-p_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty=oneline -p initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty=oneline_initial b/t/t4013/diff.diff-tree_--pretty=oneline_initial new file mode 100644 index 0000000000..d50970d574 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty=oneline_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty=oneline initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--pretty_--patch-with-raw_initial new file mode 100644 index 0000000000..3a85316d8a --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--patch-with-raw_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty --patch-with-raw initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_initial new file mode 100644 index 0000000000..2e08239a46 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty --patch-with-stat initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_side b/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_side new file mode 100644 index 0000000000..4d30e7eddc --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--patch-with-stat_side @@ -0,0 +1,43 @@ +$ git diff-tree --pretty --patch-with-stat side +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-raw_initial new file mode 100644 index 0000000000..a3203bd19b --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-raw_initial @@ -0,0 +1,38 @@ +$ git diff-tree --pretty --root --patch-with-raw initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + +:000000 100644 0000000000000000000000000000000000000000 35d242ba79ae89ac695e26b3d4c27a8e6f028f9e A dir/sub +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file0 +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file2 + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-stat_initial new file mode 100644 index 0000000000..7dfa6af3c9 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--root_--patch-with-stat_initial @@ -0,0 +1,39 @@ +$ git diff-tree --pretty --root --patch-with-stat initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--stat_--summary_initial b/t/t4013/diff.diff-tree_--pretty_--root_--stat_--summary_initial new file mode 100644 index 0000000000..43bfce253e --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--root_--stat_--summary_initial @@ -0,0 +1,15 @@ +$ git diff-tree --pretty --root --stat --summary initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + create mode 100644 dir/sub + create mode 100644 file0 + create mode 100644 file2 +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--stat_initial b/t/t4013/diff.diff-tree_--pretty_--root_--stat_initial new file mode 100644 index 0000000000..9154aa4d47 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--root_--stat_initial @@ -0,0 +1,12 @@ +$ git diff-tree --pretty --root --stat initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--summary_-r_initial b/t/t4013/diff.diff-tree_--pretty_--root_--summary_-r_initial new file mode 100644 index 0000000000..ccdaafb377 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--root_--summary_-r_initial @@ -0,0 +1,11 @@ +$ git diff-tree --pretty --root --summary -r initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + + create mode 100644 dir/sub + create mode 100644 file0 + create mode 100644 file2 +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--root_--summary_initial b/t/t4013/diff.diff-tree_--pretty_--root_--summary_initial new file mode 100644 index 0000000000..58e5f74aea --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--root_--summary_initial @@ -0,0 +1,11 @@ +$ git diff-tree --pretty --root --summary initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + + create mode 100644 dir/sub + create mode 100644 file0 + create mode 100644 file2 +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--root_-p_initial b/t/t4013/diff.diff-tree_--pretty_--root_-p_initial new file mode 100644 index 0000000000..d0411f64ec --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--root_-p_initial @@ -0,0 +1,34 @@ +$ git diff-tree --pretty --root -p initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--root_initial b/t/t4013/diff.diff-tree_--pretty_--root_initial new file mode 100644 index 0000000000..94e32eabb1 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--root_initial @@ -0,0 +1,11 @@ +$ git diff-tree --pretty --root initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + +:000000 040000 0000000000000000000000000000000000000000 da7a33fa77d8066d6698643940ce5860fe2d7fb3 A dir +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file0 +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file2 +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--stat_--summary_initial b/t/t4013/diff.diff-tree_--pretty_--stat_--summary_initial new file mode 100644 index 0000000000..c22983ac4a --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--stat_--summary_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty --stat --summary initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--stat_initial b/t/t4013/diff.diff-tree_--pretty_--stat_initial new file mode 100644 index 0000000000..8fdcfb4c0a --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--stat_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty --stat initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty_--summary_initial b/t/t4013/diff.diff-tree_--pretty_--summary_initial new file mode 100644 index 0000000000..9bc2c4fbad --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_--summary_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty --summary initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty_-p_initial b/t/t4013/diff.diff-tree_--pretty_-p_initial new file mode 100644 index 0000000000..3c9942faf4 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_-p_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty -p initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty_-p_side b/t/t4013/diff.diff-tree_--pretty_-p_side new file mode 100644 index 0000000000..b993aa7b89 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_-p_side @@ -0,0 +1,38 @@ +$ git diff-tree --pretty -p side +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.diff-tree_--pretty_initial b/t/t4013/diff.diff-tree_--pretty_initial new file mode 100644 index 0000000000..14715bf7d0 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_initial @@ -0,0 +1,2 @@ +$ git diff-tree --pretty initial +$ diff --git a/t/t4013/diff.diff-tree_--pretty_side b/t/t4013/diff.diff-tree_--pretty_side new file mode 100644 index 0000000000..e9b6e1c102 --- /dev/null +++ b/t/t4013/diff.diff-tree_--pretty_side @@ -0,0 +1,11 @@ +$ git diff-tree --pretty side +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +:040000 040000 da7a33fa77d8066d6698643940ce5860fe2d7fb3 f977ed46ae6873c1c30ab878e15a4accedc3618b M dir +:100644 100644 01e79c32a8c99c557f0757da7cb6d65b3414466d f4615da674c09df322d6ba8d6b21ecfb1b1ba510 M file0 +:000000 100644 0000000000000000000000000000000000000000 7289e35bff32727c08dda207511bec138fdb9ea5 A file3 +$ diff --git a/t/t4013/diff.diff-tree_--root_--abbrev_initial b/t/t4013/diff.diff-tree_--root_--abbrev_initial new file mode 100644 index 0000000000..5aa84b2a86 --- /dev/null +++ b/t/t4013/diff.diff-tree_--root_--abbrev_initial @@ -0,0 +1,6 @@ +$ git diff-tree --root --abbrev initial +444ac553ac7612cc88969031b02b3767fb8a353a +:000000 040000 0000000... da7a33f... A dir +:000000 100644 0000000... 01e79c3... A file0 +:000000 100644 0000000... 01e79c3... A file2 +$ diff --git a/t/t4013/diff.diff-tree_--root_--patch-with-raw_initial b/t/t4013/diff.diff-tree_--root_--patch-with-raw_initial new file mode 100644 index 0000000000..d295e475dd --- /dev/null +++ b/t/t4013/diff.diff-tree_--root_--patch-with-raw_initial @@ -0,0 +1,33 @@ +$ git diff-tree --root --patch-with-raw initial +444ac553ac7612cc88969031b02b3767fb8a353a +:000000 100644 0000000000000000000000000000000000000000 35d242ba79ae89ac695e26b3d4c27a8e6f028f9e A dir/sub +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file0 +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file2 + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.diff-tree_--root_--patch-with-stat_initial b/t/t4013/diff.diff-tree_--root_--patch-with-stat_initial new file mode 100644 index 0000000000..1562b62708 --- /dev/null +++ b/t/t4013/diff.diff-tree_--root_--patch-with-stat_initial @@ -0,0 +1,34 @@ +$ git diff-tree --root --patch-with-stat initial +444ac553ac7612cc88969031b02b3767fb8a353a + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.diff-tree_--root_-p_initial b/t/t4013/diff.diff-tree_--root_-p_initial new file mode 100644 index 0000000000..3219c72fcb --- /dev/null +++ b/t/t4013/diff.diff-tree_--root_-p_initial @@ -0,0 +1,29 @@ +$ git diff-tree --root -p initial +444ac553ac7612cc88969031b02b3767fb8a353a +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.diff-tree_--root_-r_--abbrev=4_initial b/t/t4013/diff.diff-tree_--root_-r_--abbrev=4_initial new file mode 100644 index 0000000000..0c5361688c --- /dev/null +++ b/t/t4013/diff.diff-tree_--root_-r_--abbrev=4_initial @@ -0,0 +1,6 @@ +$ git diff-tree --root -r --abbrev=4 initial +444ac553ac7612cc88969031b02b3767fb8a353a +:000000 100644 0000... 35d2... A dir/sub +:000000 100644 0000... 01e7... A file0 +:000000 100644 0000... 01e7... A file2 +$ diff --git a/t/t4013/diff.diff-tree_--root_-r_--abbrev_initial b/t/t4013/diff.diff-tree_--root_-r_--abbrev_initial new file mode 100644 index 0000000000..c7b460faf6 --- /dev/null +++ b/t/t4013/diff.diff-tree_--root_-r_--abbrev_initial @@ -0,0 +1,6 @@ +$ git diff-tree --root -r --abbrev initial +444ac553ac7612cc88969031b02b3767fb8a353a +:000000 100644 0000000... 35d242b... A dir/sub +:000000 100644 0000000... 01e79c3... A file0 +:000000 100644 0000000... 01e79c3... A file2 +$ diff --git a/t/t4013/diff.diff-tree_--root_-r_initial b/t/t4013/diff.diff-tree_--root_-r_initial new file mode 100644 index 0000000000..eed435e175 --- /dev/null +++ b/t/t4013/diff.diff-tree_--root_-r_initial @@ -0,0 +1,6 @@ +$ git diff-tree --root -r initial +444ac553ac7612cc88969031b02b3767fb8a353a +:000000 100644 0000000000000000000000000000000000000000 35d242ba79ae89ac695e26b3d4c27a8e6f028f9e A dir/sub +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file0 +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file2 +$ diff --git a/t/t4013/diff.diff-tree_--root_initial b/t/t4013/diff.diff-tree_--root_initial new file mode 100644 index 0000000000..ddf6b068ab --- /dev/null +++ b/t/t4013/diff.diff-tree_--root_initial @@ -0,0 +1,6 @@ +$ git diff-tree --root initial +444ac553ac7612cc88969031b02b3767fb8a353a +:000000 040000 0000000000000000000000000000000000000000 da7a33fa77d8066d6698643940ce5860fe2d7fb3 A dir +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file0 +:000000 100644 0000000000000000000000000000000000000000 01e79c32a8c99c557f0757da7cb6d65b3414466d A file2 +$ diff --git a/t/t4013/diff.diff-tree_-c_--abbrev_master b/t/t4013/diff.diff-tree_-c_--abbrev_master new file mode 100644 index 0000000000..b8e4aa2530 --- /dev/null +++ b/t/t4013/diff.diff-tree_-c_--abbrev_master @@ -0,0 +1,5 @@ +$ git diff-tree -c --abbrev master +59d314ad6f356dd08601a4cd5e530381da3e3c64 +::100644 100644 100644 cead32e... 7289e35... 992913c... MM dir/sub +::100644 100644 100644 b414108... f4615da... 10a8a9f... MM file0 +$ diff --git a/t/t4013/diff.diff-tree_-c_--stat_--summary_master b/t/t4013/diff.diff-tree_-c_--stat_--summary_master new file mode 100644 index 0000000000..ac9f641fb4 --- /dev/null +++ b/t/t4013/diff.diff-tree_-c_--stat_--summary_master @@ -0,0 +1,6 @@ +$ git diff-tree -c --stat --summary master +59d314ad6f356dd08601a4cd5e530381da3e3c64 + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) +$ diff --git a/t/t4013/diff.diff-tree_-c_--stat_--summary_side b/t/t4013/diff.diff-tree_-c_--stat_--summary_side new file mode 100644 index 0000000000..2afcca11f4 --- /dev/null +++ b/t/t4013/diff.diff-tree_-c_--stat_--summary_side @@ -0,0 +1,8 @@ +$ git diff-tree -c --stat --summary side +c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 +$ diff --git a/t/t4013/diff.diff-tree_-c_--stat_master b/t/t4013/diff.diff-tree_-c_--stat_master new file mode 100644 index 0000000000..c2fe6a98c5 --- /dev/null +++ b/t/t4013/diff.diff-tree_-c_--stat_master @@ -0,0 +1,6 @@ +$ git diff-tree -c --stat master +59d314ad6f356dd08601a4cd5e530381da3e3c64 + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) +$ diff --git a/t/t4013/diff.diff-tree_-c_master b/t/t4013/diff.diff-tree_-c_master new file mode 100644 index 0000000000..e2d2bb2611 --- /dev/null +++ b/t/t4013/diff.diff-tree_-c_master @@ -0,0 +1,5 @@ +$ git diff-tree -c master +59d314ad6f356dd08601a4cd5e530381da3e3c64 +::100644 100644 100644 cead32e925b1420c84c14cbf7cf755e7e45af8ad 7289e35bff32727c08dda207511bec138fdb9ea5 992913c5aa0a5476d10c49ed0f21fc0c6d1aedf3 MM dir/sub +::100644 100644 100644 b414108e81e5091fe0974a1858b4d0d22b107f70 f4615da674c09df322d6ba8d6b21ecfb1b1ba510 10a8a9f3657f91a156b9f0184ed79a20adef9f7f MM file0 +$ diff --git a/t/t4013/diff.diff-tree_-p_-m_master b/t/t4013/diff.diff-tree_-p_-m_master new file mode 100644 index 0000000000..b60bea039d --- /dev/null +++ b/t/t4013/diff.diff-tree_-p_-m_master @@ -0,0 +1,80 @@ +$ git diff-tree -p -m master +59d314ad6f356dd08601a4cd5e530381da3e3c64 +diff --git a/dir/sub b/dir/sub +index cead32e..992913c 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -4,3 +4,5 @@ C + D + E + F ++1 ++2 +diff --git a/file0 b/file0 +index b414108..10a8a9f 100644 +--- a/file0 ++++ b/file0 +@@ -4,3 +4,6 @@ + 4 + 5 + 6 ++A ++B ++C +59d314ad6f356dd08601a4cd5e530381da3e3c64 +diff --git a/dir/sub b/dir/sub +index 7289e35..992913c 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,4 +1,8 @@ + A + B ++C ++D ++E ++F + 1 + 2 +diff --git a/file0 b/file0 +index f4615da..10a8a9f 100644 +--- a/file0 ++++ b/file0 +@@ -1,6 +1,9 @@ + 1 + 2 + 3 ++4 ++5 ++6 + A + B + C +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 +diff --git a/file3 b/file3 +deleted file mode 100644 +index 7289e35..0000000 +--- a/file3 ++++ /dev/null +@@ -1,4 +0,0 @@ +-A +-B +-1 +-2 +$ diff --git a/t/t4013/diff.diff-tree_-p_initial b/t/t4013/diff.diff-tree_-p_initial new file mode 100644 index 0000000000..e20ce88370 --- /dev/null +++ b/t/t4013/diff.diff-tree_-p_initial @@ -0,0 +1,2 @@ +$ git diff-tree -p initial +$ diff --git a/t/t4013/diff.diff-tree_-p_master b/t/t4013/diff.diff-tree_-p_master new file mode 100644 index 0000000000..b182875fb2 --- /dev/null +++ b/t/t4013/diff.diff-tree_-p_master @@ -0,0 +1,2 @@ +$ git diff-tree -p master +$ diff --git a/t/t4013/diff.diff-tree_-r_--abbrev=4_initial b/t/t4013/diff.diff-tree_-r_--abbrev=4_initial new file mode 100644 index 0000000000..c5a3aa5aa4 --- /dev/null +++ b/t/t4013/diff.diff-tree_-r_--abbrev=4_initial @@ -0,0 +1,2 @@ +$ git diff-tree -r --abbrev=4 initial +$ diff --git a/t/t4013/diff.diff-tree_-r_--abbrev_initial b/t/t4013/diff.diff-tree_-r_--abbrev_initial new file mode 100644 index 0000000000..0b689b773c --- /dev/null +++ b/t/t4013/diff.diff-tree_-r_--abbrev_initial @@ -0,0 +1,2 @@ +$ git diff-tree -r --abbrev initial +$ diff --git a/t/t4013/diff.diff-tree_-r_initial b/t/t4013/diff.diff-tree_-r_initial new file mode 100644 index 0000000000..1765d83ce4 --- /dev/null +++ b/t/t4013/diff.diff-tree_-r_initial @@ -0,0 +1,2 @@ +$ git diff-tree -r initial +$ diff --git a/t/t4013/diff.diff-tree_initial b/t/t4013/diff.diff-tree_initial new file mode 100644 index 0000000000..b49fc53457 --- /dev/null +++ b/t/t4013/diff.diff-tree_initial @@ -0,0 +1,2 @@ +$ git diff-tree initial +$ diff --git a/t/t4013/diff.diff-tree_master b/t/t4013/diff.diff-tree_master new file mode 100644 index 0000000000..fe9226f8a1 --- /dev/null +++ b/t/t4013/diff.diff-tree_master @@ -0,0 +1,2 @@ +$ git diff-tree master +$ diff --git a/t/t4013/diff.diff_--abbrev_initial..side b/t/t4013/diff.diff_--abbrev_initial..side new file mode 100644 index 0000000000..a88e66f817 --- /dev/null +++ b/t/t4013/diff.diff_--abbrev_initial..side @@ -0,0 +1,32 @@ +$ git diff --abbrev initial..side +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.diff_--patch-with-raw_-r_initial..side b/t/t4013/diff.diff_--patch-with-raw_-r_initial..side new file mode 100644 index 0000000000..3590dc79a6 --- /dev/null +++ b/t/t4013/diff.diff_--patch-with-raw_-r_initial..side @@ -0,0 +1,36 @@ +$ git diff --patch-with-raw -r initial..side +:100644 100644 35d242b... 7289e35... M dir/sub +:100644 100644 01e79c3... f4615da... M file0 +:000000 100644 0000000... 7289e35... A file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.diff_--patch-with-raw_initial..side b/t/t4013/diff.diff_--patch-with-raw_initial..side new file mode 100644 index 0000000000..b21d5dc6f3 --- /dev/null +++ b/t/t4013/diff.diff_--patch-with-raw_initial..side @@ -0,0 +1,36 @@ +$ git diff --patch-with-raw initial..side +:100644 100644 35d242b... 7289e35... M dir/sub +:100644 100644 01e79c3... f4615da... M file0 +:000000 100644 0000000... 7289e35... A file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.diff_--patch-with-stat_-r_initial..side b/t/t4013/diff.diff_--patch-with-stat_-r_initial..side new file mode 100644 index 0000000000..9ed317a198 --- /dev/null +++ b/t/t4013/diff.diff_--patch-with-stat_-r_initial..side @@ -0,0 +1,37 @@ +$ git diff --patch-with-stat -r initial..side + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.diff_--patch-with-stat_initial..side b/t/t4013/diff.diff_--patch-with-stat_initial..side new file mode 100644 index 0000000000..8b50629e66 --- /dev/null +++ b/t/t4013/diff.diff_--patch-with-stat_initial..side @@ -0,0 +1,37 @@ +$ git diff --patch-with-stat initial..side + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.diff_--stat_initial..side b/t/t4013/diff.diff_--stat_initial..side new file mode 100644 index 0000000000..0517b5d631 --- /dev/null +++ b/t/t4013/diff.diff_--stat_initial..side @@ -0,0 +1,6 @@ +$ git diff --stat initial..side + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) +$ diff --git a/t/t4013/diff.diff_-r_--stat_initial..side b/t/t4013/diff.diff_-r_--stat_initial..side new file mode 100644 index 0000000000..245220d3f9 --- /dev/null +++ b/t/t4013/diff.diff_-r_--stat_initial..side @@ -0,0 +1,6 @@ +$ git diff -r --stat initial..side + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) +$ diff --git a/t/t4013/diff.diff_-r_initial..side b/t/t4013/diff.diff_-r_initial..side new file mode 100644 index 0000000000..5bb2fe2f28 --- /dev/null +++ b/t/t4013/diff.diff_-r_initial..side @@ -0,0 +1,32 @@ +$ git diff -r initial..side +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.diff_initial..side b/t/t4013/diff.diff_initial..side new file mode 100644 index 0000000000..c8adaf5958 --- /dev/null +++ b/t/t4013/diff.diff_initial..side @@ -0,0 +1,32 @@ +$ git diff initial..side +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.format-patch_--attach_--stdout_initial..master b/t/t4013/diff.format-patch_--attach_--stdout_initial..master new file mode 100644 index 0000000000..e5ddd6fcbb --- /dev/null +++ b/t/t4013/diff.format-patch_--attach_--stdout_initial..master @@ -0,0 +1,173 @@ +$ git format-patch --attach --stdout initial..master +From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:01:00 +0000 +Subject: [PATCH] Second +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------g-i-t--v-e-r-s-i-o-n" + +This is a multi-part message in MIME format. +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/plain; charset=UTF-8; format=fixed +Content-Transfer-Encoding: 8bit + + +This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/x-patch; + name="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff" +Content-Transfer-Encoding: 8bit +Content-Disposition: inline; + filename="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff" + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +--------------g-i-t--v-e-r-s-i-o-n-- + + + +From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:02:00 +0000 +Subject: [PATCH] Third +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------g-i-t--v-e-r-s-i-o-n" + +This is a multi-part message in MIME format. +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/plain; charset=UTF-8; format=fixed +Content-Transfer-Encoding: 8bit + +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/x-patch; + name="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff" +Content-Transfer-Encoding: 8bit +Content-Disposition: inline; + filename="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff" + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +--------------g-i-t--v-e-r-s-i-o-n-- + + + +From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:03:00 +0000 +Subject: [PATCH] Side +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------g-i-t--v-e-r-s-i-o-n" + +This is a multi-part message in MIME format. +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/plain; charset=UTF-8; format=fixed +Content-Transfer-Encoding: 8bit + +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/x-patch; + name="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff" +Content-Transfer-Encoding: 8bit +Content-Disposition: inline; + filename="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff" + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +--------------g-i-t--v-e-r-s-i-o-n-- + + +$ diff --git a/t/t4013/diff.format-patch_--attach_--stdout_initial..master^ b/t/t4013/diff.format-patch_--attach_--stdout_initial..master^ new file mode 100644 index 0000000000..d0dd19b623 --- /dev/null +++ b/t/t4013/diff.format-patch_--attach_--stdout_initial..master^ @@ -0,0 +1,112 @@ +$ git format-patch --attach --stdout initial..master^ +From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:01:00 +0000 +Subject: [PATCH] Second +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------g-i-t--v-e-r-s-i-o-n" + +This is a multi-part message in MIME format. +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/plain; charset=UTF-8; format=fixed +Content-Transfer-Encoding: 8bit + + +This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/x-patch; + name="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff" +Content-Transfer-Encoding: 8bit +Content-Disposition: inline; + filename="1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44.diff" + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +--------------g-i-t--v-e-r-s-i-o-n-- + + + +From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:02:00 +0000 +Subject: [PATCH] Third +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------g-i-t--v-e-r-s-i-o-n" + +This is a multi-part message in MIME format. +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/plain; charset=UTF-8; format=fixed +Content-Transfer-Encoding: 8bit + +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/x-patch; + name="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff" +Content-Transfer-Encoding: 8bit +Content-Disposition: inline; + filename="9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0.diff" + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +--------------g-i-t--v-e-r-s-i-o-n-- + + +$ diff --git a/t/t4013/diff.format-patch_--attach_--stdout_initial..side b/t/t4013/diff.format-patch_--attach_--stdout_initial..side new file mode 100644 index 0000000000..67a95c5cba --- /dev/null +++ b/t/t4013/diff.format-patch_--attach_--stdout_initial..side @@ -0,0 +1,62 @@ +$ git format-patch --attach --stdout initial..side +From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:03:00 +0000 +Subject: [PATCH] Side +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------g-i-t--v-e-r-s-i-o-n" + +This is a multi-part message in MIME format. +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/plain; charset=UTF-8; format=fixed +Content-Transfer-Encoding: 8bit + +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 +--------------g-i-t--v-e-r-s-i-o-n +Content-Type: text/x-patch; + name="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff" +Content-Transfer-Encoding: 8bit +Content-Disposition: inline; + filename="c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a.diff" + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +--------------g-i-t--v-e-r-s-i-o-n-- + + +$ diff --git a/t/t4013/diff.format-patch_--stdout_initial..master b/t/t4013/diff.format-patch_--stdout_initial..master new file mode 100644 index 0000000000..8b88ca4927 --- /dev/null +++ b/t/t4013/diff.format-patch_--stdout_initial..master @@ -0,0 +1,127 @@ +$ git format-patch --stdout initial..master +From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:01:00 +0000 +Subject: [PATCH] Second + +This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 +-- +g-i-t--v-e-r-s-i-o-n + + +From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:02:00 +0000 +Subject: [PATCH] Third + +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C +-- +g-i-t--v-e-r-s-i-o-n + + +From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:03:00 +0000 +Subject: [PATCH] Side + +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +-- +g-i-t--v-e-r-s-i-o-n + +$ diff --git a/t/t4013/diff.format-patch_--stdout_initial..master^ b/t/t4013/diff.format-patch_--stdout_initial..master^ new file mode 100644 index 0000000000..47a4b88637 --- /dev/null +++ b/t/t4013/diff.format-patch_--stdout_initial..master^ @@ -0,0 +1,81 @@ +$ git format-patch --stdout initial..master^ +From 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:01:00 +0000 +Subject: [PATCH] Second + +This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 +-- +g-i-t--v-e-r-s-i-o-n + + +From 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:02:00 +0000 +Subject: [PATCH] Third + +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C +-- +g-i-t--v-e-r-s-i-o-n + +$ diff --git a/t/t4013/diff.format-patch_--stdout_initial..side b/t/t4013/diff.format-patch_--stdout_initial..side new file mode 100644 index 0000000000..e765088475 --- /dev/null +++ b/t/t4013/diff.format-patch_--stdout_initial..side @@ -0,0 +1,47 @@ +$ git format-patch --stdout initial..side +From c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a Mon Sep 17 00:00:00 2001 +From: A U Thor <author@example.com> +Date: Mon, 26 Jun 2006 00:03:00 +0000 +Subject: [PATCH] Side + +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +-- +g-i-t--v-e-r-s-i-o-n + +$ diff --git a/t/t4013/diff.log_--patch-with-stat_--summary_master_--_dir_ b/t/t4013/diff.log_--patch-with-stat_--summary_master_--_dir_ new file mode 100644 index 0000000000..3ceb8e73c5 --- /dev/null +++ b/t/t4013/diff.log_--patch-with-stat_--summary_master_--_dir_ @@ -0,0 +1,74 @@ +$ git log --patch-with-stat --summary master -- dir/ +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +$ diff --git a/t/t4013/diff.log_--patch-with-stat_master b/t/t4013/diff.log_--patch-with-stat_master new file mode 100644 index 0000000000..43d77761f9 --- /dev/null +++ b/t/t4013/diff.log_--patch-with-stat_master @@ -0,0 +1,129 @@ +$ git log --patch-with-stat master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +$ diff --git a/t/t4013/diff.log_--patch-with-stat_master_--_dir_ b/t/t4013/diff.log_--patch-with-stat_master_--_dir_ new file mode 100644 index 0000000000..5187a26816 --- /dev/null +++ b/t/t4013/diff.log_--patch-with-stat_master_--_dir_ @@ -0,0 +1,74 @@ +$ git log --patch-with-stat master -- dir/ +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +$ diff --git a/t/t4013/diff.log_--root_--cc_--patch-with-stat_--summary_master b/t/t4013/diff.log_--root_--cc_--patch-with-stat_--summary_master new file mode 100644 index 0000000000..c9640976a8 --- /dev/null +++ b/t/t4013/diff.log_--root_--cc_--patch-with-stat_--summary_master @@ -0,0 +1,199 @@ +$ git log --root --cc --patch-with-stat --summary master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --cc dir/sub +index cead32e,7289e35..992913c +--- a/dir/sub ++++ b/dir/sub +@@@ -1,6 -1,4 +1,8 @@@ + A + B + +C + +D + +E + +F ++ 1 ++ 2 +diff --cc file0 +index b414108,f4615da..10a8a9f +--- a/file0 ++++ b/file0 +@@@ -1,6 -1,6 +1,9 @@@ + 1 + 2 + 3 + +4 + +5 + +6 ++ A ++ B ++ C + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + create mode 100644 dir/sub + create mode 100644 file0 + create mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.log_--root_--patch-with-stat_--summary_master b/t/t4013/diff.log_--root_--patch-with-stat_--summary_master new file mode 100644 index 0000000000..ad050af55f --- /dev/null +++ b/t/t4013/diff.log_--root_--patch-with-stat_--summary_master @@ -0,0 +1,167 @@ +$ git log --root --patch-with-stat --summary master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + create mode 100644 dir/sub + create mode 100644 file0 + create mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.log_--root_--patch-with-stat_master b/t/t4013/diff.log_--root_--patch-with-stat_master new file mode 100644 index 0000000000..628c6c03bc --- /dev/null +++ b/t/t4013/diff.log_--root_--patch-with-stat_master @@ -0,0 +1,161 @@ +$ git log --root --patch-with-stat master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.log_--root_-c_--patch-with-stat_--summary_master b/t/t4013/diff.log_--root_-c_--patch-with-stat_--summary_master new file mode 100644 index 0000000000..5d4e0f13b5 --- /dev/null +++ b/t/t4013/diff.log_--root_-c_--patch-with-stat_--summary_master @@ -0,0 +1,199 @@ +$ git log --root -c --patch-with-stat --summary master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --combined dir/sub +index cead32e,7289e35..992913c +--- a/dir/sub ++++ b/dir/sub +@@@ -1,6 -1,4 +1,8 @@@ + A + B + +C + +D + +E + +F ++ 1 ++ 2 +diff --combined file0 +index b414108,f4615da..10a8a9f +--- a/file0 ++++ b/file0 +@@@ -1,6 -1,6 +1,9 @@@ + 1 + 2 + 3 + +4 + +5 + +6 ++ A ++ B ++ C + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + create mode 100644 dir/sub + create mode 100644 file0 + create mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.log_--root_-p_master b/t/t4013/diff.log_--root_-p_master new file mode 100644 index 0000000000..217a2eb203 --- /dev/null +++ b/t/t4013/diff.log_--root_-p_master @@ -0,0 +1,142 @@ +$ git log --root -p master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.log_--root_master b/t/t4013/diff.log_--root_master new file mode 100644 index 0000000000..e17ccfc234 --- /dev/null +++ b/t/t4013/diff.log_--root_master @@ -0,0 +1,34 @@ +$ git log --root master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +$ diff --git a/t/t4013/diff.log_-SF_-p_master b/t/t4013/diff.log_-SF_-p_master new file mode 100644 index 0000000000..5e32438972 --- /dev/null +++ b/t/t4013/diff.log_-SF_-p_master @@ -0,0 +1,18 @@ +$ git log -SF -p master +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +$ diff --git a/t/t4013/diff.log_-SF_master b/t/t4013/diff.log_-SF_master new file mode 100644 index 0000000000..6162ed2018 --- /dev/null +++ b/t/t4013/diff.log_-SF_master @@ -0,0 +1,8 @@ +$ git log -SF master +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +$ diff --git a/t/t4013/diff.log_-p_master b/t/t4013/diff.log_-p_master new file mode 100644 index 0000000000..f8fefef2c3 --- /dev/null +++ b/t/t4013/diff.log_-p_master @@ -0,0 +1,115 @@ +$ git log -p master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +$ diff --git a/t/t4013/diff.log_master b/t/t4013/diff.log_master new file mode 100644 index 0000000000..e9d9e7b40a --- /dev/null +++ b/t/t4013/diff.log_master @@ -0,0 +1,34 @@ +$ git log master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +$ diff --git a/t/t4013/diff.show_--patch-with-raw_side b/t/t4013/diff.show_--patch-with-raw_side new file mode 100644 index 0000000000..221b46a7cc --- /dev/null +++ b/t/t4013/diff.show_--patch-with-raw_side @@ -0,0 +1,42 @@ +$ git show --patch-with-raw side +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +:100644 100644 35d242b... 7289e35... M dir/sub +:100644 100644 01e79c3... f4615da... M file0 +:000000 100644 0000000... 7289e35... A file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.show_--patch-with-stat_--summary_side b/t/t4013/diff.show_--patch-with-stat_--summary_side new file mode 100644 index 0000000000..377f2b7b7a --- /dev/null +++ b/t/t4013/diff.show_--patch-with-stat_--summary_side @@ -0,0 +1,44 @@ +$ git show --patch-with-stat --summary side +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.show_--patch-with-stat_side b/t/t4013/diff.show_--patch-with-stat_side new file mode 100644 index 0000000000..fb14c530d2 --- /dev/null +++ b/t/t4013/diff.show_--patch-with-stat_side @@ -0,0 +1,43 @@ +$ git show --patch-with-stat side +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.show_--root_initial b/t/t4013/diff.show_--root_initial new file mode 100644 index 0000000000..8c89136c4d --- /dev/null +++ b/t/t4013/diff.show_--root_initial @@ -0,0 +1,34 @@ +$ git show --root initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.show_--stat_--summary_side b/t/t4013/diff.show_--stat_--summary_side new file mode 100644 index 0000000000..5bd5977628 --- /dev/null +++ b/t/t4013/diff.show_--stat_--summary_side @@ -0,0 +1,13 @@ +$ git show --stat --summary side +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 +$ diff --git a/t/t4013/diff.show_--stat_side b/t/t4013/diff.show_--stat_side new file mode 100644 index 0000000000..3b22327e48 --- /dev/null +++ b/t/t4013/diff.show_--stat_side @@ -0,0 +1,12 @@ +$ git show --stat side +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) +$ diff --git a/t/t4013/diff.show_initial b/t/t4013/diff.show_initial new file mode 100644 index 0000000000..4c4066ae48 --- /dev/null +++ b/t/t4013/diff.show_initial @@ -0,0 +1,7 @@ +$ git show initial +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +$ diff --git a/t/t4013/diff.show_master b/t/t4013/diff.show_master new file mode 100644 index 0000000000..9e6e1f2710 --- /dev/null +++ b/t/t4013/diff.show_master @@ -0,0 +1,36 @@ +$ git show master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + +diff --cc dir/sub +index cead32e,7289e35..992913c +--- a/dir/sub ++++ b/dir/sub +@@@ -1,6 -1,4 +1,8 @@@ + A + B + +C + +D + +E + +F ++ 1 ++ 2 +diff --cc file0 +index b414108,f4615da..10a8a9f +--- a/file0 ++++ b/file0 +@@@ -1,6 -1,6 +1,9 @@@ + 1 + 2 + 3 + +4 + +5 + +6 ++ A ++ B ++ C +$ diff --git a/t/t4013/diff.show_side b/t/t4013/diff.show_side new file mode 100644 index 0000000000..530a073b19 --- /dev/null +++ b/t/t4013/diff.show_side @@ -0,0 +1,38 @@ +$ git show side +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 +$ diff --git a/t/t4013/diff.whatchanged_--patch-with-stat_--summary_master_--_dir_ b/t/t4013/diff.whatchanged_--patch-with-stat_--summary_master_--_dir_ new file mode 100644 index 0000000000..6a467cccc1 --- /dev/null +++ b/t/t4013/diff.whatchanged_--patch-with-stat_--summary_master_--_dir_ @@ -0,0 +1,61 @@ +$ git whatchanged --patch-with-stat --summary master -- dir/ +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +$ diff --git a/t/t4013/diff.whatchanged_--patch-with-stat_master b/t/t4013/diff.whatchanged_--patch-with-stat_master new file mode 100644 index 0000000000..1e1bbe1963 --- /dev/null +++ b/t/t4013/diff.whatchanged_--patch-with-stat_master @@ -0,0 +1,116 @@ +$ git whatchanged --patch-with-stat master +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 +$ diff --git a/t/t4013/diff.whatchanged_--patch-with-stat_master_--_dir_ b/t/t4013/diff.whatchanged_--patch-with-stat_master_--_dir_ new file mode 100644 index 0000000000..13789f169b --- /dev/null +++ b/t/t4013/diff.whatchanged_--patch-with-stat_master_--_dir_ @@ -0,0 +1,61 @@ +$ git whatchanged --patch-with-stat master -- dir/ +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + 1 files changed, 2 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +$ diff --git a/t/t4013/diff.whatchanged_--root_--cc_--patch-with-stat_--summary_master b/t/t4013/diff.whatchanged_--root_--cc_--patch-with-stat_--summary_master new file mode 100644 index 0000000000..5facf2543d --- /dev/null +++ b/t/t4013/diff.whatchanged_--root_--cc_--patch-with-stat_--summary_master @@ -0,0 +1,199 @@ +$ git whatchanged --root --cc --patch-with-stat --summary master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --cc dir/sub +index cead32e,7289e35..992913c +--- a/dir/sub ++++ b/dir/sub +@@@ -1,6 -1,4 +1,8 @@@ + A + B + +C + +D + +E + +F ++ 1 ++ 2 +diff --cc file0 +index b414108,f4615da..10a8a9f +--- a/file0 ++++ b/file0 +@@@ -1,6 -1,6 +1,9 @@@ + 1 + 2 + 3 + +4 + +5 + +6 ++ A ++ B ++ C + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + create mode 100644 dir/sub + create mode 100644 file0 + create mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.whatchanged_--root_--patch-with-stat_--summary_master b/t/t4013/diff.whatchanged_--root_--patch-with-stat_--summary_master new file mode 100644 index 0000000000..0291153587 --- /dev/null +++ b/t/t4013/diff.whatchanged_--root_--patch-with-stat_--summary_master @@ -0,0 +1,160 @@ +$ git whatchanged --root --patch-with-stat --summary master +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + create mode 100644 dir/sub + create mode 100644 file0 + create mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.whatchanged_--root_--patch-with-stat_master b/t/t4013/diff.whatchanged_--root_--patch-with-stat_master new file mode 100644 index 0000000000..9b0349cd55 --- /dev/null +++ b/t/t4013/diff.whatchanged_--root_--patch-with-stat_master @@ -0,0 +1,154 @@ +$ git whatchanged --root --patch-with-stat master +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.whatchanged_--root_-c_--patch-with-stat_--summary_master b/t/t4013/diff.whatchanged_--root_-c_--patch-with-stat_--summary_master new file mode 100644 index 0000000000..10f6767e49 --- /dev/null +++ b/t/t4013/diff.whatchanged_--root_-c_--patch-with-stat_--summary_master @@ -0,0 +1,199 @@ +$ git whatchanged --root -c --patch-with-stat --summary master +commit 59d314ad6f356dd08601a4cd5e530381da3e3c64 +Merge: 9a6d494... c7a2ab9... +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:04:00 2006 +0000 + + Merge branch 'side' + + dir/sub | 2 ++ + file0 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + +diff --combined dir/sub +index cead32e,7289e35..992913c +--- a/dir/sub ++++ b/dir/sub +@@@ -1,6 -1,4 +1,8 @@@ + A + B + +C + +D + +E + +F ++ 1 ++ 2 +diff --combined file0 +index b414108,f4615da..10a8a9f +--- a/file0 ++++ b/file0 +@@@ -1,6 -1,6 +1,9 @@@ + 1 + 2 + 3 + +4 + +5 + +6 ++ A ++ B ++ C + +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side +--- + dir/sub | 2 ++ + file0 | 3 +++ + file3 | 4 ++++ + 3 files changed, 9 insertions(+), 0 deletions(-) + create mode 100644 file3 + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third +--- + dir/sub | 2 ++ + file1 | 3 +++ + 2 files changed, 5 insertions(+), 0 deletions(-) + create mode 100644 file1 + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 --- + 3 files changed, 5 insertions(+), 3 deletions(-) + delete mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial +--- + dir/sub | 2 ++ + file0 | 3 +++ + file2 | 3 +++ + 3 files changed, 8 insertions(+), 0 deletions(-) + create mode 100644 dir/sub + create mode 100644 file0 + create mode 100644 file2 + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.whatchanged_--root_-p_master b/t/t4013/diff.whatchanged_--root_-p_master new file mode 100644 index 0000000000..ebf1f0661e --- /dev/null +++ b/t/t4013/diff.whatchanged_--root_-p_master @@ -0,0 +1,135 @@ +$ git whatchanged --root -p master +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + +diff --git a/dir/sub b/dir/sub +new file mode 100644 +index 0000000..35d242b +--- /dev/null ++++ b/dir/sub +@@ -0,0 +1,2 @@ ++A ++B +diff --git a/file0 b/file0 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file0 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +diff --git a/file2 b/file2 +new file mode 100644 +index 0000000..01e79c3 +--- /dev/null ++++ b/file2 +@@ -0,0 +1,3 @@ ++1 ++2 ++3 +$ diff --git a/t/t4013/diff.whatchanged_--root_master b/t/t4013/diff.whatchanged_--root_master new file mode 100644 index 0000000000..a405cb6138 --- /dev/null +++ b/t/t4013/diff.whatchanged_--root_master @@ -0,0 +1,42 @@ +$ git whatchanged --root master +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +:100644 100644 35d242b... 7289e35... M dir/sub +:100644 100644 01e79c3... f4615da... M file0 +:000000 100644 0000000... 7289e35... A file3 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +:100644 100644 8422d40... cead32e... M dir/sub +:000000 100644 0000000... b1e6722... A file1 + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. + +:100644 100644 35d242b... 8422d40... M dir/sub +:100644 100644 01e79c3... b414108... M file0 +:100644 000000 01e79c3... 0000000... D file2 + +commit 444ac553ac7612cc88969031b02b3767fb8a353a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:00:00 2006 +0000 + + Initial + +:000000 100644 0000000... 35d242b... A dir/sub +:000000 100644 0000000... 01e79c3... A file0 +:000000 100644 0000000... 01e79c3... A file2 +$ diff --git a/t/t4013/diff.whatchanged_-SF_-p_master b/t/t4013/diff.whatchanged_-SF_-p_master new file mode 100644 index 0000000000..f39da84822 --- /dev/null +++ b/t/t4013/diff.whatchanged_-SF_-p_master @@ -0,0 +1,18 @@ +$ git whatchanged -SF -p master +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +$ diff --git a/t/t4013/diff.whatchanged_-SF_master b/t/t4013/diff.whatchanged_-SF_master new file mode 100644 index 0000000000..0499321d0e --- /dev/null +++ b/t/t4013/diff.whatchanged_-SF_master @@ -0,0 +1,9 @@ +$ git whatchanged -SF master +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +:100644 100644 8422d40... cead32e... M dir/sub +$ diff --git a/t/t4013/diff.whatchanged_-p_master b/t/t4013/diff.whatchanged_-p_master new file mode 100644 index 0000000000..f18d43209c --- /dev/null +++ b/t/t4013/diff.whatchanged_-p_master @@ -0,0 +1,102 @@ +$ git whatchanged -p master +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +diff --git a/dir/sub b/dir/sub +index 35d242b..7289e35 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++1 ++2 +diff --git a/file0 b/file0 +index 01e79c3..f4615da 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++A ++B ++C +diff --git a/file3 b/file3 +new file mode 100644 +index 0000000..7289e35 +--- /dev/null ++++ b/file3 +@@ -0,0 +1,4 @@ ++A ++B ++1 ++2 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +diff --git a/dir/sub b/dir/sub +index 8422d40..cead32e 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -2,3 +2,5 @@ A + B + C + D ++E ++F +diff --git a/file1 b/file1 +new file mode 100644 +index 0000000..b1e6722 +--- /dev/null ++++ b/file1 +@@ -0,0 +1,3 @@ ++A ++B ++C + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. + +diff --git a/dir/sub b/dir/sub +index 35d242b..8422d40 100644 +--- a/dir/sub ++++ b/dir/sub +@@ -1,2 +1,4 @@ + A + B ++C ++D +diff --git a/file0 b/file0 +index 01e79c3..b414108 100644 +--- a/file0 ++++ b/file0 +@@ -1,3 +1,6 @@ + 1 + 2 + 3 ++4 ++5 ++6 +diff --git a/file2 b/file2 +deleted file mode 100644 +index 01e79c3..0000000 +--- a/file2 ++++ /dev/null +@@ -1,3 +0,0 @@ +-1 +-2 +-3 +$ diff --git a/t/t4013/diff.whatchanged_master b/t/t4013/diff.whatchanged_master new file mode 100644 index 0000000000..cd3bcc2c72 --- /dev/null +++ b/t/t4013/diff.whatchanged_master @@ -0,0 +1,32 @@ +$ git whatchanged master +commit c7a2ab9e8eac7b117442a607d5a9b3950ae34d5a +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:03:00 2006 +0000 + + Side + +:100644 100644 35d242b... 7289e35... M dir/sub +:100644 100644 01e79c3... f4615da... M file0 +:000000 100644 0000000... 7289e35... A file3 + +commit 9a6d4949b6b76956d9d5e26f2791ec2ceff5fdc0 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:02:00 2006 +0000 + + Third + +:100644 100644 8422d40... cead32e... M dir/sub +:000000 100644 0000000... b1e6722... A file1 + +commit 1bde4ae5f36c8d9abe3a0fce0c6aab3c4a12fe44 +Author: A U Thor <author@example.com> +Date: Mon Jun 26 00:01:00 2006 +0000 + + Second + + This is the second commit. + +:100644 100644 35d242b... 8422d40... M dir/sub +:100644 100644 01e79c3... b414108... M file0 +:100644 000000 01e79c3... 0000000... D file2 +$ diff --git a/t/t4014-format-patch.sh b/t/t4014-format-patch.sh new file mode 100755 index 0000000000..4795872a77 --- /dev/null +++ b/t/t4014-format-patch.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='Format-patch skipping already incorporated patches' + +. ./test-lib.sh + +test_expect_success setup ' + + for i in 1 2 3 4 5 6 7 8 9 10; do echo "$i"; done >file && + git add file && + git commit -m Initial && + git checkout -b side && + + for i in 1 2 5 6 A B C 7 8 9 10; do echo "$i"; done >file && + git update-index file && + git commit -m "Side change #1" && + + for i in D E F; do echo "$i"; done >>file && + git update-index file && + git commit -m "Side change #2" && + git tag C2 && + + for i in 5 6 1 2 3 A 4 B C 7 8 9 10 D E F; do echo "$i"; done >file && + git update-index file && + git commit -m "Side change #3" && + + git checkout master && + git diff-tree -p C2 | git apply --index && + git commit -m "Master accepts moral equivalent of #2" + +' + +test_expect_success "format-patch --ignore-if-in-upstream" ' + + git format-patch --stdout master..side >patch0 && + cnt=`grep "^From " patch0 | wc -l` && + test $cnt = 3 + +' + +test_expect_success "format-patch --ignore-if-in-upstream" ' + + git format-patch --stdout \ + --ignore-if-in-upstream master..side >patch1 && + cnt=`grep "^From " patch1 | wc -l` && + test $cnt = 2 + +' + +test_expect_success "format-patch result applies" ' + + git checkout -b rebuild-0 master && + git am -3 patch0 && + cnt=`git rev-list master.. | wc -l` && + test $cnt = 2 +' + +test_expect_success "format-patch --ignore-if-in-upstream result applies" ' + + git checkout -b rebuild-1 master && + git am -3 patch1 && + cnt=`git rev-list master.. | wc -l` && + test $cnt = 2 +' + +test_done diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh new file mode 100755 index 0000000000..adf4993bac --- /dev/null +++ b/t/t4015-diff-whitespace.sh @@ -0,0 +1,120 @@ +#!/bin/sh +# +# Copyright (c) 2006 Johannes E. Schindelin +# + +test_description='Test special whitespace in diff engine. + +' +. ./test-lib.sh +. ../diff-lib.sh + +# Ray Lehtiniemi's example + +cat << EOF > x +do { + nothing; +} while (0); +EOF + +git-update-index --add x + +cat << EOF > x +do +{ + nothing; +} +while (0); +EOF + +cat << EOF > expect +diff --git a/x b/x +index adf3937..6edc172 100644 +--- a/x ++++ b/x +@@ -1,3 +1,5 @@ +-do { ++do ++{ + nothing; +-} while (0); ++} ++while (0); +EOF + +git-diff > out +test_expect_success "Ray's example without options" 'diff -u expect out' + +git-diff -w > out +test_expect_success "Ray's example with -w" 'diff -u expect out' + +git-diff -b > out +test_expect_success "Ray's example with -b" 'diff -u expect out' + +tr 'Q' '\015' << EOF > x +whitespace at beginning +whitespace change +whitespace in the middle +whitespace at end +unchanged line +CR at endQ +EOF + +git-update-index x + +cat << EOF > x + whitespace at beginning +whitespace change +white space in the middle +whitespace at end +unchanged line +CR at end +EOF + +tr 'Q' '\015' << EOF > expect +diff --git a/x b/x +index d99af23..8b32fb5 100644 +--- a/x ++++ b/x +@@ -1,6 +1,6 @@ +-whitespace at beginning +-whitespace change +-whitespace in the middle +-whitespace at end ++ whitespace at beginning ++whitespace change ++white space in the middle ++whitespace at end + unchanged line +-CR at endQ ++CR at end +EOF +git-diff > out +test_expect_success 'another test, without options' 'diff -u expect out' + +cat << EOF > expect +diff --git a/x b/x +index d99af23..8b32fb5 100644 +EOF +git-diff -w > out +test_expect_success 'another test, with -w' 'diff -u expect out' + +tr 'Q' '\015' << EOF > expect +diff --git a/x b/x +index d99af23..8b32fb5 100644 +--- a/x ++++ b/x +@@ -1,6 +1,6 @@ +-whitespace at beginning ++ whitespace at beginning + whitespace change +-whitespace in the middle ++white space in the middle + whitespace at end + unchanged line + CR at endQ +EOF +git-diff -b > out +test_expect_success 'another test, with -b' 'diff -u expect out' + +test_done diff --git a/t/t4016-diff-quote.sh b/t/t4016-diff-quote.sh new file mode 100755 index 0000000000..edde8f5568 --- /dev/null +++ b/t/t4016-diff-quote.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# +# Copyright (c) 2007 Junio C Hamano +# + +test_description='Quoting paths in diff output. +' + +. ./test-lib.sh + +P0='pathname' +P1='pathname with HT' +P2='pathname with SP' +P3='pathname +with LF' + +test_expect_success setup ' + echo P0.0 >"$P0.0" && + echo P0.1 >"$P0.1" && + echo P0.2 >"$P0.2" && + echo P0.3 >"$P0.3" && + echo P1.0 >"$P1.0" && + echo P1.2 >"$P1.2" && + echo P1.3 >"$P1.3" && + git add . && + git commit -m initial && + git mv "$P0.0" "R$P0.0" && + git mv "$P0.1" "R$P1.0" && + git mv "$P0.2" "R$P2.0" && + git mv "$P0.3" "R$P3.0" && + git mv "$P1.0" "R$P0.1" && + git mv "$P1.2" "R$P2.1" && + git mv "$P1.3" "R$P3.1" && + : +' + +cat >expect <<\EOF + rename pathname.1 => "Rpathname\twith HT.0" (100%) + rename pathname.3 => "Rpathname\nwith LF.0" (100%) + rename "pathname\twith HT.3" => "Rpathname\nwith LF.1" (100%) + rename pathname.2 => Rpathname with SP.0 (100%) + rename "pathname\twith HT.2" => Rpathname with SP.1 (100%) + rename pathname.0 => Rpathname.0 (100%) + rename "pathname\twith HT.0" => Rpathname.1 (100%) +EOF +test_expect_success 'git diff --summary -M HEAD' ' + git diff --summary -M HEAD >actual && + diff -u expect actual +' + +cat >expect <<\EOF + pathname.1 => "Rpathname\twith HT.0" | 0 + pathname.3 => "Rpathname\nwith LF.0" | 0 + "pathname\twith HT.3" => "Rpathname\nwith LF.1" | 0 + pathname.2 => Rpathname with SP.0 | 0 + "pathname\twith HT.2" => Rpathname with SP.1 | 0 + pathname.0 => Rpathname.0 | 0 + "pathname\twith HT.0" => Rpathname.1 | 0 + 7 files changed, 0 insertions(+), 0 deletions(-) +EOF +test_expect_success 'git diff --stat -M HEAD' ' + git diff --stat -M HEAD >actual && + diff -u expect actual +' + +test_done diff --git a/t/t4100-apply-stat.sh b/t/t4100-apply-stat.sh new file mode 100755 index 0000000000..6579f06b05 --- /dev/null +++ b/t/t4100-apply-stat.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-apply --stat --summary test. + +' +. ./test-lib.sh + +test_expect_success \ + 'rename' \ + 'git-apply --stat --summary <../t4100/t-apply-1.patch >current && + diff -u ../t4100/t-apply-1.expect current' + +test_expect_success \ + 'copy' \ + 'git-apply --stat --summary <../t4100/t-apply-2.patch >current && + diff -u ../t4100/t-apply-2.expect current' + +test_expect_success \ + 'rewrite' \ + 'git-apply --stat --summary <../t4100/t-apply-3.patch >current && + diff -u ../t4100/t-apply-3.expect current' + +test_expect_success \ + 'mode' \ + 'git-apply --stat --summary <../t4100/t-apply-4.patch >current && + diff -u ../t4100/t-apply-4.expect current' + +test_expect_success \ + 'non git' \ + 'git-apply --stat --summary <../t4100/t-apply-5.patch >current && + diff -u ../t4100/t-apply-5.expect current' + +test_expect_success \ + 'non git' \ + 'git-apply --stat --summary <../t4100/t-apply-6.patch >current && + diff -u ../t4100/t-apply-6.expect current' + +test_expect_success \ + 'non git' \ + 'git-apply --stat --summary <../t4100/t-apply-7.patch >current && + diff -u ../t4100/t-apply-7.expect current' + +test_done + diff --git a/t/t4100/t-apply-1.expect b/t/t4100/t-apply-1.expect new file mode 100644 index 0000000000..540e64db85 --- /dev/null +++ b/t/t4100/t-apply-1.expect @@ -0,0 +1,11 @@ + Documentation/git-ssh-pull.txt | 12 ++++++------ + Documentation/git-ssh-push.txt | 10 +++++----- + Documentation/git.txt | 6 +++--- + Makefile | 6 +++--- + ssh-pull.c | 4 ++-- + ssh-push.c | 14 +++++++------- + 6 files changed, 26 insertions(+), 26 deletions(-) + rename Documentation/{git-rpull.txt => git-ssh-pull.txt} (90%) + rename Documentation/{git-rpush.txt => git-ssh-push.txt} (71%) + rename rpull.c => ssh-pull.c (97%) + rename rpush.c => ssh-push.c (93%) diff --git a/t/t4100/t-apply-1.patch b/t/t4100/t-apply-1.patch new file mode 100644 index 0000000000..de587517f4 --- /dev/null +++ b/t/t4100/t-apply-1.patch @@ -0,0 +1,194 @@ +418aaf847a8b3ffffb4f777a2dd5262ca5ce0ef7 (from dc93841715dfa9a9cdda6f2c4a25eec831ea7aa0) +diff --git a/Documentation/git-rpull.txt b/Documentation/git-ssh-pull.txt +similarity index 90% +rename from Documentation/git-rpull.txt +rename to Documentation/git-ssh-pull.txt +--- a/Documentation/git-rpull.txt ++++ b/Documentation/git-ssh-pull.txt +@@ -1,21 +1,21 @@ +-git-rpull(1) +-============ ++git-ssh-pull(1) ++=============== + v0.1, May 2005 + + NAME + ---- +-git-rpull - Pulls from a remote repository over ssh connection ++git-ssh-pull - Pulls from a remote repository over ssh connection + + + + SYNOPSIS + -------- +-'git-rpull' [-c] [-t] [-a] [-d] [-v] [--recover] commit-id url ++'git-ssh-pull' [-c] [-t] [-a] [-d] [-v] [--recover] commit-id url + + DESCRIPTION + ----------- +-Pulls from a remote repository over ssh connection, invoking git-rpush on +-the other end. ++Pulls from a remote repository over ssh connection, invoking git-ssh-push ++on the other end. + + OPTIONS + ------- +diff --git a/Documentation/git-rpush.txt b/Documentation/git-ssh-push.txt +similarity index 71% +rename from Documentation/git-rpush.txt +rename to Documentation/git-ssh-push.txt +--- a/Documentation/git-rpush.txt ++++ b/Documentation/git-ssh-push.txt +@@ -1,19 +1,19 @@ +-git-rpush(1) +-============ ++git-ssh-push(1) ++=============== + v0.1, May 2005 + + NAME + ---- +-git-rpush - Helper "server-side" program used by git-rpull ++git-ssh-push - Helper "server-side" program used by git-ssh-pull + + + SYNOPSIS + -------- +-'git-rpush' ++'git-ssh-push' + + DESCRIPTION + ----------- +-Helper "server-side" program used by git-rpull. ++Helper "server-side" program used by git-ssh-pull. + + + Author +diff --git a/Documentation/git.txt b/Documentation/git.txt +--- a/Documentation/git.txt ++++ b/Documentation/git.txt +@@ -148,7 +148,7 @@ link:git-resolve-script.html[git-resolve + link:git-tag-script.html[git-tag-script]:: + An example script to create a tag object signed with GPG + +-link:git-rpull.html[git-rpull]:: ++link:git-ssh-pull.html[git-ssh-pull]:: + Pulls from a remote repository over ssh connection + + Interogators: +@@ -156,8 +156,8 @@ Interogators: + link:git-diff-helper.html[git-diff-helper]:: + Generates patch format output for git-diff-* + +-link:git-rpush.html[git-rpush]:: +- Helper "server-side" program used by git-rpull ++link:git-ssh-push.html[git-ssh-push]:: ++ Helper "server-side" program used by git-ssh-pull + + + +diff --git a/Makefile b/Makefile +--- a/Makefile ++++ b/Makefile +@@ -30,7 +30,7 @@ PROG= git-update-cache git-diff-files + git-checkout-cache git-diff-tree git-rev-tree git-ls-files \ + git-check-files git-ls-tree git-merge-base git-merge-cache \ + git-unpack-file git-export git-diff-cache git-convert-cache \ +- git-http-pull git-rpush git-rpull git-rev-list git-mktag \ ++ git-http-pull git-ssh-push git-ssh-pull git-rev-list git-mktag \ + git-diff-helper git-tar-tree git-local-pull git-write-blob \ + git-get-tar-commit-id git-mkdelta git-apply git-stripspace + +@@ -105,8 +105,8 @@ git-diff-cache: diff-cache.c + git-convert-cache: convert-cache.c + git-http-pull: http-pull.c pull.c + git-local-pull: local-pull.c pull.c +-git-rpush: rsh.c +-git-rpull: rsh.c pull.c ++git-ssh-push: rsh.c ++git-ssh-pull: rsh.c pull.c + git-rev-list: rev-list.c + git-mktag: mktag.c + git-diff-helper: diff-helper.c +diff --git a/rpull.c b/ssh-pull.c +similarity index 97% +rename from rpull.c +rename to ssh-pull.c +--- a/rpull.c ++++ b/ssh-pull.c +@@ -64,13 +64,13 @@ int main(int argc, char **argv) + arg++; + } + if (argc < arg + 2) { +- usage("git-rpull [-c] [-t] [-a] [-v] [-d] [--recover] commit-id url"); ++ usage("git-ssh-pull [-c] [-t] [-a] [-v] [-d] [--recover] commit-id url"); + return 1; + } + commit_id = argv[arg]; + url = argv[arg + 1]; + +- if (setup_connection(&fd_in, &fd_out, "git-rpush", url, arg, argv + 1)) ++ if (setup_connection(&fd_in, &fd_out, "git-ssh-push", url, arg, argv + 1)) + return 1; + + if (get_version()) +diff --git a/rpush.c b/ssh-push.c +similarity index 93% +rename from rpush.c +rename to ssh-push.c +--- a/rpush.c ++++ b/ssh-push.c +@@ -16,7 +16,7 @@ int serve_object(int fd_in, int fd_out) + do { + size = read(fd_in, sha1 + posn, 20 - posn); + if (size < 0) { +- perror("git-rpush: read "); ++ perror("git-ssh-push: read "); + return -1; + } + if (!size) +@@ -30,7 +30,7 @@ int serve_object(int fd_in, int fd_out) + buf = map_sha1_file(sha1, &objsize); + + if (!buf) { +- fprintf(stderr, "git-rpush: could not find %s\n", ++ fprintf(stderr, "git-ssh-push: could not find %s\n", + sha1_to_hex(sha1)); + remote = -1; + } +@@ -45,9 +45,9 @@ int serve_object(int fd_in, int fd_out) + size = write(fd_out, buf + posn, objsize - posn); + if (size <= 0) { + if (!size) { +- fprintf(stderr, "git-rpush: write closed"); ++ fprintf(stderr, "git-ssh-push: write closed"); + } else { +- perror("git-rpush: write "); ++ perror("git-ssh-push: write "); + } + return -1; + } +@@ -71,7 +71,7 @@ void service(int fd_in, int fd_out) { + retval = read(fd_in, &type, 1); + if (retval < 1) { + if (retval < 0) +- perror("rpush: read "); ++ perror("git-ssh-push: read "); + return; + } + if (type == 'v' && serve_version(fd_in, fd_out)) +@@ -91,12 +91,12 @@ int main(int argc, char **argv) + arg++; + } + if (argc < arg + 2) { +- usage("git-rpush [-c] [-t] [-a] commit-id url"); ++ usage("git-ssh-push [-c] [-t] [-a] commit-id url"); + return 1; + } + commit_id = argv[arg]; + url = argv[arg + 1]; +- if (setup_connection(&fd_in, &fd_out, "git-rpull", url, arg, argv + 1)) ++ if (setup_connection(&fd_in, &fd_out, "git-ssh-pull", url, arg, argv + 1)) + return 1; + + service(fd_in, fd_out); diff --git a/t/t4100/t-apply-2.expect b/t/t4100/t-apply-2.expect new file mode 100644 index 0000000000..d1e6459749 --- /dev/null +++ b/t/t4100/t-apply-2.expect @@ -0,0 +1,5 @@ + Makefile | 2 +- + git-fetch-script | 5 ----- + git-pull-script | 34 +--------------------------------- + 3 files changed, 2 insertions(+), 39 deletions(-) + copy git-pull-script => git-fetch-script (87%) diff --git a/t/t4100/t-apply-2.patch b/t/t4100/t-apply-2.patch new file mode 100644 index 0000000000..cfdc80885b --- /dev/null +++ b/t/t4100/t-apply-2.patch @@ -0,0 +1,72 @@ +7ef76925d9c19ef74874e1735e2436e56d0c4897 (from 6b14d7faf0bad026a81a27bac07b47691f621b8f) +diff --git a/Makefile b/Makefile +--- a/Makefile ++++ b/Makefile +@@ -20,7 +20,7 @@ INSTALL=install + + SCRIPTS=git-apply-patch-script git-merge-one-file-script git-prune-script \ + git-pull-script git-tag-script git-resolve-script git-whatchanged \ +- git-deltafy-script ++ git-deltafy-script git-fetch-script + + PROG= git-update-cache git-diff-files git-init-db git-write-tree \ + git-read-tree git-commit-tree git-cat-file git-fsck-cache \ +diff --git a/git-pull-script b/git-fetch-script +similarity index 87% +copy from git-pull-script +copy to git-fetch-script +--- a/git-pull-script ++++ b/git-fetch-script +@@ -39,8 +39,3 @@ download_one "$merge_repo/$merge_name" " + + echo "Getting object database" + download_objects "$merge_repo" "$(cat "$GIT_DIR"/MERGE_HEAD)" +- +-git-resolve-script \ +- "$(cat "$GIT_DIR"/HEAD)" \ +- "$(cat "$GIT_DIR"/MERGE_HEAD)" \ +- "$merge_repo" +diff --git a/git-pull-script b/git-pull-script +--- a/git-pull-script ++++ b/git-pull-script +@@ -6,39 +6,7 @@ merge_name=${2:-HEAD} + : ${GIT_DIR=.git} + : ${GIT_OBJECT_DIRECTORY="${SHA1_FILE_DIRECTORY-"$GIT_DIR/objects"}"} + +-download_one () { +- # remote_path="$1" local_file="$2" +- case "$1" in +- http://*) +- wget -q -O "$2" "$1" ;; +- /*) +- test -f "$1" && cat >"$2" "$1" ;; +- *) +- rsync -L "$1" "$2" ;; +- esac +-} +- +-download_objects () { +- # remote_repo="$1" head_sha1="$2" +- case "$1" in +- http://*) +- git-http-pull -a "$2" "$1/" +- ;; +- /*) +- git-local-pull -l -a "$2" "$1/" +- ;; +- *) +- rsync -avz --ignore-existing \ +- "$1/objects/." "$GIT_OBJECT_DIRECTORY"/. +- ;; +- esac +-} +- +-echo "Getting remote $merge_name" +-download_one "$merge_repo/$merge_name" "$GIT_DIR"/MERGE_HEAD +- +-echo "Getting object database" +-download_objects "$merge_repo" "$(cat "$GIT_DIR"/MERGE_HEAD)" ++git-fetch-script "$merge_repo" "$merge_name" + + git-resolve-script \ + "$(cat "$GIT_DIR"/HEAD)" \ diff --git a/t/t4100/t-apply-3.expect b/t/t4100/t-apply-3.expect new file mode 100644 index 0000000000..912a552a7a --- /dev/null +++ b/t/t4100/t-apply-3.expect @@ -0,0 +1,7 @@ + Documentation/git-ls-tree.txt | 20 +- + ls-tree.c | 459 ++++++++++++++++++++++------------------- + t/t3100-ls-tree-restrict.sh | 3 + tree.c | 2 + tree.h | 1 + 5 files changed, 262 insertions(+), 223 deletions(-) + rewrite ls-tree.c (82%) diff --git a/t/t4100/t-apply-3.patch b/t/t4100/t-apply-3.patch new file mode 100644 index 0000000000..90cdbaa5bb --- /dev/null +++ b/t/t4100/t-apply-3.patch @@ -0,0 +1,567 @@ +6af1f0192ff8740fe77db7cf02c739ccfbdf119c (from 2bc2564145835996734d6ed5d1880f85b17233d6) +diff --git a/Documentation/git-ls-tree.txt b/Documentation/git-ls-tree.txt +--- a/Documentation/git-ls-tree.txt ++++ b/Documentation/git-ls-tree.txt +@@ -4,23 +4,26 @@ v0.1, May 2005 + + NAME + ---- +-git-ls-tree - Displays a tree object in human readable form ++git-ls-tree - Lists the contents of a tree object. + + + SYNOPSIS + -------- +-'git-ls-tree' [-r] [-z] <tree-ish> [paths...] ++'git-ls-tree' [-d] [-r] [-z] <tree-ish> [paths...] + + DESCRIPTION + ----------- +-Converts the tree object to a human readable (and script processable) +-form. ++Lists the contents of a tree object, like what "/bin/ls -a" does ++in the current working directory. + + OPTIONS + ------- + <tree-ish>:: + Id of a tree. + ++-d:: ++ show only the named tree entry itself, not its children ++ + -r:: + recurse into sub-trees + +@@ -28,18 +31,19 @@ OPTIONS + \0 line termination on output + + paths:: +- Optionally, restrict the output of git-ls-tree to specific +- paths. Directories will only list their tree blob ids. +- Implies -r. ++ When paths are given, shows them. Otherwise implicitly ++ uses the root level of the tree as the sole path argument. ++ + + Output Format + ------------- +- <mode>\t <type>\t <object>\t <file> ++ <mode> SP <type> SP <object> TAB <file> + + + Author + ------ + Written by Linus Torvalds <torvalds@osdl.org> ++Completely rewritten from scratch by Junio C Hamano <junkio@cox.net> + + Documentation + -------------- +diff --git a/ls-tree.c b/ls-tree.c +dissimilarity index 82% +--- ls-tree.c ++++ ls-tree.c +@@ -1,212 +1,247 @@ +-/* +- * GIT - The information manager from hell +- * +- * Copyright (C) Linus Torvalds, 2005 +- */ +-#include "cache.h" +- +-static int line_termination = '\n'; +-static int recursive = 0; +- +-struct path_prefix { +- struct path_prefix *prev; +- const char *name; +-}; +- +-#define DEBUG(fmt, ...) +- +-static int string_path_prefix(char *buff, size_t blen, struct path_prefix *prefix) +-{ +- int len = 0; +- if (prefix) { +- if (prefix->prev) { +- len = string_path_prefix(buff,blen,prefix->prev); +- buff += len; +- blen -= len; +- if (blen > 0) { +- *buff = '/'; +- len++; +- buff++; +- blen--; +- } +- } +- strncpy(buff,prefix->name,blen); +- return len + strlen(prefix->name); +- } +- +- return 0; +-} +- +-static void print_path_prefix(struct path_prefix *prefix) +-{ +- if (prefix) { +- if (prefix->prev) { +- print_path_prefix(prefix->prev); +- putchar('/'); +- } +- fputs(prefix->name, stdout); +- } +-} +- +-/* +- * return: +- * -1 if prefix is *not* a subset of path +- * 0 if prefix == path +- * 1 if prefix is a subset of path +- */ +-static int pathcmp(const char *path, struct path_prefix *prefix) +-{ +- char buff[PATH_MAX]; +- int len,slen; +- +- if (prefix == NULL) +- return 1; +- +- len = string_path_prefix(buff, sizeof buff, prefix); +- slen = strlen(path); +- +- if (slen < len) +- return -1; +- +- if (strncmp(path,buff,len) == 0) { +- if (slen == len) +- return 0; +- else +- return 1; +- } +- +- return -1; +-} +- +-/* +- * match may be NULL, or a *sorted* list of paths +- */ +-static void list_recursive(void *buffer, +- const char *type, +- unsigned long size, +- struct path_prefix *prefix, +- char **match, int matches) +-{ +- struct path_prefix this_prefix; +- this_prefix.prev = prefix; +- +- if (strcmp(type, "tree")) +- die("expected a 'tree' node"); +- +- if (matches) +- recursive = 1; +- +- while (size) { +- int namelen = strlen(buffer)+1; +- void *eltbuf = NULL; +- char elttype[20]; +- unsigned long eltsize; +- unsigned char *sha1 = buffer + namelen; +- char *path = strchr(buffer, ' ') + 1; +- unsigned int mode; +- const char *matched = NULL; +- int mtype = -1; +- int mindex; +- +- if (size < namelen + 20 || sscanf(buffer, "%o", &mode) != 1) +- die("corrupt 'tree' file"); +- buffer = sha1 + 20; +- size -= namelen + 20; +- +- this_prefix.name = path; +- for ( mindex = 0; mindex < matches; mindex++) { +- mtype = pathcmp(match[mindex],&this_prefix); +- if (mtype >= 0) { +- matched = match[mindex]; +- break; +- } +- } +- +- /* +- * If we're not matching, or if this is an exact match, +- * print out the info +- */ +- if (!matches || (matched != NULL && mtype == 0)) { +- printf("%06o %s %s\t", mode, +- S_ISDIR(mode) ? "tree" : "blob", +- sha1_to_hex(sha1)); +- print_path_prefix(&this_prefix); +- putchar(line_termination); +- } +- +- if (! recursive || ! S_ISDIR(mode)) +- continue; +- +- if (matches && ! matched) +- continue; +- +- if (! (eltbuf = read_sha1_file(sha1, elttype, &eltsize)) ) { +- error("cannot read %s", sha1_to_hex(sha1)); +- continue; +- } +- +- /* If this is an exact directory match, we may have +- * directory files following this path. Match on them. +- * Otherwise, we're at a pach subcomponent, and we need +- * to try to match again. +- */ +- if (mtype == 0) +- mindex++; +- +- list_recursive(eltbuf, elttype, eltsize, &this_prefix, &match[mindex], matches-mindex); +- free(eltbuf); +- } +-} +- +-static int qcmp(const void *a, const void *b) +-{ +- return strcmp(*(char **)a, *(char **)b); +-} +- +-static int list(unsigned char *sha1,char **path) +-{ +- void *buffer; +- unsigned long size; +- int npaths; +- +- for (npaths = 0; path[npaths] != NULL; npaths++) +- ; +- +- qsort(path,npaths,sizeof(char *),qcmp); +- +- buffer = read_object_with_reference(sha1, "tree", &size, NULL); +- if (!buffer) +- die("unable to read sha1 file"); +- list_recursive(buffer, "tree", size, NULL, path, npaths); +- free(buffer); +- return 0; +-} +- +-static const char *ls_tree_usage = "git-ls-tree [-r] [-z] <key> [paths...]"; +- +-int main(int argc, char **argv) +-{ +- unsigned char sha1[20]; +- +- while (1 < argc && argv[1][0] == '-') { +- switch (argv[1][1]) { +- case 'z': +- line_termination = 0; +- break; +- case 'r': +- recursive = 1; +- break; +- default: +- usage(ls_tree_usage); +- } +- argc--; argv++; +- } +- +- if (argc < 2) +- usage(ls_tree_usage); +- if (get_sha1(argv[1], sha1) < 0) +- usage(ls_tree_usage); +- if (list(sha1, &argv[2]) < 0) +- die("list failed"); +- return 0; +-} ++/* ++ * GIT - The information manager from hell ++ * ++ * Copyright (C) Linus Torvalds, 2005 ++ */ ++#include "cache.h" ++#include "blob.h" ++#include "tree.h" ++ ++static int line_termination = '\n'; ++#define LS_RECURSIVE 1 ++#define LS_TREE_ONLY 2 ++static int ls_options = 0; ++ ++static struct tree_entry_list root_entry; ++ ++static void prepare_root(unsigned char *sha1) ++{ ++ unsigned char rsha[20]; ++ unsigned long size; ++ void *buf; ++ struct tree *root_tree; ++ ++ buf = read_object_with_reference(sha1, "tree", &size, rsha); ++ free(buf); ++ if (!buf) ++ die("Could not read %s", sha1_to_hex(sha1)); ++ ++ root_tree = lookup_tree(rsha); ++ if (!root_tree) ++ die("Could not read %s", sha1_to_hex(sha1)); ++ ++ /* Prepare a fake entry */ ++ root_entry.directory = 1; ++ root_entry.executable = root_entry.symlink = 0; ++ root_entry.mode = S_IFDIR; ++ root_entry.name = ""; ++ root_entry.item.tree = root_tree; ++ root_entry.parent = NULL; ++} ++ ++static int prepare_children(struct tree_entry_list *elem) ++{ ++ if (!elem->directory) ++ return -1; ++ if (!elem->item.tree->object.parsed) { ++ struct tree_entry_list *e; ++ if (parse_tree(elem->item.tree)) ++ return -1; ++ /* Set up the parent link */ ++ for (e = elem->item.tree->entries; e; e = e->next) ++ e->parent = elem; ++ } ++ return 0; ++} ++ ++static struct tree_entry_list *find_entry_0(struct tree_entry_list *elem, ++ const char *path, ++ const char *path_end) ++{ ++ const char *ep; ++ int len; ++ ++ while (path < path_end) { ++ if (prepare_children(elem)) ++ return NULL; ++ ++ /* In elem->tree->entries, find the one that has name ++ * that matches what is between path and ep. ++ */ ++ elem = elem->item.tree->entries; ++ ++ ep = strchr(path, '/'); ++ if (!ep || path_end <= ep) ++ ep = path_end; ++ len = ep - path; ++ ++ while (elem) { ++ if ((strlen(elem->name) == len) && ++ !strncmp(elem->name, path, len)) ++ break; ++ elem = elem->next; ++ } ++ if (path_end <= ep || !elem) ++ return elem; ++ while (*ep == '/' && ep < path_end) ++ ep++; ++ path = ep; ++ } ++ return NULL; ++} ++ ++static struct tree_entry_list *find_entry(const char *path, ++ const char *path_end) ++{ ++ /* Find tree element, descending from root, that ++ * corresponds to the named path, lazily expanding ++ * the tree if possible. ++ */ ++ if (path == path_end) { ++ /* Special. This is the root level */ ++ return &root_entry; ++ } ++ return find_entry_0(&root_entry, path, path_end); ++} ++ ++static void show_entry_name(struct tree_entry_list *e) ++{ ++ /* This is yucky. The root level is there for ++ * our convenience but we really want to do a ++ * forest. ++ */ ++ if (e->parent && e->parent != &root_entry) { ++ show_entry_name(e->parent); ++ putchar('/'); ++ } ++ printf("%s", e->name); ++} ++ ++static const char *entry_type(struct tree_entry_list *e) ++{ ++ return (e->directory ? "tree" : "blob"); ++} ++ ++static const char *entry_hex(struct tree_entry_list *e) ++{ ++ return sha1_to_hex(e->directory ++ ? e->item.tree->object.sha1 ++ : e->item.blob->object.sha1); ++} ++ ++/* forward declaration for mutually recursive routines */ ++static int show_entry(struct tree_entry_list *, int); ++ ++static int show_children(struct tree_entry_list *e, int level) ++{ ++ if (prepare_children(e)) ++ die("internal error: ls-tree show_children called with non tree"); ++ e = e->item.tree->entries; ++ while (e) { ++ show_entry(e, level); ++ e = e->next; ++ } ++ return 0; ++} ++ ++static int show_entry(struct tree_entry_list *e, int level) ++{ ++ int err = 0; ++ ++ if (e != &root_entry) { ++ printf("%06o %s %s ", e->mode, entry_type(e), ++ entry_hex(e)); ++ show_entry_name(e); ++ putchar(line_termination); ++ } ++ ++ if (e->directory) { ++ /* If this is a directory, we have the following cases: ++ * (1) This is the top-level request (explicit path from the ++ * command line, or "root" if there is no command line). ++ * a. Without any flag. We show direct children. We do not ++ * recurse into them. ++ * b. With -r. We do recurse into children. ++ * c. With -d. We do not recurse into children. ++ * (2) We came here because our caller is either (1-a) or ++ * (1-b). ++ * a. Without any flag. We do not show our children (which ++ * are grandchildren for the original request). ++ * b. With -r. We continue to recurse into our children. ++ * c. With -d. We should not have come here to begin with. ++ */ ++ if (level == 0 && !(ls_options & LS_TREE_ONLY)) ++ /* case (1)-a and (1)-b */ ++ err = err | show_children(e, level+1); ++ else if (level && ls_options & LS_RECURSIVE) ++ /* case (2)-b */ ++ err = err | show_children(e, level+1); ++ } ++ return err; ++} ++ ++static int list_one(const char *path, const char *path_end) ++{ ++ int err = 0; ++ struct tree_entry_list *e = find_entry(path, path_end); ++ if (!e) { ++ /* traditionally ls-tree does not complain about ++ * missing path. We may change this later to match ++ * what "/bin/ls -a" does, which is to complain. ++ */ ++ return err; ++ } ++ err = err | show_entry(e, 0); ++ return err; ++} ++ ++static int list(char **path) ++{ ++ int i; ++ int err = 0; ++ for (i = 0; path[i]; i++) { ++ int len = strlen(path[i]); ++ while (0 <= len && path[i][len] == '/') ++ len--; ++ err = err | list_one(path[i], path[i] + len); ++ } ++ return err; ++} ++ ++static const char *ls_tree_usage = ++ "git-ls-tree [-d] [-r] [-z] <tree-ish> [path...]"; ++ ++int main(int argc, char **argv) ++{ ++ static char *path0[] = { "", NULL }; ++ char **path; ++ unsigned char sha1[20]; ++ ++ while (1 < argc && argv[1][0] == '-') { ++ switch (argv[1][1]) { ++ case 'z': ++ line_termination = 0; ++ break; ++ case 'r': ++ ls_options |= LS_RECURSIVE; ++ break; ++ case 'd': ++ ls_options |= LS_TREE_ONLY; ++ break; ++ default: ++ usage(ls_tree_usage); ++ } ++ argc--; argv++; ++ } ++ ++ if (argc < 2) ++ usage(ls_tree_usage); ++ if (get_sha1(argv[1], sha1) < 0) ++ usage(ls_tree_usage); ++ ++ path = (argc == 2) ? path0 : (argv + 2); ++ prepare_root(sha1); ++ if (list(path) < 0) ++ die("list failed"); ++ return 0; ++} +diff --git a/t/t3100-ls-tree-restrict.sh b/t/t3100-ls-tree-restrict.sh +--- a/t/t3100-ls-tree-restrict.sh ++++ b/t/t3100-ls-tree-restrict.sh +@@ -74,8 +74,8 @@ test_expect_success \ + 'ls-tree filtered' \ + 'git-ls-tree $tree path1 path0 >current && + cat >expected <<\EOF && +-100644 blob X path0 + 120000 blob X path1 ++100644 blob X path0 + EOF + test_output' + +@@ -85,7 +85,6 @@ test_expect_success \ + cat >expected <<\EOF && + 040000 tree X path2 + 040000 tree X path2/baz +-100644 blob X path2/baz/b + 120000 blob X path2/bazbo + 100644 blob X path2/foo + EOF +diff --git a/tree.c b/tree.c +--- a/tree.c ++++ b/tree.c +@@ -133,7 +133,7 @@ int parse_tree_buffer(struct tree *item, + } + if (obj) + add_ref(&item->object, obj); +- ++ entry->parent = NULL; /* needs to be filled by the user */ + *list_p = entry; + list_p = &entry->next; + } +diff --git a/tree.h b/tree.h +--- a/tree.h ++++ b/tree.h +@@ -16,6 +16,7 @@ struct tree_entry_list { + struct tree *tree; + struct blob *blob; + } item; ++ struct tree_entry_list *parent; + }; + + struct tree { diff --git a/t/t4100/t-apply-4.expect b/t/t4100/t-apply-4.expect new file mode 100644 index 0000000000..1ec028b3d0 --- /dev/null +++ b/t/t4100/t-apply-4.expect @@ -0,0 +1,5 @@ + t/t0000-basic.sh | 0 + t/test-lib.sh | 0 + 2 files changed, 0 insertions(+), 0 deletions(-) + mode change 100644 => 100755 t/t0000-basic.sh + mode change 100644 => 100755 t/test-lib.sh diff --git a/t/t4100/t-apply-4.patch b/t/t4100/t-apply-4.patch new file mode 100644 index 0000000000..4a56ab5cf4 --- /dev/null +++ b/t/t4100/t-apply-4.patch @@ -0,0 +1,7 @@ +ceede59ea90cebad52ba9c8263fef3fb6ef17593 (from 368f99d57e8ed17243f2e164431449d48bfca2fb) +diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh +old mode 100644 +new mode 100755 +diff --git a/t/test-lib.sh b/t/test-lib.sh +old mode 100644 +new mode 100755 diff --git a/t/t4100/t-apply-5.expect b/t/t4100/t-apply-5.expect new file mode 100644 index 0000000000..b387df15d4 --- /dev/null +++ b/t/t4100/t-apply-5.expect @@ -0,0 +1,19 @@ + Documentation/git-rpull.txt | 50 ------------------- + Documentation/git-rpush.txt | 30 ------------ + Documentation/git-ssh-pull.txt | 50 +++++++++++++++++++ + Documentation/git-ssh-push.txt | 30 ++++++++++++ + Documentation/git.txt | 6 +- + Makefile | 6 +- + rpull.c | 83 -------------------------------- + rpush.c | 104 ---------------------------------------- + ssh-pull.c | 83 ++++++++++++++++++++++++++++++++ + ssh-push.c | 104 ++++++++++++++++++++++++++++++++++++++++ + 10 files changed, 273 insertions(+), 273 deletions(-) + delete Documentation/git-rpull.txt + delete Documentation/git-rpush.txt + create Documentation/git-ssh-pull.txt + create Documentation/git-ssh-push.txt + delete rpull.c + delete rpush.c + create ssh-pull.c + create ssh-push.c diff --git a/t/t4100/t-apply-5.patch b/t/t4100/t-apply-5.patch new file mode 100644 index 0000000000..de11623d1b --- /dev/null +++ b/t/t4100/t-apply-5.patch @@ -0,0 +1,612 @@ +diff a/Documentation/git-rpull.txt b/Documentation/git-rpull.txt +--- a/Documentation/git-rpull.txt ++++ /dev/null +@@ -1,50 +0,0 @@ +-git-rpull(1) +-============ +-v0.1, May 2005 +- +-NAME +----- +-git-rpull - Pulls from a remote repository over ssh connection +- +- +- +-SYNOPSIS +--------- +-'git-rpull' [-c] [-t] [-a] [-d] [-v] [--recover] commit-id url +- +-DESCRIPTION +------------ +-Pulls from a remote repository over ssh connection, invoking git-rpush on +-the other end. +- +-OPTIONS +-------- +--c:: +- Get the commit objects. +--t:: +- Get trees associated with the commit objects. +--a:: +- Get all the objects. +--d:: +- Do not check for delta base objects (use this option +- only when you know the remote repository is not +- deltified). +---recover:: +- Check dependency of deltified object more carefully than +- usual, to recover after earlier pull that was interrupted. +--v:: +- Report what is downloaded. +- +- +-Author +------- +-Written by Linus Torvalds <torvalds@osdl.org> +- +-Documentation +--------------- +-Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. +- +-GIT +---- +-Part of the link:git.html[git] suite +- +diff a/Documentation/git-rpush.txt b/Documentation/git-rpush.txt +--- a/Documentation/git-rpush.txt ++++ /dev/null +@@ -1,30 +0,0 @@ +-git-rpush(1) +-============ +-v0.1, May 2005 +- +-NAME +----- +-git-rpush - Helper "server-side" program used by git-rpull +- +- +-SYNOPSIS +--------- +-'git-rpush' +- +-DESCRIPTION +------------ +-Helper "server-side" program used by git-rpull. +- +- +-Author +------- +-Written by Linus Torvalds <torvalds@osdl.org> +- +-Documentation +--------------- +-Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. +- +-GIT +---- +-Part of the link:git.html[git] suite +- +diff a/Documentation/git-ssh-pull.txt b/Documentation/git-ssh-pull.txt +--- /dev/null ++++ b/Documentation/git-ssh-pull.txt +@@ -0,0 +1,50 @@ ++git-ssh-pull(1) ++=============== ++v0.1, May 2005 ++ ++NAME ++---- ++git-ssh-pull - Pulls from a remote repository over ssh connection ++ ++ ++ ++SYNOPSIS ++-------- ++'git-ssh-pull' [-c] [-t] [-a] [-d] [-v] [--recover] commit-id url ++ ++DESCRIPTION ++----------- ++Pulls from a remote repository over ssh connection, invoking git-ssh-push ++on the other end. ++ ++OPTIONS ++------- ++-c:: ++ Get the commit objects. ++-t:: ++ Get trees associated with the commit objects. ++-a:: ++ Get all the objects. ++-d:: ++ Do not check for delta base objects (use this option ++ only when you know the remote repository is not ++ deltified). ++--recover:: ++ Check dependency of deltified object more carefully than ++ usual, to recover after earlier pull that was interrupted. ++-v:: ++ Report what is downloaded. ++ ++ ++Author ++------ ++Written by Linus Torvalds <torvalds@osdl.org> ++ ++Documentation ++-------------- ++Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. ++ ++GIT ++--- ++Part of the link:git.html[git] suite ++ +diff a/Documentation/git-ssh-push.txt b/Documentation/git-ssh-push.txt +--- /dev/null ++++ b/Documentation/git-ssh-push.txt +@@ -0,0 +1,30 @@ ++git-ssh-push(1) ++=============== ++v0.1, May 2005 ++ ++NAME ++---- ++git-ssh-push - Helper "server-side" program used by git-ssh-pull ++ ++ ++SYNOPSIS ++-------- ++'git-ssh-push' ++ ++DESCRIPTION ++----------- ++Helper "server-side" program used by git-ssh-pull. ++ ++ ++Author ++------ ++Written by Linus Torvalds <torvalds@osdl.org> ++ ++Documentation ++-------------- ++Documentation by David Greaves, Junio C Hamano and the git-list <git@vger.kernel.org>. ++ ++GIT ++--- ++Part of the link:git.html[git] suite ++ +diff a/Documentation/git.txt b/Documentation/git.txt +--- a/Documentation/git.txt ++++ b/Documentation/git.txt +@@ -148,7 +148,7 @@ link:git-resolve-script.html[git-resolve + link:git-tag-script.html[git-tag-script]:: + An example script to create a tag object signed with GPG + +-link:git-rpull.html[git-rpull]:: ++link:git-ssh-pull.html[git-ssh-pull]:: + Pulls from a remote repository over ssh connection + + Interogators: +@@ -156,8 +156,8 @@ Interogators: + link:git-diff-helper.html[git-diff-helper]:: + Generates patch format output for git-diff-* + +-link:git-rpush.html[git-rpush]:: +- Helper "server-side" program used by git-rpull ++link:git-ssh-push.html[git-ssh-push]:: ++ Helper "server-side" program used by git-ssh-pull + + + +diff a/Makefile b/Makefile +--- a/Makefile ++++ b/Makefile +@@ -30,7 +30,7 @@ PROG= git-update-cache git-diff-files + git-checkout-cache git-diff-tree git-rev-tree git-ls-files \ + git-check-files git-ls-tree git-merge-base git-merge-cache \ + git-unpack-file git-export git-diff-cache git-convert-cache \ +- git-http-pull git-rpush git-rpull git-rev-list git-mktag \ ++ git-http-pull git-ssh-push git-ssh-pull git-rev-list git-mktag \ + git-diff-helper git-tar-tree git-local-pull git-write-blob \ + git-get-tar-commit-id git-mkdelta git-apply git-stripspace + +@@ -105,8 +105,8 @@ git-diff-cache: diff-cache.c + git-convert-cache: convert-cache.c + git-http-pull: http-pull.c pull.c + git-local-pull: local-pull.c pull.c +-git-rpush: rsh.c +-git-rpull: rsh.c pull.c ++git-ssh-push: rsh.c ++git-ssh-pull: rsh.c pull.c + git-rev-list: rev-list.c + git-mktag: mktag.c + git-diff-helper: diff-helper.c +diff a/rpull.c b/rpull.c +--- a/rpull.c ++++ /dev/null +@@ -1,83 +0,0 @@ +-#include "cache.h" +-#include "commit.h" +-#include "rsh.h" +-#include "pull.h" +- +-static int fd_in; +-static int fd_out; +- +-static unsigned char remote_version = 0; +-static unsigned char local_version = 1; +- +-int fetch(unsigned char *sha1) +-{ +- int ret; +- signed char remote; +- char type = 'o'; +- if (has_sha1_file(sha1)) +- return 0; +- write(fd_out, &type, 1); +- write(fd_out, sha1, 20); +- if (read(fd_in, &remote, 1) < 1) +- return -1; +- if (remote < 0) +- return remote; +- ret = write_sha1_from_fd(sha1, fd_in); +- if (!ret) +- pull_say("got %s\n", sha1_to_hex(sha1)); +- return ret; +-} +- +-int get_version(void) +-{ +- char type = 'v'; +- write(fd_out, &type, 1); +- write(fd_out, &local_version, 1); +- if (read(fd_in, &remote_version, 1) < 1) { +- return error("Couldn't read version from remote end"); +- } +- return 0; +-} +- +-int main(int argc, char **argv) +-{ +- char *commit_id; +- char *url; +- int arg = 1; +- +- while (arg < argc && argv[arg][0] == '-') { +- if (argv[arg][1] == 't') { +- get_tree = 1; +- } else if (argv[arg][1] == 'c') { +- get_history = 1; +- } else if (argv[arg][1] == 'd') { +- get_delta = 0; +- } else if (!strcmp(argv[arg], "--recover")) { +- get_delta = 2; +- } else if (argv[arg][1] == 'a') { +- get_all = 1; +- get_tree = 1; +- get_history = 1; +- } else if (argv[arg][1] == 'v') { +- get_verbosely = 1; +- } +- arg++; +- } +- if (argc < arg + 2) { +- usage("git-rpull [-c] [-t] [-a] [-v] [-d] [--recover] commit-id url"); +- return 1; +- } +- commit_id = argv[arg]; +- url = argv[arg + 1]; +- +- if (setup_connection(&fd_in, &fd_out, "git-rpush", url, arg, argv + 1)) +- return 1; +- +- if (get_version()) +- return 1; +- +- if (pull(commit_id)) +- return 1; +- +- return 0; +-} +diff a/rpush.c b/rpush.c +--- a/rpush.c ++++ /dev/null +@@ -1,104 +0,0 @@ +-#include "cache.h" +-#include "rsh.h" +-#include <sys/socket.h> +-#include <errno.h> +- +-unsigned char local_version = 1; +-unsigned char remote_version = 0; +- +-int serve_object(int fd_in, int fd_out) { +- ssize_t size; +- int posn = 0; +- char sha1[20]; +- unsigned long objsize; +- void *buf; +- signed char remote; +- do { +- size = read(fd_in, sha1 + posn, 20 - posn); +- if (size < 0) { +- perror("git-rpush: read "); +- return -1; +- } +- if (!size) +- return -1; +- posn += size; +- } while (posn < 20); +- +- /* fprintf(stderr, "Serving %s\n", sha1_to_hex(sha1)); */ +- remote = 0; +- +- buf = map_sha1_file(sha1, &objsize); +- +- if (!buf) { +- fprintf(stderr, "git-rpush: could not find %s\n", +- sha1_to_hex(sha1)); +- remote = -1; +- } +- +- write(fd_out, &remote, 1); +- +- if (remote < 0) +- return 0; +- +- posn = 0; +- do { +- size = write(fd_out, buf + posn, objsize - posn); +- if (size <= 0) { +- if (!size) { +- fprintf(stderr, "git-rpush: write closed"); +- } else { +- perror("git-rpush: write "); +- } +- return -1; +- } +- posn += size; +- } while (posn < objsize); +- return 0; +-} +- +-int serve_version(int fd_in, int fd_out) +-{ +- if (read(fd_in, &remote_version, 1) < 1) +- return -1; +- write(fd_out, &local_version, 1); +- return 0; +-} +- +-void service(int fd_in, int fd_out) { +- char type; +- int retval; +- do { +- retval = read(fd_in, &type, 1); +- if (retval < 1) { +- if (retval < 0) +- perror("rpush: read "); +- return; +- } +- if (type == 'v' && serve_version(fd_in, fd_out)) +- return; +- if (type == 'o' && serve_object(fd_in, fd_out)) +- return; +- } while (1); +-} +- +-int main(int argc, char **argv) +-{ +- int arg = 1; +- char *commit_id; +- char *url; +- int fd_in, fd_out; +- while (arg < argc && argv[arg][0] == '-') { +- arg++; +- } +- if (argc < arg + 2) { +- usage("git-rpush [-c] [-t] [-a] commit-id url"); +- return 1; +- } +- commit_id = argv[arg]; +- url = argv[arg + 1]; +- if (setup_connection(&fd_in, &fd_out, "git-rpull", url, arg, argv + 1)) +- return 1; +- +- service(fd_in, fd_out); +- return 0; +-} +diff a/ssh-pull.c b/ssh-pull.c +--- /dev/null ++++ b/ssh-pull.c +@@ -0,0 +1,83 @@ ++#include "cache.h" ++#include "commit.h" ++#include "rsh.h" ++#include "pull.h" ++ ++static int fd_in; ++static int fd_out; ++ ++static unsigned char remote_version = 0; ++static unsigned char local_version = 1; ++ ++int fetch(unsigned char *sha1) ++{ ++ int ret; ++ signed char remote; ++ char type = 'o'; ++ if (has_sha1_file(sha1)) ++ return 0; ++ write(fd_out, &type, 1); ++ write(fd_out, sha1, 20); ++ if (read(fd_in, &remote, 1) < 1) ++ return -1; ++ if (remote < 0) ++ return remote; ++ ret = write_sha1_from_fd(sha1, fd_in); ++ if (!ret) ++ pull_say("got %s\n", sha1_to_hex(sha1)); ++ return ret; ++} ++ ++int get_version(void) ++{ ++ char type = 'v'; ++ write(fd_out, &type, 1); ++ write(fd_out, &local_version, 1); ++ if (read(fd_in, &remote_version, 1) < 1) { ++ return error("Couldn't read version from remote end"); ++ } ++ return 0; ++} ++ ++int main(int argc, char **argv) ++{ ++ char *commit_id; ++ char *url; ++ int arg = 1; ++ ++ while (arg < argc && argv[arg][0] == '-') { ++ if (argv[arg][1] == 't') { ++ get_tree = 1; ++ } else if (argv[arg][1] == 'c') { ++ get_history = 1; ++ } else if (argv[arg][1] == 'd') { ++ get_delta = 0; ++ } else if (!strcmp(argv[arg], "--recover")) { ++ get_delta = 2; ++ } else if (argv[arg][1] == 'a') { ++ get_all = 1; ++ get_tree = 1; ++ get_history = 1; ++ } else if (argv[arg][1] == 'v') { ++ get_verbosely = 1; ++ } ++ arg++; ++ } ++ if (argc < arg + 2) { ++ usage("git-ssh-pull [-c] [-t] [-a] [-v] [-d] [--recover] commit-id url"); ++ return 1; ++ } ++ commit_id = argv[arg]; ++ url = argv[arg + 1]; ++ ++ if (setup_connection(&fd_in, &fd_out, "git-ssh-push", url, arg, argv + 1)) ++ return 1; ++ ++ if (get_version()) ++ return 1; ++ ++ if (pull(commit_id)) ++ return 1; ++ ++ return 0; ++} +diff a/ssh-push.c b/ssh-push.c +--- /dev/null ++++ b/ssh-push.c +@@ -0,0 +1,104 @@ ++#include "cache.h" ++#include "rsh.h" ++#include <sys/socket.h> ++#include <errno.h> ++ ++unsigned char local_version = 1; ++unsigned char remote_version = 0; ++ ++int serve_object(int fd_in, int fd_out) { ++ ssize_t size; ++ int posn = 0; ++ char sha1[20]; ++ unsigned long objsize; ++ void *buf; ++ signed char remote; ++ do { ++ size = read(fd_in, sha1 + posn, 20 - posn); ++ if (size < 0) { ++ perror("git-ssh-push: read "); ++ return -1; ++ } ++ if (!size) ++ return -1; ++ posn += size; ++ } while (posn < 20); ++ ++ /* fprintf(stderr, "Serving %s\n", sha1_to_hex(sha1)); */ ++ remote = 0; ++ ++ buf = map_sha1_file(sha1, &objsize); ++ ++ if (!buf) { ++ fprintf(stderr, "git-ssh-push: could not find %s\n", ++ sha1_to_hex(sha1)); ++ remote = -1; ++ } ++ ++ write(fd_out, &remote, 1); ++ ++ if (remote < 0) ++ return 0; ++ ++ posn = 0; ++ do { ++ size = write(fd_out, buf + posn, objsize - posn); ++ if (size <= 0) { ++ if (!size) { ++ fprintf(stderr, "git-ssh-push: write closed"); ++ } else { ++ perror("git-ssh-push: write "); ++ } ++ return -1; ++ } ++ posn += size; ++ } while (posn < objsize); ++ return 0; ++} ++ ++int serve_version(int fd_in, int fd_out) ++{ ++ if (read(fd_in, &remote_version, 1) < 1) ++ return -1; ++ write(fd_out, &local_version, 1); ++ return 0; ++} ++ ++void service(int fd_in, int fd_out) { ++ char type; ++ int retval; ++ do { ++ retval = read(fd_in, &type, 1); ++ if (retval < 1) { ++ if (retval < 0) ++ perror("git-ssh-push: read "); ++ return; ++ } ++ if (type == 'v' && serve_version(fd_in, fd_out)) ++ return; ++ if (type == 'o' && serve_object(fd_in, fd_out)) ++ return; ++ } while (1); ++} ++ ++int main(int argc, char **argv) ++{ ++ int arg = 1; ++ char *commit_id; ++ char *url; ++ int fd_in, fd_out; ++ while (arg < argc && argv[arg][0] == '-') { ++ arg++; ++ } ++ if (argc < arg + 2) { ++ usage("git-ssh-push [-c] [-t] [-a] commit-id url"); ++ return 1; ++ } ++ commit_id = argv[arg]; ++ url = argv[arg + 1]; ++ if (setup_connection(&fd_in, &fd_out, "git-ssh-pull", url, arg, argv + 1)) ++ return 1; ++ ++ service(fd_in, fd_out); ++ return 0; ++} diff --git a/t/t4100/t-apply-6.expect b/t/t4100/t-apply-6.expect new file mode 100644 index 0000000000..1c343d459e --- /dev/null +++ b/t/t4100/t-apply-6.expect @@ -0,0 +1,5 @@ + Makefile | 2 +- + git-fetch-script | 41 +++++++++++++++++++++++++++++++++++++++++ + git-pull-script | 34 +--------------------------------- + 3 files changed, 43 insertions(+), 34 deletions(-) + create git-fetch-script diff --git a/t/t4100/t-apply-6.patch b/t/t4100/t-apply-6.patch new file mode 100644 index 0000000000..d9753637fc --- /dev/null +++ b/t/t4100/t-apply-6.patch @@ -0,0 +1,101 @@ +diff a/Makefile b/Makefile +--- a/Makefile ++++ b/Makefile +@@ -20,7 +20,7 @@ INSTALL=install + + SCRIPTS=git-apply-patch-script git-merge-one-file-script git-prune-script \ + git-pull-script git-tag-script git-resolve-script git-whatchanged \ +- git-deltafy-script ++ git-deltafy-script git-fetch-script + + PROG= git-update-cache git-diff-files git-init-db git-write-tree \ + git-read-tree git-commit-tree git-cat-file git-fsck-cache \ +diff a/git-fetch-script b/git-fetch-script +--- /dev/null ++++ b/git-fetch-script +@@ -0,0 +1,41 @@ ++#!/bin/sh ++# ++merge_repo=$1 ++merge_name=${2:-HEAD} ++ ++: ${GIT_DIR=.git} ++: ${GIT_OBJECT_DIRECTORY="${SHA1_FILE_DIRECTORY-"$GIT_DIR/objects"}"} ++ ++download_one () { ++ # remote_path="$1" local_file="$2" ++ case "$1" in ++ http://*) ++ wget -q -O "$2" "$1" ;; ++ /*) ++ test -f "$1" && cat >"$2" "$1" ;; ++ *) ++ rsync -L "$1" "$2" ;; ++ esac ++} ++ ++download_objects () { ++ # remote_repo="$1" head_sha1="$2" ++ case "$1" in ++ http://*) ++ git-http-pull -a "$2" "$1/" ++ ;; ++ /*) ++ git-local-pull -l -a "$2" "$1/" ++ ;; ++ *) ++ rsync -avz --ignore-existing \ ++ "$1/objects/." "$GIT_OBJECT_DIRECTORY"/. ++ ;; ++ esac ++} ++ ++echo "Getting remote $merge_name" ++download_one "$merge_repo/$merge_name" "$GIT_DIR"/MERGE_HEAD ++ ++echo "Getting object database" ++download_objects "$merge_repo" "$(cat "$GIT_DIR"/MERGE_HEAD)" +diff a/git-pull-script b/git-pull-script +--- a/git-pull-script ++++ b/git-pull-script +@@ -6,39 +6,7 @@ merge_name=${2:-HEAD} + : ${GIT_DIR=.git} + : ${GIT_OBJECT_DIRECTORY="${SHA1_FILE_DIRECTORY-"$GIT_DIR/objects"}"} + +-download_one () { +- # remote_path="$1" local_file="$2" +- case "$1" in +- http://*) +- wget -q -O "$2" "$1" ;; +- /*) +- test -f "$1" && cat >"$2" "$1" ;; +- *) +- rsync -L "$1" "$2" ;; +- esac +-} +- +-download_objects () { +- # remote_repo="$1" head_sha1="$2" +- case "$1" in +- http://*) +- git-http-pull -a "$2" "$1/" +- ;; +- /*) +- git-local-pull -l -a "$2" "$1/" +- ;; +- *) +- rsync -avz --ignore-existing \ +- "$1/objects/." "$GIT_OBJECT_DIRECTORY"/. +- ;; +- esac +-} +- +-echo "Getting remote $merge_name" +-download_one "$merge_repo/$merge_name" "$GIT_DIR"/MERGE_HEAD +- +-echo "Getting object database" +-download_objects "$merge_repo" "$(cat "$GIT_DIR"/MERGE_HEAD)" ++git-fetch-script "$merge_repo" "$merge_name" + + git-resolve-script \ + "$(cat "$GIT_DIR"/HEAD)" \ diff --git a/t/t4100/t-apply-7.expect b/t/t4100/t-apply-7.expect new file mode 100644 index 0000000000..1283164d99 --- /dev/null +++ b/t/t4100/t-apply-7.expect @@ -0,0 +1,6 @@ + Documentation/git-ls-tree.txt | 20 +- + ls-tree.c | 333 +++++++++++++++++++++++------------------ + t/t3100-ls-tree-restrict.sh | 3 + tree.c | 2 + tree.h | 1 + 5 files changed, 199 insertions(+), 160 deletions(-) diff --git a/t/t4100/t-apply-7.patch b/t/t4100/t-apply-7.patch new file mode 100644 index 0000000000..07c6589e74 --- /dev/null +++ b/t/t4100/t-apply-7.patch @@ -0,0 +1,494 @@ +diff a/Documentation/git-ls-tree.txt b/Documentation/git-ls-tree.txt +--- a/Documentation/git-ls-tree.txt ++++ b/Documentation/git-ls-tree.txt +@@ -4,23 +4,26 @@ v0.1, May 2005 + + NAME + ---- +-git-ls-tree - Displays a tree object in human readable form ++git-ls-tree - Lists the contents of a tree object. + + + SYNOPSIS + -------- +-'git-ls-tree' [-r] [-z] <tree-ish> [paths...] ++'git-ls-tree' [-d] [-r] [-z] <tree-ish> [paths...] + + DESCRIPTION + ----------- +-Converts the tree object to a human readable (and script processable) +-form. ++Lists the contents of a tree object, like what "/bin/ls -a" does ++in the current working directory. + + OPTIONS + ------- + <tree-ish>:: + Id of a tree. + ++-d:: ++ show only the named tree entry itself, not its children ++ + -r:: + recurse into sub-trees + +@@ -28,18 +31,19 @@ OPTIONS + \0 line termination on output + + paths:: +- Optionally, restrict the output of git-ls-tree to specific +- paths. Directories will only list their tree blob ids. +- Implies -r. ++ When paths are given, shows them. Otherwise implicitly ++ uses the root level of the tree as the sole path argument. ++ + + Output Format + ------------- +- <mode>\t <type>\t <object>\t <file> ++ <mode> SP <type> SP <object> TAB <file> + + + Author + ------ + Written by Linus Torvalds <torvalds@osdl.org> ++Completely rewritten from scratch by Junio C Hamano <junkio@cox.net> + + Documentation + -------------- +diff a/ls-tree.c b/ls-tree.c +--- a/ls-tree.c ++++ b/ls-tree.c +@@ -4,188 +4,217 @@ + * Copyright (C) Linus Torvalds, 2005 + */ + #include "cache.h" ++#include "blob.h" ++#include "tree.h" + + static int line_termination = '\n'; +-static int recursive = 0; ++#define LS_RECURSIVE 1 ++#define LS_TREE_ONLY 2 ++static int ls_options = 0; + +-struct path_prefix { +- struct path_prefix *prev; +- const char *name; +-}; +- +-#define DEBUG(fmt, ...) +- +-static int string_path_prefix(char *buff, size_t blen, struct path_prefix *prefix) +-{ +- int len = 0; +- if (prefix) { +- if (prefix->prev) { +- len = string_path_prefix(buff,blen,prefix->prev); +- buff += len; +- blen -= len; +- if (blen > 0) { +- *buff = '/'; +- len++; +- buff++; +- blen--; +- } +- } +- strncpy(buff,prefix->name,blen); +- return len + strlen(prefix->name); +- } ++static struct tree_entry_list root_entry; + +- return 0; ++static void prepare_root(unsigned char *sha1) ++{ ++ unsigned char rsha[20]; ++ unsigned long size; ++ void *buf; ++ struct tree *root_tree; ++ ++ buf = read_object_with_reference(sha1, "tree", &size, rsha); ++ free(buf); ++ if (!buf) ++ die("Could not read %s", sha1_to_hex(sha1)); ++ ++ root_tree = lookup_tree(rsha); ++ if (!root_tree) ++ die("Could not read %s", sha1_to_hex(sha1)); ++ ++ /* Prepare a fake entry */ ++ root_entry.directory = 1; ++ root_entry.executable = root_entry.symlink = 0; ++ root_entry.mode = S_IFDIR; ++ root_entry.name = ""; ++ root_entry.item.tree = root_tree; ++ root_entry.parent = NULL; + } + +-static void print_path_prefix(struct path_prefix *prefix) ++static int prepare_children(struct tree_entry_list *elem) + { +- if (prefix) { +- if (prefix->prev) { +- print_path_prefix(prefix->prev); +- putchar('/'); +- } +- fputs(prefix->name, stdout); ++ if (!elem->directory) ++ return -1; ++ if (!elem->item.tree->object.parsed) { ++ struct tree_entry_list *e; ++ if (parse_tree(elem->item.tree)) ++ return -1; ++ /* Set up the parent link */ ++ for (e = elem->item.tree->entries; e; e = e->next) ++ e->parent = elem; + } ++ return 0; + } + +-/* +- * return: +- * -1 if prefix is *not* a subset of path +- * 0 if prefix == path +- * 1 if prefix is a subset of path +- */ +-static int pathcmp(const char *path, struct path_prefix *prefix) +-{ +- char buff[PATH_MAX]; +- int len,slen; ++static struct tree_entry_list *find_entry_0(struct tree_entry_list *elem, ++ const char *path, ++ const char *path_end) ++{ ++ const char *ep; ++ int len; ++ ++ while (path < path_end) { ++ if (prepare_children(elem)) ++ return NULL; + +- if (prefix == NULL) +- return 1; ++ /* In elem->tree->entries, find the one that has name ++ * that matches what is between path and ep. ++ */ ++ elem = elem->item.tree->entries; + +- len = string_path_prefix(buff, sizeof buff, prefix); +- slen = strlen(path); ++ ep = strchr(path, '/'); ++ if (!ep || path_end <= ep) ++ ep = path_end; ++ len = ep - path; ++ ++ while (elem) { ++ if ((strlen(elem->name) == len) && ++ !strncmp(elem->name, path, len)) ++ break; ++ elem = elem->next; ++ } ++ if (path_end <= ep || !elem) ++ return elem; ++ while (*ep == '/' && ep < path_end) ++ ep++; ++ path = ep; ++ } ++ return NULL; ++} + +- if (slen < len) +- return -1; ++static struct tree_entry_list *find_entry(const char *path, ++ const char *path_end) ++{ ++ /* Find tree element, descending from root, that ++ * corresponds to the named path, lazily expanding ++ * the tree if possible. ++ */ ++ if (path == path_end) { ++ /* Special. This is the root level */ ++ return &root_entry; ++ } ++ return find_entry_0(&root_entry, path, path_end); ++} + +- if (strncmp(path,buff,len) == 0) { +- if (slen == len) +- return 0; +- else +- return 1; ++static void show_entry_name(struct tree_entry_list *e) ++{ ++ /* This is yucky. The root level is there for ++ * our convenience but we really want to do a ++ * forest. ++ */ ++ if (e->parent && e->parent != &root_entry) { ++ show_entry_name(e->parent); ++ putchar('/'); + } ++ printf("%s", e->name); ++} + +- return -1; +-} ++static const char *entry_type(struct tree_entry_list *e) ++{ ++ return (e->directory ? "tree" : "blob"); ++} + +-/* +- * match may be NULL, or a *sorted* list of paths +- */ +-static void list_recursive(void *buffer, +- const char *type, +- unsigned long size, +- struct path_prefix *prefix, +- char **match, int matches) +-{ +- struct path_prefix this_prefix; +- this_prefix.prev = prefix; +- +- if (strcmp(type, "tree")) +- die("expected a 'tree' node"); +- +- if (matches) +- recursive = 1; +- +- while (size) { +- int namelen = strlen(buffer)+1; +- void *eltbuf = NULL; +- char elttype[20]; +- unsigned long eltsize; +- unsigned char *sha1 = buffer + namelen; +- char *path = strchr(buffer, ' ') + 1; +- unsigned int mode; +- const char *matched = NULL; +- int mtype = -1; +- int mindex; +- +- if (size < namelen + 20 || sscanf(buffer, "%o", &mode) != 1) +- die("corrupt 'tree' file"); +- buffer = sha1 + 20; +- size -= namelen + 20; +- +- this_prefix.name = path; +- for ( mindex = 0; mindex < matches; mindex++) { +- mtype = pathcmp(match[mindex],&this_prefix); +- if (mtype >= 0) { +- matched = match[mindex]; +- break; +- } +- } ++static const char *entry_hex(struct tree_entry_list *e) ++{ ++ return sha1_to_hex(e->directory ++ ? e->item.tree->object.sha1 ++ : e->item.blob->object.sha1); ++} + +- /* +- * If we're not matching, or if this is an exact match, +- * print out the info +- */ +- if (!matches || (matched != NULL && mtype == 0)) { +- printf("%06o %s %s\t", mode, +- S_ISDIR(mode) ? "tree" : "blob", +- sha1_to_hex(sha1)); +- print_path_prefix(&this_prefix); +- putchar(line_termination); +- } ++/* forward declaration for mutually recursive routines */ ++static int show_entry(struct tree_entry_list *, int); + +- if (! recursive || ! S_ISDIR(mode)) +- continue; ++static int show_children(struct tree_entry_list *e, int level) ++{ ++ if (prepare_children(e)) ++ die("internal error: ls-tree show_children called with non tree"); ++ e = e->item.tree->entries; ++ while (e) { ++ show_entry(e, level); ++ e = e->next; ++ } ++ return 0; ++} + +- if (matches && ! matched) +- continue; ++static int show_entry(struct tree_entry_list *e, int level) ++{ ++ int err = 0; + +- if (! (eltbuf = read_sha1_file(sha1, elttype, &eltsize)) ) { +- error("cannot read %s", sha1_to_hex(sha1)); +- continue; +- } ++ if (e != &root_entry) { ++ printf("%06o %s %s ", e->mode, entry_type(e), ++ entry_hex(e)); ++ show_entry_name(e); ++ putchar(line_termination); ++ } + +- /* If this is an exact directory match, we may have +- * directory files following this path. Match on them. +- * Otherwise, we're at a pach subcomponent, and we need +- * to try to match again. ++ if (e->directory) { ++ /* If this is a directory, we have the following cases: ++ * (1) This is the top-level request (explicit path from the ++ * command line, or "root" if there is no command line). ++ * a. Without any flag. We show direct children. We do not ++ * recurse into them. ++ * b. With -r. We do recurse into children. ++ * c. With -d. We do not recurse into children. ++ * (2) We came here because our caller is either (1-a) or ++ * (1-b). ++ * a. Without any flag. We do not show our children (which ++ * are grandchildren for the original request). ++ * b. With -r. We continue to recurse into our children. ++ * c. With -d. We should not have come here to begin with. + */ +- if (mtype == 0) +- mindex++; +- +- list_recursive(eltbuf, elttype, eltsize, &this_prefix, &match[mindex], matches-mindex); +- free(eltbuf); ++ if (level == 0 && !(ls_options & LS_TREE_ONLY)) ++ /* case (1)-a and (1)-b */ ++ err = err | show_children(e, level+1); ++ else if (level && ls_options & LS_RECURSIVE) ++ /* case (2)-b */ ++ err = err | show_children(e, level+1); + } ++ return err; + } + +-static int qcmp(const void *a, const void *b) ++static int list_one(const char *path, const char *path_end) + { +- return strcmp(*(char **)a, *(char **)b); ++ int err = 0; ++ struct tree_entry_list *e = find_entry(path, path_end); ++ if (!e) { ++ /* traditionally ls-tree does not complain about ++ * missing path. We may change this later to match ++ * what "/bin/ls -a" does, which is to complain. ++ */ ++ return err; ++ } ++ err = err | show_entry(e, 0); ++ return err; + } + +-static int list(unsigned char *sha1,char **path) ++static int list(char **path) + { +- void *buffer; +- unsigned long size; +- int npaths; +- +- for (npaths = 0; path[npaths] != NULL; npaths++) +- ; +- +- qsort(path,npaths,sizeof(char *),qcmp); +- +- buffer = read_object_with_reference(sha1, "tree", &size, NULL); +- if (!buffer) +- die("unable to read sha1 file"); +- list_recursive(buffer, "tree", size, NULL, path, npaths); +- free(buffer); +- return 0; ++ int i; ++ int err = 0; ++ for (i = 0; path[i]; i++) { ++ int len = strlen(path[i]); ++ while (0 <= len && path[i][len] == '/') ++ len--; ++ err = err | list_one(path[i], path[i] + len); ++ } ++ return err; + } + +-static const char *ls_tree_usage = "git-ls-tree [-r] [-z] <key> [paths...]"; ++static const char *ls_tree_usage = ++ "git-ls-tree [-d] [-r] [-z] <tree-ish> [path...]"; + + int main(int argc, char **argv) + { ++ static char *path0[] = { "", NULL }; ++ char **path; + unsigned char sha1[20]; + + while (1 < argc && argv[1][0] == '-') { +@@ -194,7 +223,10 @@ int main(int argc, char **argv) + line_termination = 0; + break; + case 'r': +- recursive = 1; ++ ls_options |= LS_RECURSIVE; ++ break; ++ case 'd': ++ ls_options |= LS_TREE_ONLY; + break; + default: + usage(ls_tree_usage); +@@ -206,7 +238,10 @@ int main(int argc, char **argv) + usage(ls_tree_usage); + if (get_sha1(argv[1], sha1) < 0) + usage(ls_tree_usage); +- if (list(sha1, &argv[2]) < 0) ++ ++ path = (argc == 2) ? path0 : (argv + 2); ++ prepare_root(sha1); ++ if (list(path) < 0) + die("list failed"); + return 0; + } +diff a/t/t3100-ls-tree-restrict.sh b/t/t3100-ls-tree-restrict.sh +--- a/t/t3100-ls-tree-restrict.sh ++++ b/t/t3100-ls-tree-restrict.sh +@@ -74,8 +74,8 @@ test_expect_success \ + 'ls-tree filtered' \ + 'git-ls-tree $tree path1 path0 >current && + cat >expected <<\EOF && +-100644 blob X path0 + 120000 blob X path1 ++100644 blob X path0 + EOF + test_output' + +@@ -85,7 +85,6 @@ test_expect_success \ + cat >expected <<\EOF && + 040000 tree X path2 + 040000 tree X path2/baz +-100644 blob X path2/baz/b + 120000 blob X path2/bazbo + 100644 blob X path2/foo + EOF +diff a/tree.c b/tree.c +--- a/tree.c ++++ b/tree.c +@@ -133,7 +133,7 @@ int parse_tree_buffer(struct tree *item, + } + if (obj) + add_ref(&item->object, obj); +- ++ entry->parent = NULL; /* needs to be filled by the user */ + *list_p = entry; + list_p = &entry->next; + } +diff a/tree.h b/tree.h +--- a/tree.h ++++ b/tree.h +@@ -16,6 +16,7 @@ struct tree_entry_list { + struct tree *tree; + struct blob *blob; + } item; ++ struct tree_entry_list *parent; + }; + + struct tree { diff --git a/t/t4101-apply-nonl.sh b/t/t4101-apply-nonl.sh new file mode 100755 index 0000000000..026fac8c55 --- /dev/null +++ b/t/t4101-apply-nonl.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-apply should handle files with incomplete lines. + +' +. ./test-lib.sh + +# setup + +(echo a; echo b) >frotz.0 +(echo a; echo b; echo c) >frotz.1 +(echo a; echo b | tr -d '\012') >frotz.2 +(echo a; echo c; echo b | tr -d '\012') >frotz.3 + +for i in 0 1 2 3 +do + for j in 0 1 2 3 + do + test $i -eq $j && continue + cat frotz.$i >frotz + test_expect_success \ + "apply diff between $i and $j" \ + "git-apply <../t4101/diff.$i-$j && diff frotz.$j frotz" + done +done + +test_done diff --git a/t/t4101/diff.0-1 b/t/t4101/diff.0-1 new file mode 100644 index 0000000000..1010a88f47 --- /dev/null +++ b/t/t4101/diff.0-1 @@ -0,0 +1,6 @@ +--- a/frotz ++++ b/frotz +@@ -1,2 +1,3 @@ + a + b ++c diff --git a/t/t4101/diff.0-2 b/t/t4101/diff.0-2 new file mode 100644 index 0000000000..36460a243a --- /dev/null +++ b/t/t4101/diff.0-2 @@ -0,0 +1,7 @@ +--- a/frotz ++++ b/frotz +@@ -1,2 +1,2 @@ + a +-b ++b +\ No newline at end of file diff --git a/t/t4101/diff.0-3 b/t/t4101/diff.0-3 new file mode 100644 index 0000000000..b281c43e5b --- /dev/null +++ b/t/t4101/diff.0-3 @@ -0,0 +1,8 @@ +--- a/frotz ++++ b/frotz +@@ -1,2 +1,3 @@ + a +-b ++c ++b +\ No newline at end of file diff --git a/t/t4101/diff.1-0 b/t/t4101/diff.1-0 new file mode 100644 index 0000000000..f0a2e92770 --- /dev/null +++ b/t/t4101/diff.1-0 @@ -0,0 +1,6 @@ +--- a/frotz ++++ b/frotz +@@ -1,3 +1,2 @@ + a + b +-c diff --git a/t/t4101/diff.1-2 b/t/t4101/diff.1-2 new file mode 100644 index 0000000000..2a440a5ce2 --- /dev/null +++ b/t/t4101/diff.1-2 @@ -0,0 +1,8 @@ +--- a/frotz ++++ b/frotz +@@ -1,3 +1,2 @@ + a +-b +-c ++b +\ No newline at end of file diff --git a/t/t4101/diff.1-3 b/t/t4101/diff.1-3 new file mode 100644 index 0000000000..61aff975b6 --- /dev/null +++ b/t/t4101/diff.1-3 @@ -0,0 +1,8 @@ +--- a/frotz ++++ b/frotz +@@ -1,3 +1,3 @@ + a +-b + c ++b +\ No newline at end of file diff --git a/t/t4101/diff.2-0 b/t/t4101/diff.2-0 new file mode 100644 index 0000000000..c2e71ee344 --- /dev/null +++ b/t/t4101/diff.2-0 @@ -0,0 +1,7 @@ +--- a/frotz ++++ b/frotz +@@ -1,2 +1,2 @@ + a +-b +\ No newline at end of file ++b diff --git a/t/t4101/diff.2-1 b/t/t4101/diff.2-1 new file mode 100644 index 0000000000..a66d9fd3a1 --- /dev/null +++ b/t/t4101/diff.2-1 @@ -0,0 +1,8 @@ +--- a/frotz ++++ b/frotz +@@ -1,2 +1,3 @@ + a +-b +\ No newline at end of file ++b ++c diff --git a/t/t4101/diff.2-3 b/t/t4101/diff.2-3 new file mode 100644 index 0000000000..5633c831de --- /dev/null +++ b/t/t4101/diff.2-3 @@ -0,0 +1,7 @@ +--- a/frotz ++++ b/frotz +@@ -1,2 +1,3 @@ + a ++c + b +\ No newline at end of file diff --git a/t/t4101/diff.3-0 b/t/t4101/diff.3-0 new file mode 100644 index 0000000000..10b1a41edf --- /dev/null +++ b/t/t4101/diff.3-0 @@ -0,0 +1,8 @@ +--- a/frotz ++++ b/frotz +@@ -1,3 +1,2 @@ + a +-c +-b +\ No newline at end of file ++b diff --git a/t/t4101/diff.3-1 b/t/t4101/diff.3-1 new file mode 100644 index 0000000000..c799c60fb9 --- /dev/null +++ b/t/t4101/diff.3-1 @@ -0,0 +1,8 @@ +--- a/frotz ++++ b/frotz +@@ -1,3 +1,3 @@ + a ++b + c +-b +\ No newline at end of file diff --git a/t/t4101/diff.3-2 b/t/t4101/diff.3-2 new file mode 100644 index 0000000000..f8d1ba6dc2 --- /dev/null +++ b/t/t4101/diff.3-2 @@ -0,0 +1,7 @@ +--- a/frotz ++++ b/frotz +@@ -1,3 +1,2 @@ + a +-c + b +\ No newline at end of file diff --git a/t/t4102-apply-rename.sh b/t/t4102-apply-rename.sh new file mode 100755 index 0000000000..b4662b0364 --- /dev/null +++ b/t/t4102-apply-rename.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-apply handling copy/rename patch. + +' +. ./test-lib.sh + +# setup + +cat >test-patch <<\EOF +diff --git a/foo b/bar +similarity index 47% +rename from foo +rename to bar +--- a/foo ++++ b/bar +@@ -1 +1 @@ +-This is foo ++This is bar +EOF + +echo 'This is foo' >foo +chmod +x foo + +test_expect_success setup \ + 'git-update-index --add foo' + +test_expect_success apply \ + 'git-apply --index --stat --summary --apply test-patch' + +if [ "$(git config --get core.filemode)" = false ] +then + say 'filemode disabled on the filesystem' +else + test_expect_success validate \ + 'test -f bar && ls -l bar | grep "^-..x......"' +fi + +test_expect_success 'apply reverse' \ + 'git-apply -R --index --stat --summary --apply test-patch && + test "$(cat foo)" = "This is foo"' + +cat >test-patch <<\EOF +diff --git a/foo b/bar +similarity index 47% +copy from foo +copy to bar +--- a/foo ++++ b/bar +@@ -1 +1 @@ +-This is foo ++This is bar +EOF + +test_expect_success 'apply copy' \ + 'git-apply --index --stat --summary --apply test-patch && + test "$(cat bar)" = "This is bar" -a "$(cat foo)" = "This is foo"' + +test_done diff --git a/t/t4103-apply-binary.sh b/t/t4103-apply-binary.sh new file mode 100755 index 0000000000..e2b1124c78 --- /dev/null +++ b/t/t4103-apply-binary.sh @@ -0,0 +1,115 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-apply handling binary patches + +' +. ./test-lib.sh + +# setup + +cat >file1 <<EOF +A quick brown fox jumps over the lazy dog. +A tiny little penguin runs around in circles. +There is a flag with Linux written on it. +A slow black-and-white panda just sits there, +munching on his bamboo. +EOF +cat file1 >file2 +cat file1 >file4 + +git-update-index --add --remove file1 file2 file4 +git-commit -m 'Initial Version' 2>/dev/null + +git-checkout -b binary +tr 'x' '\0' <file1 >file3 +cat file3 >file4 +git-add file2 +tr '\0' 'v' <file3 >file1 +rm -f file2 +git-update-index --add --remove file1 file2 file3 file4 +git-commit -m 'Second Version' + +git-diff-tree -p master binary >B.diff +git-diff-tree -p -C master binary >C.diff + +git-diff-tree -p --binary master binary >BF.diff +git-diff-tree -p --binary -C master binary >CF.diff + +test_expect_success 'stat binary diff -- should not fail.' \ + 'git-checkout master + git-apply --stat --summary B.diff' + +test_expect_success 'stat binary diff (copy) -- should not fail.' \ + 'git-checkout master + git-apply --stat --summary C.diff' + +test_expect_failure 'check binary diff -- should fail.' \ + 'git-checkout master + git-apply --check B.diff' + +test_expect_failure 'check binary diff (copy) -- should fail.' \ + 'git-checkout master + git-apply --check C.diff' + +test_expect_failure 'check incomplete binary diff with replacement -- should fail.' \ + 'git-checkout master + git-apply --check --allow-binary-replacement B.diff' + +test_expect_failure 'check incomplete binary diff with replacement (copy) -- should fail.' \ + 'git-checkout master + git-apply --check --allow-binary-replacement C.diff' + +test_expect_success 'check binary diff with replacement.' \ + 'git-checkout master + git-apply --check --allow-binary-replacement BF.diff' + +test_expect_success 'check binary diff with replacement (copy).' \ + 'git-checkout master + git-apply --check --allow-binary-replacement CF.diff' + +# Now we start applying them. + +do_reset () { + rm -f file? + git-reset --hard + git-checkout -f master +} + +test_expect_failure 'apply binary diff -- should fail.' \ + 'do_reset + git-apply B.diff' + +test_expect_failure 'apply binary diff -- should fail.' \ + 'do_reset + git-apply --index B.diff' + +test_expect_failure 'apply binary diff (copy) -- should fail.' \ + 'do_reset + git-apply C.diff' + +test_expect_failure 'apply binary diff (copy) -- should fail.' \ + 'do_reset + git-apply --index C.diff' + +test_expect_success 'apply binary diff without replacement.' \ + 'do_reset + git-apply BF.diff' + +test_expect_success 'apply binary diff without replacement (copy).' \ + 'do_reset + git-apply CF.diff' + +test_expect_success 'apply binary diff.' \ + 'do_reset + git-apply --allow-binary-replacement --index BF.diff && + test -z "$(git-diff --name-status binary)"' + +test_expect_success 'apply binary diff (copy).' \ + 'do_reset + git-apply --allow-binary-replacement --index CF.diff && + test -z "$(git-diff --name-status binary)"' + +test_done diff --git a/t/t4104-apply-boundary.sh b/t/t4104-apply-boundary.sh new file mode 100755 index 0000000000..2ff800c23f --- /dev/null +++ b/t/t4104-apply-boundary.sh @@ -0,0 +1,115 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-apply boundary tests + +' +. ./test-lib.sh + +L="c d e f g h i j k l m n o p q r s t u v w x" + +test_expect_success setup ' + for i in b '"$L"' y + do + echo $i + done >victim && + cat victim >original && + git update-index --add victim && + + : add to the head + for i in a b '"$L"' y + do + echo $i + done >victim && + cat victim >add-a-expect && + git diff victim >add-a-patch.with && + git diff --unified=0 >add-a-patch.without && + + : modify at the head + for i in a '"$L"' y + do + echo $i + done >victim && + cat victim >mod-a-expect && + git diff victim >mod-a-patch.with && + git diff --unified=0 >mod-a-patch.without && + + : remove from the head + for i in '"$L"' y + do + echo $i + done >victim && + cat victim >del-a-expect && + git diff victim >del-a-patch.with + git diff --unified=0 >del-a-patch.without && + + : add to the tail + for i in b '"$L"' y z + do + echo $i + done >victim && + cat victim >add-z-expect && + git diff victim >add-z-patch.with && + git diff --unified=0 >add-z-patch.without && + + : modify at the tail + for i in a '"$L"' y + do + echo $i + done >victim && + cat victim >mod-z-expect && + git diff victim >mod-z-patch.with && + git diff --unified=0 >mod-z-patch.without && + + : remove from the tail + for i in b '"$L"' + do + echo $i + done >victim && + cat victim >del-z-expect && + git diff victim >del-z-patch.with + git diff --unified=0 >del-z-patch.without && + + : done +' + +for with in with without +do + case "$with" in + with) u= ;; + without) u='--unidiff-zero ' ;; + esac + for kind in add-a add-z mod-a mod-z del-a del-z + do + test_expect_success "apply $kind-patch $with context" ' + cat original >victim && + git update-index victim && + git apply --index '"$u$kind-patch.$with"' || { + cat '"$kind-patch.$with"' + (exit 1) + } && + diff -u '"$kind"'-expect victim + ' + done +done + +for kind in add-a add-z mod-a mod-z del-a del-z +do + rm -f $kind-ng.without + sed -e "s/^diff --git /diff /" \ + -e '/^index /d' \ + <$kind-patch.without >$kind-ng.without + test_expect_success "apply non-git $kind-patch without context" ' + cat original >victim && + git update-index victim && + git apply --unidiff-zero --index '"$kind-ng.without"' || { + cat '"$kind-ng.without"' + (exit 1) + } && + diff -u '"$kind"'-expect victim + ' +done + +test_done diff --git a/t/t4109-apply-multifrag.sh b/t/t4109-apply-multifrag.sh new file mode 100755 index 0000000000..5988e1ae4c --- /dev/null +++ b/t/t4109-apply-multifrag.sh @@ -0,0 +1,176 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# Copyright (c) 2005 Robert Fitzsimons +# + +test_description='git-apply test patches with multiple fragments. + +' +. ./test-lib.sh + +# setup + +cat > patch1.patch <<\EOF +diff --git a/main.c b/main.c +new file mode 100644 +--- /dev/null ++++ b/main.c +@@ -0,0 +1,23 @@ ++#include <stdio.h> ++ ++int func(int num); ++void print_int(int num); ++ ++int main() { ++ int i; ++ ++ for (i = 0; i < 10; i++) { ++ print_int(func(i)); ++ } ++ ++ return 0; ++} ++ ++int func(int num) { ++ return num * num; ++} ++ ++void print_int(int num) { ++ printf("%d", num); ++} ++ +EOF +cat > patch2.patch <<\EOF +diff --git a/main.c b/main.c +--- a/main.c ++++ b/main.c +@@ -1,7 +1,9 @@ ++#include <stdlib.h> + #include <stdio.h> + + int func(int num); + void print_int(int num); ++void print_ln(); + + int main() { + int i; +@@ -10,6 +12,8 @@ + print_int(func(i)); + } + ++ print_ln(); ++ + return 0; + } + +@@ -21,3 +25,7 @@ + printf("%d", num); + } + ++void print_ln() { ++ printf("\n"); ++} ++ +EOF +cat > patch3.patch <<\EOF +diff --git a/main.c b/main.c +--- a/main.c ++++ b/main.c +@@ -1,9 +1,7 @@ +-#include <stdlib.h> + #include <stdio.h> + + int func(int num); + void print_int(int num); +-void print_ln(); + + int main() { + int i; +@@ -12,8 +10,6 @@ + print_int(func(i)); + } + +- print_ln(); +- + return 0; + } + +@@ -25,7 +21,3 @@ + printf("%d", num); + } + +-void print_ln() { +- printf("\n"); +-} +- +EOF +cat > patch4.patch <<\EOF +diff --git a/main.c b/main.c +--- a/main.c ++++ b/main.c +@@ -1,13 +1,14 @@ + #include <stdio.h> + + int func(int num); +-void print_int(int num); ++int func2(int num); + + int main() { + int i; + + for (i = 0; i < 10; i++) { +- print_int(func(i)); ++ printf("%d", func(i)); ++ printf("%d", func3(i)); + } + + return 0; +@@ -17,7 +18,7 @@ + return num * num; + } + +-void print_int(int num) { +- printf("%d", num); ++int func2(int num) { ++ return num * num * num; + } + +EOF + +test_expect_success "S = git-apply (1)" \ + 'git-apply patch1.patch patch2.patch' +mv main.c main.c.git + +test_expect_success "S = patch (1)" \ + 'cat patch1.patch patch2.patch | patch -p1' + +test_expect_success "S = cmp (1)" \ + 'cmp main.c.git main.c' + +rm -f main.c main.c.git + +test_expect_success "S = git-apply (2)" \ + 'git-apply patch1.patch patch2.patch patch3.patch' +mv main.c main.c.git + +test_expect_success "S = patch (2)" \ + 'cat patch1.patch patch2.patch patch3.patch | patch -p1' + +test_expect_success "S = cmp (2)" \ + 'cmp main.c.git main.c' + +rm -f main.c main.c.git + +test_expect_success "S = git-apply (3)" \ + 'git-apply patch1.patch patch4.patch' +mv main.c main.c.git + +test_expect_success "S = patch (3)" \ + 'cat patch1.patch patch4.patch | patch -p1' + +test_expect_success "S = cmp (3)" \ + 'cmp main.c.git main.c' + +test_done + diff --git a/t/t4110-apply-scan.sh b/t/t4110-apply-scan.sh new file mode 100755 index 0000000000..005f744816 --- /dev/null +++ b/t/t4110-apply-scan.sh @@ -0,0 +1,101 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# Copyright (c) 2005 Robert Fitzsimons +# + +test_description='git-apply test for patches which require scanning forwards and backwards. + +' +. ./test-lib.sh + +# setup + +cat > patch1.patch <<\EOF +diff --git a/new.txt b/new.txt +new file mode 100644 +--- /dev/null ++++ b/new.txt +@@ -0,0 +1,12 @@ ++a1 ++a11 ++a111 ++a1111 ++b1 ++b11 ++b111 ++b1111 ++c1 ++c11 ++c111 ++c1111 +EOF +cat > patch2.patch <<\EOF +diff --git a/new.txt b/new.txt +--- a/new.txt ++++ b/new.txt +@@ -1,7 +1,3 @@ +-a1 +-a11 +-a111 +-a1111 + b1 + b11 + b111 +EOF +cat > patch3.patch <<\EOF +diff --git a/new.txt b/new.txt +--- a/new.txt ++++ b/new.txt +@@ -6,6 +6,10 @@ + b11 + b111 + b1111 ++b2 ++b22 ++b222 ++b2222 + c1 + c11 + c111 +EOF +cat > patch4.patch <<\EOF +diff --git a/new.txt b/new.txt +--- a/new.txt ++++ b/new.txt +@@ -1,3 +1,7 @@ ++a1 ++a11 ++a111 ++a1111 + b1 + b11 + b111 +EOF +cat > patch5.patch <<\EOF +diff --git a/new.txt b/new.txt +--- a/new.txt ++++ b/new.txt +@@ -10,3 +10,7 @@ + c11 + c111 + c1111 ++c2 ++c22 ++c222 ++c2222 +EOF + +test_expect_success "S = git-apply scan" \ + 'git-apply patch1.patch patch2.patch patch3.patch patch4.patch patch5.patch' +mv new.txt apply.txt + +test_expect_success "S = patch scan" \ + 'cat patch1.patch patch2.patch patch3.patch patch4.patch patch5.patch | patch' +mv new.txt patch.txt + +test_expect_success "S = cmp" \ + 'cmp apply.txt patch.txt' + +test_done + diff --git a/t/t4112-apply-renames.sh b/t/t4112-apply-renames.sh new file mode 100755 index 0000000000..69e9603c78 --- /dev/null +++ b/t/t4112-apply-renames.sh @@ -0,0 +1,124 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-apply should not get confused with rename/copy. + +' + +. ./test-lib.sh + +# setup + +mkdir -p klibc/arch/x86_64/include/klibc + +cat >klibc/arch/x86_64/include/klibc/archsetjmp.h <<\EOF +/* + * arch/x86_64/include/klibc/archsetjmp.h + */ + +#ifndef _KLIBC_ARCHSETJMP_H +#define _KLIBC_ARCHSETJMP_H + +struct __jmp_buf { + unsigned long __rbx; + unsigned long __rsp; + unsigned long __rbp; + unsigned long __r12; + unsigned long __r13; + unsigned long __r14; + unsigned long __r15; + unsigned long __rip; +}; + +typedef struct __jmp_buf jmp_buf[1]; + +#endif /* _SETJMP_H */ +EOF + +cat >patch <<\EOF +diff --git a/klibc/arch/x86_64/include/klibc/archsetjmp.h b/include/arch/cris/klibc/archsetjmp.h +similarity index 76% +copy from klibc/arch/x86_64/include/klibc/archsetjmp.h +copy to include/arch/cris/klibc/archsetjmp.h +--- a/klibc/arch/x86_64/include/klibc/archsetjmp.h ++++ b/include/arch/cris/klibc/archsetjmp.h +@@ -1,21 +1,24 @@ + /* +- * arch/x86_64/include/klibc/archsetjmp.h ++ * arch/cris/include/klibc/archsetjmp.h + */ + + #ifndef _KLIBC_ARCHSETJMP_H + #define _KLIBC_ARCHSETJMP_H + + struct __jmp_buf { +- unsigned long __rbx; +- unsigned long __rsp; +- unsigned long __rbp; +- unsigned long __r12; +- unsigned long __r13; +- unsigned long __r14; +- unsigned long __r15; +- unsigned long __rip; ++ unsigned long __r0; ++ unsigned long __r1; ++ unsigned long __r2; ++ unsigned long __r3; ++ unsigned long __r4; ++ unsigned long __r5; ++ unsigned long __r6; ++ unsigned long __r7; ++ unsigned long __r8; ++ unsigned long __sp; ++ unsigned long __srp; + }; + + typedef struct __jmp_buf jmp_buf[1]; + +-#endif /* _SETJMP_H */ ++#endif /* _KLIBC_ARCHSETJMP_H */ +diff --git a/klibc/arch/x86_64/include/klibc/archsetjmp.h b/include/arch/m32r/klibc/archsetjmp.h +similarity index 66% +rename from klibc/arch/x86_64/include/klibc/archsetjmp.h +rename to include/arch/m32r/klibc/archsetjmp.h +--- a/klibc/arch/x86_64/include/klibc/archsetjmp.h ++++ b/include/arch/m32r/klibc/archsetjmp.h +@@ -1,21 +1,21 @@ + /* +- * arch/x86_64/include/klibc/archsetjmp.h ++ * arch/m32r/include/klibc/archsetjmp.h + */ + + #ifndef _KLIBC_ARCHSETJMP_H + #define _KLIBC_ARCHSETJMP_H + + struct __jmp_buf { +- unsigned long __rbx; +- unsigned long __rsp; +- unsigned long __rbp; ++ unsigned long __r8; ++ unsigned long __r9; ++ unsigned long __r10; ++ unsigned long __r11; + unsigned long __r12; + unsigned long __r13; + unsigned long __r14; + unsigned long __r15; +- unsigned long __rip; + }; + + typedef struct __jmp_buf jmp_buf[1]; + +-#endif /* _SETJMP_H */ ++#endif /* _KLIBC_ARCHSETJMP_H */ +EOF + +find klibc -type f -print | xargs git-update-index --add -- + +test_expect_success 'check rename/copy patch' 'git-apply --check patch' + +test_expect_success 'apply rename/copy patch' 'git-apply --index patch' + +test_done diff --git a/t/t4113-apply-ending.sh b/t/t4113-apply-ending.sh new file mode 100755 index 0000000000..7fd0cf62ec --- /dev/null +++ b/t/t4113-apply-ending.sh @@ -0,0 +1,53 @@ +#!/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 diff --git a/t/t4114-apply-typechange.sh b/t/t4114-apply-typechange.sh new file mode 100755 index 0000000000..ca81d72157 --- /dev/null +++ b/t/t4114-apply-typechange.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# +# Copyright (c) 2006 Eric Wong +# + +test_description='git-apply should not get confused with type changes. + +' + +. ./test-lib.sh + +test_expect_success 'setup repository and commits' ' + echo "hello world" > foo && + echo "hi planet" > bar && + git update-index --add foo bar && + git commit -m initial && + git branch initial && + rm -f foo && + ln -s bar foo && + git update-index foo && + git commit -m "foo symlinked to bar" && + git branch foo-symlinked-to-bar && + rm -f foo && + echo "how far is the sun?" > foo && + git update-index foo && + git commit -m "foo back to file" && + git branch foo-back-to-file && + rm -f foo && + git update-index --remove foo && + mkdir foo && + echo "if only I knew" > foo/baz && + git update-index --add foo/baz && + git commit -m "foo becomes a directory" && + git branch "foo-becomes-a-directory" && + echo "hello world" > foo/baz && + git update-index foo/baz && + git commit -m "foo/baz is the original foo" && + git branch foo-baz-renamed-from-foo + ' + +test_expect_success 'file renamed from foo to foo/baz' ' + git checkout -f initial && + git diff-tree -M -p HEAD foo-baz-renamed-from-foo > patch && + git apply --index < patch + ' +test_debug 'cat patch' + + +test_expect_success 'file renamed from foo/baz to foo' ' + git checkout -f foo-baz-renamed-from-foo && + git diff-tree -M -p HEAD initial > patch && + git apply --index < patch + ' +test_debug 'cat patch' + + +test_expect_success 'directory becomes file' ' + git checkout -f foo-becomes-a-directory && + git diff-tree -p HEAD initial > patch && + git apply --index < patch + ' +test_debug 'cat patch' + + +test_expect_success 'file becomes directory' ' + git checkout -f initial && + git diff-tree -p HEAD foo-becomes-a-directory > patch && + git apply --index < patch + ' +test_debug 'cat patch' + + +test_expect_success 'file becomes symlink' ' + git checkout -f initial && + git diff-tree -p HEAD foo-symlinked-to-bar > patch && + git apply --index < patch + ' +test_debug 'cat patch' + + +test_expect_success 'symlink becomes file' ' + git checkout -f foo-symlinked-to-bar && + git diff-tree -p HEAD foo-back-to-file > patch && + git apply --index < patch + ' +test_debug 'cat patch' + + +test_expect_success 'symlink becomes directory' ' + git checkout -f foo-symlinked-to-bar && + git diff-tree -p HEAD foo-becomes-a-directory > patch && + git apply --index < patch + ' +test_debug 'cat patch' + + +test_expect_success 'directory becomes symlink' ' + git checkout -f foo-becomes-a-directory && + git diff-tree -p HEAD foo-symlinked-to-bar > patch && + git apply --index < patch + ' +test_debug 'cat patch' + + +test_done diff --git a/t/t4115-apply-symlink.sh b/t/t4115-apply-symlink.sh new file mode 100755 index 0000000000..d5f2cfb186 --- /dev/null +++ b/t/t4115-apply-symlink.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-apply symlinks and partial files + +' + +. ./test-lib.sh + +test_expect_success setup ' + + ln -s path1/path2/path3/path4/path5 link1 && + git add link? && + git commit -m initial && + + git branch side && + + rm -f link? && + + ln -s htap6 link1 && + git update-index link? && + git commit -m second && + + git diff-tree -p HEAD^ HEAD >patch && + git apply --stat --summary patch + +' + +test_expect_success 'apply symlink patch' ' + + git checkout side && + git apply patch && + git diff-files -p >patched && + diff -u patch patched + +' + +test_expect_success 'apply --index symlink patch' ' + + git checkout -f side && + git apply --index patch && + git diff-index --cached -p HEAD >patched && + diff -u patch patched + +' + +test_done diff --git a/t/t4116-apply-reverse.sh b/t/t4116-apply-reverse.sh new file mode 100755 index 0000000000..aa2c869e0e --- /dev/null +++ b/t/t4116-apply-reverse.sh @@ -0,0 +1,85 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-apply in reverse + +' + +. ./test-lib.sh + +test_expect_success setup ' + + for i in a b c d e f g h i j k l m n; do echo $i; done >file1 && + tr "[ijk]" '\''[\0\1\2]'\'' <file1 >file2 && + + git add file1 file2 && + git commit -m initial && + git tag initial && + + for i in a b c g h i J K L m o n p q; do echo $i; done >file1 && + tr "[mon]" '\''[\0\1\2]'\'' <file1 >file2 && + + git commit -a -m second && + git tag second && + + git diff --binary initial second >patch + +' + +test_expect_success 'apply in forward' ' + + T0=`git rev-parse "second^{tree}"` && + git reset --hard initial && + git apply --index --binary patch && + T1=`git write-tree` && + test "$T0" = "$T1" +' + +test_expect_success 'apply in reverse' ' + + git reset --hard second && + git apply --reverse --binary --index patch && + git diff >diff && + diff -u /dev/null diff + +' + +test_expect_success 'setup separate repository lacking postimage' ' + + git tar-tree initial initial | tar xf - && + ( + cd initial && git init && git add . + ) && + + git tar-tree second second | tar xf - && + ( + cd second && git init && git add . + ) + +' + +test_expect_success 'apply in forward without postimage' ' + + T0=`git rev-parse "second^{tree}"` && + ( + cd initial && + git apply --index --binary ../patch && + T1=`git write-tree` && + test "$T0" = "$T1" + ) +' + +test_expect_success 'apply in reverse without postimage' ' + + T0=`git rev-parse "initial^{tree}"` && + ( + cd second && + git apply --index --binary --reverse ../patch && + T1=`git write-tree` && + test "$T0" = "$T1" + ) +' + +test_done diff --git a/t/t4117-apply-reject.sh b/t/t4117-apply-reject.sh new file mode 100755 index 0000000000..b4de075a3e --- /dev/null +++ b/t/t4117-apply-reject.sh @@ -0,0 +1,157 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-apply with rejects + +' + +. ./test-lib.sh + +test_expect_success setup ' + for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + do + echo $i + done >file1 && + cat file1 >saved.file1 && + git update-index --add file1 && + git commit -m initial && + + for i in 1 2 A B 4 5 6 7 8 9 10 11 12 C 13 14 15 16 17 18 19 20 D 21 + do + echo $i + done >file1 && + git diff >patch.1 && + cat file1 >clean && + + for i in 1 E 2 3 4 5 6 7 8 9 10 11 12 C 13 14 15 16 17 18 19 20 F 21 + do + echo $i + done >expected && + + mv file1 file2 && + git update-index --add --remove file1 file2 && + git diff -M HEAD >patch.2 && + + rm -f file1 file2 && + mv saved.file1 file1 && + git update-index --add --remove file1 file2 && + + for i in 1 E 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 F 21 + do + echo $i + done >file1 && + + cat file1 >saved.file1 +' + +test_expect_success 'apply without --reject should fail' ' + + if git apply patch.1 + then + echo "Eh? Why?" + exit 1 + fi + + diff -u file1 saved.file1 +' + +test_expect_success 'apply without --reject should fail' ' + + if git apply --verbose patch.1 + then + echo "Eh? Why?" + exit 1 + fi + + diff -u file1 saved.file1 +' + +test_expect_success 'apply with --reject should fail but update the file' ' + + cat saved.file1 >file1 && + rm -f file1.rej file2.rej && + + if git apply --reject patch.1 + then + echo "succeeds with --reject?" + exit 1 + fi + + diff -u file1 expected && + + cat file1.rej && + + if test -f file2.rej + then + echo "file2 should not have been touched" + exit 1 + fi +' + +test_expect_success 'apply with --reject should fail but update the file' ' + + cat saved.file1 >file1 && + rm -f file1.rej file2.rej file2 && + + if git apply --reject patch.2 >rejects + then + echo "succeeds with --reject?" + exit 1 + fi + + test -f file1 && { + echo "file1 still exists?" + exit 1 + } + diff -u file2 expected && + + cat file2.rej && + + if test -f file1.rej + then + echo "file2 should not have been touched" + exit 1 + fi + +' + +test_expect_success 'the same test with --verbose' ' + + cat saved.file1 >file1 && + rm -f file1.rej file2.rej file2 && + + if git apply --reject --verbose patch.2 >rejects + then + echo "succeeds with --reject?" + exit 1 + fi + + test -f file1 && { + echo "file1 still exists?" + exit 1 + } + diff -u file2 expected && + + cat file2.rej && + + if test -f file1.rej + then + echo "file2 should not have been touched" + exit 1 + fi + +' + +test_expect_success 'apply cleanly with --verbose' ' + + git cat-file -p HEAD:file1 >file1 && + rm -f file?.rej file2 && + + git apply --verbose patch.1 && + + diff -u file1 clean +' + +test_done diff --git a/t/t4118-apply-empty-context.sh b/t/t4118-apply-empty-context.sh new file mode 100755 index 0000000000..7309422fe5 --- /dev/null +++ b/t/t4118-apply-empty-context.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='git-apply with new style GNU diff with empty context + +' + +. ./test-lib.sh + +test_expect_success setup ' + { + echo; echo; + echo A; echo B; echo C; + echo; + } >file1 && + cat file1 >file1.orig && + { + cat file1 && + echo Q | tr -d "\\012" + } >file2 && + cat file2 >file2.orig + git add file1 file2 && + sed -e "/^B/d" <file1.orig >file1 && + sed -e "/^B/d" <file2.orig >file2 && + cat file1 >file1.mods && + cat file2 >file2.mods && + git diff | + sed -e "s/^ \$//" >diff.output +' + +test_expect_success 'apply --numstat' ' + + git apply --numstat diff.output >actual && + { + echo "0 1 file1" && + echo "0 1 file2" + } >expect && + diff -u expect actual + +' + +test_expect_success 'apply --apply' ' + + cat file1.orig >file1 && + cat file2.orig >file2 && + git update-index file1 file2 && + git apply --index diff.output && + diff -u file1.mods file1 && + diff -u file2.mods file2 +' + +test_done + diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh new file mode 100755 index 0000000000..c571a1bd74 --- /dev/null +++ b/t/t4200-rerere.sh @@ -0,0 +1,151 @@ +#!/bin/sh +# +# Copyright (c) 2006 Johannes E. Schindelin +# + +test_description='git-rerere +' + +. ./test-lib.sh + +cat > a1 << EOF +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And by opposing end them? To die: to sleep; +No more; and by a sleep to say we end +The heart-ache and the thousand natural shocks +That flesh is heir to, 'tis a consummation +Devoutly to be wish'd. +EOF + +git add a1 +git commit -q -a -m initial + +git checkout -b first +cat >> a1 << EOF +To die, to sleep; +To sleep: perchance to dream: ay, there's the rub; +For in that sleep of death what dreams may come +When we have shuffled off this mortal coil, +Must give us pause: there's the respect +That makes calamity of so long life; +EOF +git commit -q -a -m first + +git checkout -b second master +git show first:a1 | sed 's/To die, t/To die! T/' > a1 +git commit -q -a -m second + +# activate rerere +mkdir .git/rr-cache + +test_expect_failure 'conflicting merge' 'git pull . first' + +sha1=4f58849a60b4f969a2848966b6d02893b783e8fb +rr=.git/rr-cache/$sha1 +test_expect_success 'recorded preimage' "grep ======= $rr/preimage" + +test_expect_success 'no postimage or thisimage yet' \ + "test ! -f $rr/postimage -a ! -f $rr/thisimage" + +git show first:a1 > a1 + +cat > expect << EOF +--- a/a1 ++++ b/a1 +@@ -6,11 +6,7 @@ + The heart-ache and the thousand natural shocks + That flesh is heir to, 'tis a consummation + Devoutly to be wish'd. +-<<<<<<< +-To die! To sleep; +-======= + To die, to sleep; +->>>>>>> + To sleep: perchance to dream: ay, there's the rub; + For in that sleep of death what dreams may come + When we have shuffled off this mortal coil, +EOF + +git rerere diff > out + +test_expect_success 'rerere diff' 'diff -u expect out' + +cat > expect << EOF +a1 +EOF + +git rerere status > out + +test_expect_success 'rerere status' 'diff -u expect out' + +test_expect_success 'commit succeeds' \ + "git commit -q -a -m 'prefer first over second'" + +test_expect_success 'recorded postimage' "test -f $rr/postimage" + +git checkout -b third master +git show second^:a1 | sed 's/To die: t/To die! T/' > a1 +git commit -q -a -m third + +test_expect_failure 'another conflicting merge' 'git pull . first' + +git show first:a1 | sed 's/To die: t/To die! T/' > expect +test_expect_success 'rerere kicked in' "! grep ======= a1" + +test_expect_success 'rerere prefers first change' 'diff -u a1 expect' + +rm $rr/postimage +echo "$sha1 a1" | tr '\012' '\0' > .git/rr-cache/MERGE_RR + +test_expect_success 'rerere clear' 'git rerere clear' + +test_expect_success 'clear removed the directory' "test ! -d $rr" + +mkdir $rr +echo Hello > $rr/preimage +echo World > $rr/postimage + +sha2=4000000000000000000000000000000000000000 +rr2=.git/rr-cache/$sha2 +mkdir $rr2 +echo Hello > $rr2/preimage + +case "$(date -d @11111111 +%s 2>/dev/null)" in +11111111) + # 'date' must be able to take arbitrary input with @11111111 notation. + # for this test to succeed. We should fix this part using more + # portable script someday. + + now=$(date +%s) + almost_15_days_ago=$(($now+60-15*86400)) + just_over_15_days_ago=$(($now-1-15*86400)) + almost_60_days_ago=$(($now+60-60*86400)) + just_over_60_days_ago=$(($now-1-60*86400)) + predate1="$(date -d "@$almost_60_days_ago" +%Y%m%d%H%M.%S)" + predate2="$(date -d "@$almost_15_days_ago" +%Y%m%d%H%M.%S)" + postdate1="$(date -d "@$just_over_60_days_ago" +%Y%m%d%H%M.%S)" + postdate2="$(date -d "@$just_over_15_days_ago" +%Y%m%d%H%M.%S)" + + touch -m -t "$predate1" $rr/preimage + touch -m -t "$predate2" $rr2/preimage + + test_expect_success 'garbage collection (part1)' 'git rerere gc' + + test_expect_success 'young records still live' \ + "test -f $rr/preimage -a -f $rr2/preimage" + + touch -m -t "$postdate1" $rr/preimage + touch -m -t "$postdate2" $rr2/preimage + + test_expect_success 'garbage collection (part2)' 'git rerere gc' + + test_expect_success 'old records rest in peace' \ + "test ! -f $rr/preimage -a ! -f $rr2/preimage" + ;; +esac + +test_done + + diff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh new file mode 100755 index 0000000000..ac835fe431 --- /dev/null +++ b/t/t5000-tar-tree.sh @@ -0,0 +1,133 @@ +#!/bin/sh +# +# Copyright (C) 2005 Rene Scharfe +# + +test_description='git-tar-tree and git-get-tar-commit-id test + +This test covers the topics of file contents, commit date handling and +commit id embedding: + + The contents of the repository is compared to the extracted tar + archive. The repository contains simple text files, symlinks and a + binary file (/bin/sh). Only paths shorter than 99 characters are + used. + + git-tar-tree applies the commit date to every file in the archive it + creates. The test sets the commit date to a specific value and checks + if the tar archive contains that value. + + When giving git-tar-tree a commit id (in contrast to a tree id) it + embeds this commit id into the tar archive as a comment. The test + checks the ability of git-get-tar-commit-id to figure it out from the + tar file. + +' + +. ./test-lib.sh +TAR=${TAR:-tar} +UNZIP=${UNZIP:-unzip} + +test_expect_success \ + 'populate workdir' \ + 'mkdir a b c && + echo simple textfile >a/a && + mkdir a/bin && + cp /bin/sh a/bin && + ln -s a a/l1 && + (p=long_path_to_a_file && cd a && + for depth in 1 2 3 4 5; do mkdir $p && cd $p; done && + echo text >file_with_long_path) && + (cd a && find .) | sort >a.lst' + +test_expect_success \ + 'add files to repository' \ + 'find a -type f | xargs git-update-index --add && + find a -type l | xargs git-update-index --add && + treeid=`git-write-tree` && + echo $treeid >treeid && + git-update-ref HEAD $(TZ=GMT GIT_COMMITTER_DATE="2005-05-27 22:00:00" \ + git-commit-tree $treeid </dev/null)' + +test_expect_success \ + 'git-tar-tree' \ + 'git-tar-tree HEAD >b.tar' + +test_expect_success \ + 'validate file modification time' \ + 'TZ=GMT $TAR tvf b.tar a/a | + awk \{print\ \$4,\ \(length\(\$5\)\<7\)\ ?\ \$5\":00\"\ :\ \$5\} \ + >b.mtime && + echo "2005-05-27 22:00:00" >expected.mtime && + diff expected.mtime b.mtime' + +test_expect_success \ + 'git-get-tar-commit-id' \ + 'git-get-tar-commit-id <b.tar >b.commitid && + diff .git/$(git-symbolic-ref HEAD) b.commitid' + +test_expect_success \ + 'extract tar archive' \ + '(cd b && $TAR xf -) <b.tar' + +test_expect_success \ + 'validate filenames' \ + '(cd b/a && find .) | sort >b.lst && + diff a.lst b.lst' + +test_expect_success \ + 'validate file contents' \ + 'diff -r a b/a' + +test_expect_success \ + 'git-tar-tree with prefix' \ + 'git-tar-tree HEAD prefix >c.tar' + +test_expect_success \ + 'extract tar archive with prefix' \ + '(cd c && $TAR xf -) <c.tar' + +test_expect_success \ + 'validate filenames with prefix' \ + '(cd c/prefix/a && find .) | sort >c.lst && + diff a.lst c.lst' + +test_expect_success \ + 'validate file contents with prefix' \ + 'diff -r a c/prefix/a' + +test_expect_success \ + 'git-archive --format=zip' \ + 'git-archive --format=zip HEAD >d.zip' + +test_expect_success \ + 'extract ZIP archive' \ + '(mkdir d && cd d && $UNZIP ../d.zip)' + +test_expect_success \ + 'validate filenames' \ + '(cd d/a && find .) | sort >d.lst && + diff a.lst d.lst' + +test_expect_success \ + 'validate file contents' \ + 'diff -r a d/a' + +test_expect_success \ + 'git-archive --format=zip with prefix' \ + 'git-archive --format=zip --prefix=prefix/ HEAD >e.zip' + +test_expect_success \ + 'extract ZIP archive with prefix' \ + '(mkdir e && cd e && $UNZIP ../e.zip)' + +test_expect_success \ + 'validate filenames with prefix' \ + '(cd e/prefix/a && find .) | sort >e.lst && + diff a.lst e.lst' + +test_expect_success \ + 'validate file contents with prefix' \ + 'diff -r a e/prefix/a' + +test_done diff --git a/t/t5100-mailinfo.sh b/t/t5100-mailinfo.sh new file mode 100755 index 0000000000..17c1b80b5b --- /dev/null +++ b/t/t5100-mailinfo.sh @@ -0,0 +1,28 @@ +#!/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 diff --git a/t/t5100/info0001 b/t/t5100/info0001 new file mode 100644 index 0000000000..8c052777e0 --- /dev/null +++ b/t/t5100/info0001 @@ -0,0 +1,5 @@ +Author: A U Thor +Email: a.u.thor@example.com +Subject: a commit. +Date: Fri, 9 Jun 2006 00:44:16 -0700 + diff --git a/t/t5100/info0002 b/t/t5100/info0002 new file mode 100644 index 0000000000..49bb0fec85 --- /dev/null +++ b/t/t5100/info0002 @@ -0,0 +1,5 @@ +Author: A U Thor +Email: a.u.thor@example.com +Subject: another patch +Date: Fri, 9 Jun 2006 00:44:16 -0700 + diff --git a/t/t5100/info0003 b/t/t5100/info0003 new file mode 100644 index 0000000000..bd0d1221aa --- /dev/null +++ b/t/t5100/info0003 @@ -0,0 +1,5 @@ +Author: A U Thor +Email: a.u.thor@example.com +Subject: third patch +Date: Fri, 9 Jun 2006 00:44:16 -0700 + diff --git a/t/t5100/info0004 b/t/t5100/info0004 new file mode 100644 index 0000000000..616c3092a2 --- /dev/null +++ b/t/t5100/info0004 @@ -0,0 +1,5 @@ +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) + diff --git a/t/t5100/info0005 b/t/t5100/info0005 new file mode 100644 index 0000000000..46a46fc772 --- /dev/null +++ b/t/t5100/info0005 @@ -0,0 +1,5 @@ +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 + diff --git a/t/t5100/msg0001 b/t/t5100/msg0001 new file mode 100644 index 0000000000..b275a9a9b2 --- /dev/null +++ b/t/t5100/msg0001 @@ -0,0 +1,2 @@ +Here is a patch from A U Thor. + diff --git a/t/t5100/msg0002 b/t/t5100/msg0002 new file mode 100644 index 0000000000..e2546ec733 --- /dev/null +++ b/t/t5100/msg0002 @@ -0,0 +1,21 @@ +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. + diff --git a/t/t5100/msg0003 b/t/t5100/msg0003 new file mode 100644 index 0000000000..1ac68101b1 --- /dev/null +++ b/t/t5100/msg0003 @@ -0,0 +1,9 @@ +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. + diff --git a/t/t5100/msg0004 b/t/t5100/msg0004 new file mode 100644 index 0000000000..6f8ba3b8e0 --- /dev/null +++ b/t/t5100/msg0004 @@ -0,0 +1,7 @@ +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/t/t5100/msg0005 b/t/t5100/msg0005 new file mode 100644 index 0000000000..dd94cd7b9f --- /dev/null +++ b/t/t5100/msg0005 @@ -0,0 +1,13 @@ +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> diff --git a/t/t5100/patch0001 b/t/t5100/patch0001 new file mode 100644 index 0000000000..8ce155167d --- /dev/null +++ b/t/t5100/patch0001 @@ -0,0 +1,14 @@ +--- + 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 + diff --git a/t/t5100/patch0002 b/t/t5100/patch0002 new file mode 100644 index 0000000000..8ce155167d --- /dev/null +++ b/t/t5100/patch0002 @@ -0,0 +1,14 @@ +--- + 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 + diff --git a/t/t5100/patch0003 b/t/t5100/patch0003 new file mode 100644 index 0000000000..8ce155167d --- /dev/null +++ b/t/t5100/patch0003 @@ -0,0 +1,14 @@ +--- + 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 + diff --git a/t/t5100/patch0004 b/t/t5100/patch0004 new file mode 100644 index 0000000000..196458e44e --- /dev/null +++ b/t/t5100/patch0004 @@ -0,0 +1,93 @@ +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 + diff --git a/t/t5100/patch0005 b/t/t5100/patch0005 new file mode 100644 index 0000000000..7d24b24af8 --- /dev/null +++ b/t/t5100/patch0005 @@ -0,0 +1,69 @@ +--- + + 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 + diff --git a/t/t5100/sample.mbox b/t/t5100/sample.mbox new file mode 100644 index 0000000000..a76845465a --- /dev/null +++ b/t/t5100/sample.mbox @@ -0,0 +1,317 @@ +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 + diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh new file mode 100755 index 0000000000..f511547455 --- /dev/null +++ b/t/t5300-pack-object.sh @@ -0,0 +1,199 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-pack-object + +' +. ./test-lib.sh + +TRASH=`pwd` + +test_expect_success \ + 'setup' \ + 'rm -f .git/index* + for i in a b c + do + dd if=/dev/zero bs=4k count=1 | tr "\\0" $i >$i && + git-update-index --add $i || return 1 + done && + cat c >d && echo foo >>d && git-update-index --add d && + tree=`git-write-tree` && + commit=`git-commit-tree $tree </dev/null` && { + echo $tree && + echo $commit && + git-ls-tree $tree | sed -e "s/.* \\([0-9a-f]*\\) .*/\\1/" + } >obj-list && { + git-diff-tree --root -p $commit && + while read object + do + t=`git-cat-file -t $object` && + git-cat-file $t $object || return 1 + done <obj-list + } >expect' + +test_expect_success \ + 'pack without delta' \ + 'packname_1=$(git-pack-objects --window=0 test-1 <obj-list)' + +rm -fr .git2 +mkdir .git2 + +test_expect_success \ + 'unpack without delta' \ + "GIT_OBJECT_DIRECTORY=.git2/objects && + export GIT_OBJECT_DIRECTORY && + git-init && + git-unpack-objects -n <test-1-${packname_1}.pack && + git-unpack-objects <test-1-${packname_1}.pack" + +unset GIT_OBJECT_DIRECTORY +cd "$TRASH/.git2" + +test_expect_success \ + 'check unpack without delta' \ + '(cd ../.git && find objects -type f -print) | + while read path + do + cmp $path ../.git/$path || { + echo $path differs. + return 1 + } + done' +cd "$TRASH" + +test_expect_success \ + 'pack with delta' \ + 'pwd && + packname_2=$(git-pack-objects test-2 <obj-list)' + +rm -fr .git2 +mkdir .git2 + +test_expect_success \ + 'unpack with delta' \ + 'GIT_OBJECT_DIRECTORY=.git2/objects && + export GIT_OBJECT_DIRECTORY && + git-init && + git-unpack-objects -n <test-2-${packname_2}.pack && + git-unpack-objects <test-2-${packname_2}.pack' + +unset GIT_OBJECT_DIRECTORY +cd "$TRASH/.git2" +test_expect_success \ + 'check unpack with delta' \ + '(cd ../.git && find objects -type f -print) | + while read path + do + cmp $path ../.git/$path || { + echo $path differs. + return 1 + } + done' +cd "$TRASH" + +rm -fr .git2 +mkdir .git2 + +test_expect_success \ + 'use packed objects' \ + 'GIT_OBJECT_DIRECTORY=.git2/objects && + export GIT_OBJECT_DIRECTORY && + git-init && + cp test-1-${packname_1}.pack test-1-${packname_1}.idx .git2/objects/pack && { + git-diff-tree --root -p $commit && + while read object + do + t=`git-cat-file -t $object` && + git-cat-file $t $object || return 1 + done <obj-list + } >current && + diff expect current' + + +test_expect_success \ + 'use packed deltified objects' \ + 'GIT_OBJECT_DIRECTORY=.git2/objects && + export GIT_OBJECT_DIRECTORY && + rm -f .git2/objects/pack/test-?.idx && + cp test-2-${packname_2}.pack test-2-${packname_2}.idx .git2/objects/pack && { + git-diff-tree --root -p $commit && + while read object + do + t=`git-cat-file -t $object` && + git-cat-file $t $object || return 1 + done <obj-list + } >current && + diff expect current' + +unset GIT_OBJECT_DIRECTORY + +test_expect_success \ + 'verify pack' \ + 'git-verify-pack test-1-${packname_1}.idx test-2-${packname_2}.idx' + +test_expect_success \ + 'corrupt a pack and see if verify catches' \ + 'cp test-1-${packname_1}.idx test-3.idx && + cp test-2-${packname_2}.pack test-3.pack && + if git-verify-pack test-3.idx + then false + else :; + fi && + + : PACK_SIGNATURE && + cp test-1-${packname_1}.pack test-3.pack && + dd if=/dev/zero of=test-3.pack count=1 bs=1 conv=notrunc seek=2 && + if git-verify-pack test-3.idx + then false + else :; + fi && + + : PACK_VERSION && + cp test-1-${packname_1}.pack test-3.pack && + dd if=/dev/zero of=test-3.pack count=1 bs=1 conv=notrunc seek=7 && + if git-verify-pack test-3.idx + then false + else :; + fi && + + : TYPE/SIZE byte of the first packed object data && + cp test-1-${packname_1}.pack test-3.pack && + dd if=/dev/zero of=test-3.pack count=1 bs=1 conv=notrunc seek=12 && + if git-verify-pack test-3.idx + then false + else :; + fi && + + : sum of the index file itself && + l=`wc -c <test-3.idx` && + l=`expr $l - 20` && + cp test-1-${packname_1}.pack test-3.pack && + dd if=/dev/zero of=test-3.idx count=20 bs=1 conv=notrunc seek=$l && + if git-verify-pack test-3.pack + then false + else :; + fi && + + :' + +test_expect_success \ + 'build pack index for an existing pack' \ + 'cp test-1-${packname_1}.pack test-3.pack && + git-index-pack -o tmp.idx test-3.pack && + cmp tmp.idx test-1-${packname_1}.idx && + + git-index-pack test-3.pack && + cmp test-3.idx test-1-${packname_1}.idx && + + cp test-2-${packname_2}.pack test-3.pack && + git-index-pack -o tmp.idx test-2-${packname_2}.pack && + cmp tmp.idx test-2-${packname_2}.idx && + + git-index-pack test-3.pack && + cmp test-3.idx test-2-${packname_2}.idx && + + :' + +test_done diff --git a/t/t5301-sliding-window.sh b/t/t5301-sliding-window.sh new file mode 100755 index 0000000000..a6dbb04a86 --- /dev/null +++ b/t/t5301-sliding-window.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# +# Copyright (c) 2006 Shawn Pearce +# + +test_description='mmap sliding window tests' +. ./test-lib.sh + +test_expect_success \ + 'setup' \ + 'rm -f .git/index* + for i in a b c + do + echo $i >$i && + dd if=/dev/urandom bs=32k count=1 >>$i && + git-update-index --add $i || return 1 + done && + echo d >d && cat c >>d && git-update-index --add d && + tree=`git-write-tree` && + commit1=`git-commit-tree $tree </dev/null` && + git-update-ref HEAD $commit1 && + git-repack -a -d && + test "`git-count-objects`" = "0 objects, 0 kilobytes" && + pack1=`ls .git/objects/pack/*.pack` && + test -f "$pack1"' + +test_expect_success \ + 'verify-pack -v, defaults' \ + 'git-verify-pack -v "$pack1"' + +test_expect_success \ + 'verify-pack -v, packedGitWindowSize == 1 page' \ + 'git-config core.packedGitWindowSize 512 && + git-verify-pack -v "$pack1"' + +test_expect_success \ + 'verify-pack -v, packedGit{WindowSize,Limit} == 1 page' \ + 'git-config core.packedGitWindowSize 512 && + git-config core.packedGitLimit 512 && + git-verify-pack -v "$pack1"' + +test_expect_success \ + 'repack -a -d, packedGit{WindowSize,Limit} == 1 page' \ + 'git-config core.packedGitWindowSize 512 && + git-config core.packedGitLimit 512 && + commit2=`git-commit-tree $tree -p $commit1 </dev/null` && + git-update-ref HEAD $commit2 && + git-repack -a -d && + test "`git-count-objects`" = "0 objects, 0 kilobytes" && + pack2=`ls .git/objects/pack/*.pack` && + test -f "$pack2" + test "$pack1" \!= "$pack2"' + +test_expect_success \ + 'verify-pack -v, defaults' \ + 'git-config --unset core.packedGitWindowSize && + git-config --unset core.packedGitLimit && + git-verify-pack -v "$pack2"' + +test_done diff --git a/t/t5400-send-pack.sh b/t/t5400-send-pack.sh new file mode 100755 index 0000000000..7d93d0d7c9 --- /dev/null +++ b/t/t5400-send-pack.sh @@ -0,0 +1,116 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='See why rewinding head breaks send-pack + +' +. ./test-lib.sh + +cnt=64 +test_expect_success setup ' + test_tick && + mkdir mozart mozart/is && + echo "Commit #0" >mozart/is/pink && + git-update-index --add mozart/is/pink && + tree=$(git-write-tree) && + commit=$(echo "Commit #0" | git-commit-tree $tree) && + zero=$commit && + parent=$zero && + i=0 && + while test $i -le $cnt + do + i=$(($i+1)) && + test_tick && + echo "Commit #$i" >mozart/is/pink && + git-update-index --add mozart/is/pink && + tree=$(git-write-tree) && + commit=$(echo "Commit #$i" | git-commit-tree $tree -p $parent) && + git-update-ref refs/tags/commit$i $commit && + parent=$commit || return 1 + done && + git-update-ref HEAD "$commit" && + git-clone ./. victim && + cd victim && + git-log && + cd .. && + git-update-ref HEAD "$zero" && + parent=$zero && + i=0 && + while test $i -le $cnt + do + i=$(($i+1)) && + test_tick && + echo "Rebase #$i" >mozart/is/pink && + git-update-index --add mozart/is/pink && + tree=$(git-write-tree) && + commit=$(echo "Rebase #$i" | git-commit-tree $tree -p $parent) && + git-update-ref refs/tags/rebase$i $commit && + parent=$commit || return 1 + done && + git-update-ref HEAD "$commit" && + echo Rebase && + git-log' + +test_expect_success 'pack the source repository' ' + git repack -a -d && + git prune +' + +test_expect_success 'pack the destination repository' ' + cd victim && + git repack -a -d && + git prune && + cd .. +' + +test_expect_success \ + 'pushing rewound head should not barf but require --force' ' + # should not fail but refuse to update. + if git-send-pack ./victim/.git/ master + then + # now it should fail with Pasky patch + echo >&2 Gaah, it should have failed. + false + else + echo >&2 Thanks, it correctly failed. + true + fi && + if cmp victim/.git/refs/heads/master .git/refs/heads/master + then + # should have been left as it was! + false + else + true + fi && + # this should update + git-send-pack --force ./victim/.git/ master && + cmp victim/.git/refs/heads/master .git/refs/heads/master +' + +test_expect_success \ + 'push can be used to delete a ref' ' + cd victim && + git branch extra master && + cd .. && + test -f victim/.git/refs/heads/extra && + git-send-pack ./victim/.git/ :extra master && + ! test -f victim/.git/refs/heads/extra +' + +unset GIT_CONFIG GIT_CONFIG_LOCAL +HOME=`pwd`/no-such-directory +export HOME ;# this way we force the victim/.git/config to be used. + +test_expect_success \ + 'pushing with --force should be denied with denyNonFastforwards' ' + cd victim && + git-config receive.denyNonFastforwards true && + cd .. && + git-update-ref refs/heads/master master^ && + git-send-pack --force ./victim/.git/ master && + ! diff -u .git/refs/heads/master victim/.git/refs/heads/master +' + +test_done diff --git a/t/t5401-update-hooks.sh b/t/t5401-update-hooks.sh new file mode 100755 index 0000000000..0514056ca6 --- /dev/null +++ b/t/t5401-update-hooks.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# +# Copyright (c) 2006 Shawn O. Pearce +# + +test_description='Test the update hook infrastructure.' +. ./test-lib.sh + +test_expect_success setup ' + echo This is a test. >a && + git-update-index --add a && + tree0=$(git-write-tree) && + commit0=$(echo setup | git-commit-tree $tree0) && + git-update-ref HEAD $commit0 && + git-clone ./. victim && + echo We hope it works. >a && + git-update-index a && + tree1=$(git-write-tree) && + commit1=$(echo modify | git-commit-tree $tree1 -p $commit0) && + git-update-ref HEAD $commit1 +' + +cat >victim/.git/hooks/update <<'EOF' +#!/bin/sh +echo "$@" >$GIT_DIR/update.args +read x; printf "$x" >$GIT_DIR/update.stdin +echo STDOUT update +echo STDERR update >&2 +EOF +chmod u+x victim/.git/hooks/update + +cat >victim/.git/hooks/post-update <<'EOF' +#!/bin/sh +echo "$@" >$GIT_DIR/post-update.args +read x; printf "$x" >$GIT_DIR/post-update.stdin +echo STDOUT post-update +echo STDERR post-update >&2 +EOF +chmod u+x victim/.git/hooks/post-update + +test_expect_success push ' + git-send-pack ./victim/.git/ master >send.out 2>send.err +' + +test_expect_success 'hooks ran' ' + test -f victim/.git/update.args && + test -f victim/.git/update.stdin && + test -f victim/.git/post-update.args && + test -f victim/.git/post-update.stdin +' + +test_expect_success 'update hook arguments' ' + echo refs/heads/master $commit0 $commit1 | + diff -u - victim/.git/update.args +' + +test_expect_success 'post-update hook arguments' ' + echo refs/heads/master | + diff -u - victim/.git/post-update.args +' + +test_expect_failure 'update hook stdin is /dev/null' ' + test -s victim/.git/update.stdin +' + +test_expect_failure 'post-update hook stdin is /dev/null' ' + test -s victim/.git/post-update.stdin +' + +test_expect_failure 'send-pack produced no output' ' + test -s send.out +' + +test_expect_success 'send-pack stderr contains hook messages' ' + grep "STDOUT update" send.err && + grep "STDERR update" send.err && + grep "STDOUT post-update" send.err && + grep "STDERR post-update" send.err +' + +test_done diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh new file mode 100755 index 0000000000..48e3d1705f --- /dev/null +++ b/t/t5500-fetch-pack.sh @@ -0,0 +1,182 @@ +#!/bin/sh +# +# Copyright (c) 2005 Johannes Schindelin +# + +test_description='Testing multi_ack pack fetching + +' +. ./test-lib.sh + +# Test fetch-pack/upload-pack pair. + +# Some convenience functions + +add () { + name=$1 + text="$@" + branch=`echo $name | sed -e 's/^\(.\).*$/\1/'` + parents="" + + shift + while test $1; do + parents="$parents -p $1" + shift + done + + echo "$text" > test.txt + git-update-index --add test.txt + tree=$(git-write-tree) + # make sure timestamps are in correct order + sec=$(($sec+1)) + commit=$(echo "$text" | GIT_AUTHOR_DATE=$sec \ + git-commit-tree $tree $parents 2>>log2.txt) + export $name=$commit + echo $commit > .git/refs/heads/$branch + eval ${branch}TIP=$commit +} + +count_objects () { + ls .git/objects/??/* 2>>log2.txt | wc -l | tr -d " " +} + +test_expect_object_count () { + message=$1 + count=$2 + + output="$(count_objects)" + test_expect_success \ + "new object count $message" \ + "test $count = $output" +} + +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/`echo $heads | sed -e 's/^\(.\).*$/\1/'` + + test_expect_success "fsck" 'git-fsck --full > fsck.txt 2>&1' + + test_expect_success 'check downloaded results' \ + 'mv .git/objects/pack/pack-* . && + p=`ls -1 pack-*.pack` && + git-unpack-objects <$p && + git-fsck --full' + + test_expect_success "new object count after $number pull" \ + 'idx=`echo pack-*.idx` && + pack_count=`git-show-index <$idx | wc -l` && + test $pack_count = $count' + test -z "$pack_count" && pack_count=0 + if [ -z "$no_strict_count_check" ]; then + test_expect_success "minimal count" "test $count = $pack_count" + else + test $count != $pack_count && \ + echo "WARNING: $pack_count objects transmitted, only $count of which were needed" + fi + rm -f pack-* + cd .. +} + +# Here begins the actual testing + +# A1 - ... - A20 - A21 +# \ +# B1 - B2 - .. - B70 + +# client pulls A20, B1. Then tracks only B. Then pulls A. + +( + mkdir client && + cd client && + git-init 2>> log2.txt && + git config transfer.unpacklimit 0 +) + +add A1 + +prev=1; cur=2; while [ $cur -le 10 ]; do + add A$cur $(eval echo \$A$prev) + prev=$cur + cur=$(($cur+1)) +done + +add B1 $A1 + +echo $ATIP > .git/refs/heads/A +echo $BTIP > .git/refs/heads/B +git-symbolic-ref HEAD refs/heads/B + +pull_to_client 1st "B A" $((11*3)) + +add A11 $A10 + +prev=1; cur=2; while [ $cur -le 65 ]; do + add B$cur $(eval echo \$B$prev) + prev=$cur + cur=$(($cur+1)) +done + +pull_to_client 2nd "B" $((64*3)) + +pull_to_client 3rd "A" $((1*3)) # old fails + +test_expect_success "clone shallow" "git-clone --depth 2 . shallow" + +(cd shallow; git-count-objects -v) > count.shallow + +test_expect_success "clone shallow object count" \ + "test \"in-pack: 18\" = \"$(grep in-pack count.shallow)\"" + +count_output () { + sed -e '/^in-pack:/d' -e '/^packs:/d' -e '/: 0$/d' "$1" +} + +test_expect_success "clone shallow object count (part 2)" ' + test -z "$(count_output count.shallow)" +' + +test_expect_success "fsck in shallow repo" \ + "(cd shallow; git-fsck --full)" + +#test_done; exit + +add B66 $B65 +add B67 $B66 + +test_expect_success "pull in shallow repo" \ + "(cd shallow; git pull .. B)" + +(cd shallow; git-count-objects -v) > count.shallow +test_expect_success "clone shallow object count" \ + "test \"count: 6\" = \"$(grep count count.shallow)\"" + +add B68 $B67 +add B69 $B68 + +test_expect_success "deepening pull in shallow repo" \ + "(cd shallow; git pull --depth 4 .. B)" + +(cd shallow; git-count-objects -v) > count.shallow +test_expect_success "clone shallow object count" \ + "test \"count: 12\" = \"$(grep count count.shallow)\"" + +test_expect_success "deepening fetch in shallow repo" \ + "(cd shallow; git fetch --depth 4 .. A:A)" + +(cd shallow; git-count-objects -v) > count.shallow +test_expect_success "clone shallow object count" \ + "test \"count: 18\" = \"$(grep count count.shallow)\"" + +test_expect_failure "pull in shallow repo with missing merge base" \ + "(cd shallow; git pull --depth 4 .. A)" + +test_done diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh new file mode 100755 index 0000000000..50c64856f0 --- /dev/null +++ b/t/t5510-fetch.sh @@ -0,0 +1,84 @@ +#!/bin/sh +# Copyright (c) 2006, Junio C Hamano. + +test_description='Per branch config variables affects "git fetch". + +' + +. ./test-lib.sh + +D=`pwd` + +test_expect_success setup ' + echo >file original && + git add file && + git commit -a -m original' + +test_expect_success "clone and setup child repos" ' + git clone . one && + cd one && + echo >file updated by one && + git commit -a -m "updated by one" && + cd .. && + git clone . two && + cd two && + git config branch.master.remote one && + git config remote.one.url ../one/.git/ && + git config remote.one.fetch refs/heads/master:refs/heads/one && + cd .. && + git clone . three && + cd three && + git config branch.master.remote two && + git config branch.master.merge refs/heads/one && + mkdir -p .git/remotes && + { + echo "URL: ../two/.git/" + echo "Pull: refs/heads/master:refs/heads/two" + echo "Pull: refs/heads/one:refs/heads/one" + } >.git/remotes/two +' + +test_expect_success "fetch test" ' + cd "$D" && + echo >file updated by origin && + git commit -a -m "updated by origin" && + cd two && + git fetch && + test -f .git/refs/heads/one && + mine=`git rev-parse refs/heads/one` && + his=`cd ../one && git rev-parse refs/heads/master` && + test "z$mine" = "z$his" +' + +test_expect_success "fetch test for-merge" ' + cd "$D" && + cd three && + git fetch && + test -f .git/refs/heads/two && + test -f .git/refs/heads/one && + master_in_two=`cd ../two && git rev-parse master` && + one_in_two=`cd ../two && git rev-parse one` && + { + echo "$master_in_two not-for-merge" + echo "$one_in_two " + } >expected && + cut -f -2 .git/FETCH_HEAD >actual && + diff expected actual' + +test_expect_success 'fetch following tags' ' + + cd "$D" && + git tag -a -m 'annotated' anno HEAD && + git tag light HEAD && + + mkdir four && + cd four && + git init && + + git fetch .. :track && + git show-ref --verify refs/tags/anno && + git show-ref --verify refs/tags/light + +' + +test_done diff --git a/t/t5520-pull.sh b/t/t5520-pull.sh new file mode 100755 index 0000000000..7eb37838bb --- /dev/null +++ b/t/t5520-pull.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +test_description='pulling into void' + +. ./test-lib.sh + +D=`pwd` + +test_expect_success setup ' + + echo file >file && + git add file && + git commit -a -m original + +' + +test_expect_success 'pulling into void' ' + mkdir cloned && + cd cloned && + git init && + git pull .. +' + +cd "$D" + +test_expect_success 'checking the results' ' + test -f file && + test -f cloned/file && + diff file cloned/file +' + +test_done + diff --git a/t/t5600-clone-fail-cleanup.sh b/t/t5600-clone-fail-cleanup.sh new file mode 100755 index 0000000000..1776b377f3 --- /dev/null +++ b/t/t5600-clone-fail-cleanup.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# +# Copyright (C) 2006 Carl D. Worth <cworth@cworth.org> +# + +test_description='test git-clone to cleanup after failure + +This test covers the fact that if git-clone fails, it should remove +the directory it created, to avoid the user having to manually +remove the directory before attempting a clone again.' + +. ./test-lib.sh + +test_expect_failure \ + 'clone of non-existent source should fail' \ + 'git-clone foo bar' + +test_expect_failure \ + 'failed clone should not leave a directory' \ + 'cd bar' + +# Need a repo to clone +test_create_repo foo + +# clone doesn't like it if there is no HEAD. Is that a bug? +(cd foo && touch file && git add file && git commit -m 'add file' >/dev/null 2>&1) + +# source repository given to git-clone should be relative to the +# current path not to the target dir +test_expect_failure \ + 'clone of non-existent (relative to $PWD) source should fail' \ + 'git-clone ../foo baz' + +test_expect_success \ + 'clone should work now that source exists' \ + 'git-clone foo bar' + +test_expect_success \ + 'successful clone must leave the directory' \ + 'cd bar' + +test_done diff --git a/t/t5700-clone-reference.sh b/t/t5700-clone-reference.sh new file mode 100755 index 0000000000..6d43252593 --- /dev/null +++ b/t/t5700-clone-reference.sh @@ -0,0 +1,116 @@ +#!/bin/sh +# +# Copyright (C) 2006 Martin Waitz <tali@admingilde.org> +# + +test_description='test clone --reference' +. ./test-lib.sh + +base_dir=`pwd` + +test_expect_success 'preparing first repository' \ +'test_create_repo A && cd A && +echo first > file1 && +git add file1 && +git commit -m initial' + +cd "$base_dir" + +test_expect_success 'preparing second repository' \ +'git clone A B && cd B && +echo second > file2 && +git add file2 && +git commit -m addition && +git repack -a -d && +git prune' + +cd "$base_dir" + +test_expect_success 'cloning with reference (-l -s)' \ +'git clone -l -s --reference B A C' + +cd "$base_dir" + +test_expect_success 'existence of info/alternates' \ +'test `wc -l <C/.git/objects/info/alternates` = 2' + +cd "$base_dir" + +test_expect_success 'pulling from reference' \ +'cd C && +git pull ../B' + +cd "$base_dir" + +test_expect_success 'that reference gets used' \ +'cd C && +echo "0 objects, 0 kilobytes" > expected && +git count-objects > current && +diff expected current' + +cd "$base_dir" + +test_expect_success 'cloning with reference (no -l -s)' \ +'git clone --reference B A D' + +cd "$base_dir" + +test_expect_success 'existence of info/alternates' \ +'test `wc -l <D/.git/objects/info/alternates` = 1' + +cd "$base_dir" + +test_expect_success 'pulling from reference' \ +'cd D && git pull ../B' + +cd "$base_dir" + +test_expect_success 'that reference gets used' \ +'cd D && echo "0 objects, 0 kilobytes" > expected && +git count-objects > current && +diff expected current' + +cd "$base_dir" + +test_expect_success 'updating origin' \ +'cd A && +echo third > file3 && +git add file3 && +git commit -m update && +git repack -a -d && +git prune' + +cd "$base_dir" + +test_expect_success 'pulling changes from origin' \ +'cd C && +git pull origin' + +cd "$base_dir" + +# the 2 local objects are commit and tree from the merge +test_expect_success 'that alternate to origin gets used' \ +'cd C && +echo "2 objects" > expected && +git count-objects | cut -d, -f1 > current && +diff expected current' + +cd "$base_dir" + +test_expect_success 'pulling changes from origin' \ +'cd D && +git pull origin' + +cd "$base_dir" + +# the 5 local objects are expected; file3 blob, commit in A to add it +# and its tree, and 2 are our tree and the merge commit. +test_expect_success 'check objects expected to exist locally' \ +'cd D && +echo "5 objects" > expected && +git count-objects | cut -d, -f1 > current && +diff expected current' + +cd "$base_dir" + +test_done diff --git a/t/t5710-info-alternate.sh b/t/t5710-info-alternate.sh new file mode 100755 index 0000000000..2f8e97cb7e --- /dev/null +++ b/t/t5710-info-alternate.sh @@ -0,0 +1,107 @@ +#!/bin/sh +# +# Copyright (C) 2006 Martin Waitz <tali@admingilde.org> +# + +test_description='test transitive info/alternate entries' +. ./test-lib.sh + +# test that a file is not reachable in the current repository +# but that it is after creating a info/alternate entry +reachable_via() { + alternate="$1" + file="$2" + if git cat-file -e "HEAD:$file"; then return 1; fi + echo "$alternate" >> .git/objects/info/alternate + git cat-file -e "HEAD:$file" +} + +test_valid_repo() { + git fsck --full > fsck.log && + test `wc -l < fsck.log` = 0 +} + +base_dir=`pwd` + +test_expect_success 'preparing first repository' \ +'test_create_repo A && cd A && +echo "Hello World" > file1 && +git add file1 && +git commit -m "Initial commit" file1 && +git repack -a -d && +git prune' + +cd "$base_dir" + +test_expect_success 'preparing second repository' \ +'git clone -l -s A B && cd B && +echo "foo bar" > file2 && +git add file2 && +git commit -m "next commit" file2 && +git repack -a -d -l && +git prune' + +cd "$base_dir" + +test_expect_success 'preparing third repository' \ +'git clone -l -s B C && cd C && +echo "Goodbye, cruel world" > file3 && +git add file3 && +git commit -m "one more" file3 && +git repack -a -d -l && +git prune' + +cd "$base_dir" + +test_expect_failure 'creating too deep nesting' \ +'git clone -l -s C D && +git clone -l -s D E && +git clone -l -s E F && +git clone -l -s F G && +git clone -l -s G H && +cd H && +test_valid_repo' + +cd "$base_dir" + +test_expect_success 'validity of third repository' \ +'cd C && +test_valid_repo' + +cd "$base_dir" + +test_expect_success 'validity of fourth repository' \ +'cd D && +test_valid_repo' + +cd "$base_dir" + +test_expect_success 'breaking of loops' \ +"echo '$base_dir/B/.git/objects' >> '$base_dir'/A/.git/objects/info/alternates&& +cd C && +test_valid_repo" + +cd "$base_dir" + +test_expect_failure 'that info/alternates is necessary' \ +'cd C && +rm .git/objects/info/alternates && +test_valid_repo' + +cd "$base_dir" + +test_expect_success 'that relative alternate is possible for current dir' \ +'cd C && +echo "../../../B/.git/objects" > .git/objects/info/alternates && +test_valid_repo' + +cd "$base_dir" + +test_expect_failure 'that relative alternate is only possible for current dir' \ +'cd D && +test_valid_repo' + +cd "$base_dir" + +test_done + diff --git a/t/t6000lib.sh b/t/t6000lib.sh new file mode 100755 index 0000000000..d40262159b --- /dev/null +++ b/t/t6000lib.sh @@ -0,0 +1,116 @@ +[ -d .git/refs/tags ] || mkdir -p .git/refs/tags + +:> sed.script + +# Answer the sha1 has associated with the tag. The tag must exist in .git or .git/refs/tags +tag() +{ + _tag=$1 + [ -f .git/refs/tags/$_tag ] || error "tag: \"$_tag\" does not exist" + cat .git/refs/tags/$_tag +} + +# Generate a commit using the text specified to make it unique and the tree +# named by the tag specified. +unique_commit() +{ + _text=$1 + _tree=$2 + shift 2 + echo $_text | git-commit-tree $(tag $_tree) "$@" +} + +# Save the output of a command into the tag specified. Prepend +# a substitution script for the tag onto the front of sed.script +save_tag() +{ + _tag=$1 + [ -n "$_tag" ] || error "usage: save_tag tag commit-args ..." + shift 1 + "$@" >.git/refs/tags/$_tag + + echo "s/$(tag $_tag)/$_tag/g" > sed.script.tmp + cat sed.script >> sed.script.tmp + rm sed.script + mv sed.script.tmp sed.script +} + +# Replace unhelpful sha1 hashses with their symbolic equivalents +entag() +{ + sed -f sed.script +} + +# Execute a command after first saving, then setting the GIT_AUTHOR_EMAIL +# tag to a specified value. Restore the original value on return. +as_author() +{ + _author=$1 + shift 1 + _save=$GIT_AUTHOR_EMAIL + + export GIT_AUTHOR_EMAIL="$_author" + "$@" + if test -z "$_save" + then + unset GIT_AUTHOR_EMAIL + else + export GIT_AUTHOR_EMAIL="$_save" + fi +} + +commit_date() +{ + _commit=$1 + git-cat-file commit $_commit | sed -n "s/^committer .*> \([0-9]*\) .*/\1/p" +} + +on_committer_date() +{ + _date=$1 + shift 1 + export GIT_COMMITTER_DATE="$_date" + "$@" + unset GIT_COMMITTER_DATE +} + +# Execute a command and suppress any error output. +hide_error() +{ + "$@" 2>/dev/null +} + +check_output() +{ + _name=$1 + shift 1 + if eval "$*" | entag > $_name.actual + then + diff $_name.expected $_name.actual + else + return 1; + fi +} + +# Turn a reasonable test description into a reasonable test name. +# All alphanums translated into -'s which are then compressed and stripped +# from front and back. +name_from_description() +{ + tr "'" '-' | tr '~`!@#$%^&*()_+={}[]|\;:"<>,/? ' '-' | tr -s '-' | tr '[A-Z]' '[a-z]' | sed "s/^-*//;s/-*\$//" +} + + +# Execute the test described by the first argument, by eval'ing +# command line specified in the 2nd argument. Check the status code +# is zero and that the output matches the stream read from +# stdin. +test_output_expect_success() +{ + _description=$1 + _test=$2 + [ $# -eq 2 ] || error "usage: test_output_expect_success description test <<EOF ... EOF" + _name=$(echo $_description | name_from_description) + cat > $_name.expected + test_expect_success "$_description" "check_output $_name \"$_test\"" +} diff --git a/t/t6001-rev-list-graft.sh b/t/t6001-rev-list-graft.sh new file mode 100755 index 0000000000..b2131cdacd --- /dev/null +++ b/t/t6001-rev-list-graft.sh @@ -0,0 +1,113 @@ +#!/bin/sh + +test_description='Revision traversal vs grafts and path limiter' + +. ./test-lib.sh + +test_expect_success setup ' + mkdir subdir && + echo >fileA fileA && + echo >subdir/fileB fileB && + git add fileA subdir/fileB && + git commit -a -m "Initial in one history." && + A0=`git rev-parse --verify HEAD` && + + echo >fileA fileA modified && + git commit -a -m "Second in one history." && + A1=`git rev-parse --verify HEAD` && + + echo >subdir/fileB fileB modified && + git commit -a -m "Third in one history." && + A2=`git rev-parse --verify HEAD` && + + rm -f .git/refs/heads/master .git/index && + + echo >fileA fileA again && + echo >subdir/fileB fileB again && + git add fileA subdir/fileB && + git commit -a -m "Initial in alternate history." && + B0=`git rev-parse --verify HEAD` && + + echo >fileA fileA modified in alternate history && + git commit -a -m "Second in alternate history." && + B1=`git rev-parse --verify HEAD` && + + echo >subdir/fileB fileB modified in alternate history && + git commit -a -m "Third in alternate history." && + B2=`git rev-parse --verify HEAD` && + : done +' + +check () { + type=$1 + shift + + arg= + which=arg + rm -f test.expect + for a + do + if test "z$a" = z-- + then + which=expect + child= + continue + fi + if test "$which" = arg + then + arg="$arg$a " + continue + fi + if test "$type" = basic + then + echo "$a" + else + if test "z$child" != z + then + echo "$child $a" + fi + child="$a" + fi + done >test.expect + if test "$type" != basic && test "z$child" != z + then + echo >>test.expect $child + fi + if test $type = basic + then + git rev-list $arg >test.actual + elif test $type = parents + then + git rev-list --parents $arg >test.actual + elif test $type = parents-raw + then + git rev-list --parents --pretty=raw $arg | + sed -n -e 's/^commit //p' >test.actual + fi + diff test.expect test.actual +} + +for type in basic parents parents-raw +do + test_expect_success 'without grafts' " + rm -f .git/info/grafts + check $type $B2 -- $B2 $B1 $B0 + " + + test_expect_success 'with grafts' " + echo '$B0 $A2' >.git/info/grafts + check $type $B2 -- $B2 $B1 $B0 $A2 $A1 $A0 + " + + test_expect_success 'without grafts, with pathlimit' " + rm -f .git/info/grafts + check $type $B2 subdir -- $B2 $B0 + " + + test_expect_success 'with grafts, with pathlimit' " + echo '$B0 $A2' >.git/info/grafts + check $type $B2 subdir -- $B2 $B0 $A2 $A0 + " + +done +test_done diff --git a/t/t6002-rev-list-bisect.sh b/t/t6002-rev-list-bisect.sh new file mode 100755 index 0000000000..7831e3461c --- /dev/null +++ b/t/t6002-rev-list-bisect.sh @@ -0,0 +1,238 @@ +#!/bin/sh +# +# Copyright (c) 2005 Jon Seymour +# +test_description='Tests git-rev-list --bisect functionality' + +. ./test-lib.sh +. ../t6000lib.sh # t6xxx specific functions + +# usage: test_bisection max-diff bisect-option head ^prune... +# +# e.g. test_bisection 1 --bisect l1 ^l0 +# +test_bisection_diff() +{ + _max_diff=$1 + _bisect_option=$2 + shift 2 + _bisection=$(git-rev-list $_bisect_option "$@") + _list_size=$(git-rev-list "$@" | wc -l) + _head=$1 + shift 1 + _bisection_size=$(git-rev-list $_bisection "$@" | wc -l) + [ -n "$_list_size" -a -n "$_bisection_size" ] || + error "test_bisection_diff failed" + + # Test if bisection size is close to half of list size within + # tolerance. + # + _bisect_err=`expr $_list_size - $_bisection_size \* 2` + test "$_bisect_err" -lt 0 && _bisect_err=`expr 0 - $_bisect_err` + _bisect_err=`expr $_bisect_err / 2` ; # floor + + test_expect_success \ + "bisection diff $_bisect_option $_head $* <= $_max_diff" \ + 'test $_bisect_err -le $_max_diff' +} + +date >path0 +git-update-index --add path0 +save_tag tree git-write-tree +on_committer_date "1971-08-16 00:00:00" hide_error save_tag root unique_commit root tree +on_committer_date "1971-08-16 00:00:01" save_tag l0 unique_commit l0 tree -p root +on_committer_date "1971-08-16 00:00:02" save_tag l1 unique_commit l1 tree -p l0 +on_committer_date "1971-08-16 00:00:03" save_tag l2 unique_commit l2 tree -p l1 +on_committer_date "1971-08-16 00:00:04" save_tag a0 unique_commit a0 tree -p l2 +on_committer_date "1971-08-16 00:00:05" save_tag a1 unique_commit a1 tree -p a0 +on_committer_date "1971-08-16 00:00:06" save_tag b1 unique_commit b1 tree -p a0 +on_committer_date "1971-08-16 00:00:07" save_tag c1 unique_commit c1 tree -p b1 +on_committer_date "1971-08-16 00:00:08" save_tag b2 unique_commit b2 tree -p b1 +on_committer_date "1971-08-16 00:00:09" save_tag b3 unique_commit b2 tree -p b2 +on_committer_date "1971-08-16 00:00:10" save_tag c2 unique_commit c2 tree -p c1 -p b2 +on_committer_date "1971-08-16 00:00:11" save_tag c3 unique_commit c3 tree -p c2 +on_committer_date "1971-08-16 00:00:12" save_tag a2 unique_commit a2 tree -p a1 +on_committer_date "1971-08-16 00:00:13" save_tag a3 unique_commit a3 tree -p a2 +on_committer_date "1971-08-16 00:00:14" save_tag b4 unique_commit b4 tree -p b3 -p a3 +on_committer_date "1971-08-16 00:00:15" save_tag a4 unique_commit a4 tree -p a3 -p b4 -p c3 +on_committer_date "1971-08-16 00:00:16" save_tag l3 unique_commit l3 tree -p a4 +on_committer_date "1971-08-16 00:00:17" save_tag l4 unique_commit l4 tree -p l3 +on_committer_date "1971-08-16 00:00:18" save_tag l5 unique_commit l5 tree -p l4 +git-update-ref HEAD $(tag l5) + + +# E +# / \ +# e1 | +# | | +# e2 | +# | | +# e3 | +# | | +# e4 | +# | | +# | f1 +# | | +# | f2 +# | | +# | f3 +# | | +# | f4 +# | | +# e5 | +# | | +# e6 | +# | | +# e7 | +# | | +# e8 | +# \ / +# F + + +on_committer_date "1971-08-16 00:00:00" hide_error save_tag F unique_commit F tree +on_committer_date "1971-08-16 00:00:01" save_tag e8 unique_commit e8 tree -p F +on_committer_date "1971-08-16 00:00:02" save_tag e7 unique_commit e7 tree -p e8 +on_committer_date "1971-08-16 00:00:03" save_tag e6 unique_commit e6 tree -p e7 +on_committer_date "1971-08-16 00:00:04" save_tag e5 unique_commit e5 tree -p e6 +on_committer_date "1971-08-16 00:00:05" save_tag f4 unique_commit f4 tree -p F +on_committer_date "1971-08-16 00:00:06" save_tag f3 unique_commit f3 tree -p f4 +on_committer_date "1971-08-16 00:00:07" save_tag f2 unique_commit f2 tree -p f3 +on_committer_date "1971-08-16 00:00:08" save_tag f1 unique_commit f1 tree -p f2 +on_committer_date "1971-08-16 00:00:09" save_tag e4 unique_commit e4 tree -p e5 +on_committer_date "1971-08-16 00:00:10" save_tag e3 unique_commit e3 tree -p e4 +on_committer_date "1971-08-16 00:00:11" save_tag e2 unique_commit e2 tree -p e3 +on_committer_date "1971-08-16 00:00:12" save_tag e1 unique_commit e1 tree -p e2 +on_committer_date "1971-08-16 00:00:13" save_tag E unique_commit E tree -p e1 -p f1 + +on_committer_date "1971-08-16 00:00:00" hide_error save_tag U unique_commit U tree +on_committer_date "1971-08-16 00:00:01" save_tag u0 unique_commit u0 tree -p U +on_committer_date "1971-08-16 00:00:01" save_tag u1 unique_commit u1 tree -p u0 +on_committer_date "1971-08-16 00:00:02" save_tag u2 unique_commit u2 tree -p u0 +on_committer_date "1971-08-16 00:00:03" save_tag u3 unique_commit u3 tree -p u0 +on_committer_date "1971-08-16 00:00:04" save_tag u4 unique_commit u4 tree -p u0 +on_committer_date "1971-08-16 00:00:05" save_tag u5 unique_commit u5 tree -p u0 +on_committer_date "1971-08-16 00:00:06" save_tag V unique_commit V tree -p u1 -p u2 -p u3 -p u4 -p u5 + +test_sequence() +{ + _bisect_option=$1 + + test_bisection_diff 0 $_bisect_option l0 ^root + test_bisection_diff 0 $_bisect_option l1 ^root + test_bisection_diff 0 $_bisect_option l2 ^root + test_bisection_diff 0 $_bisect_option a0 ^root + test_bisection_diff 0 $_bisect_option a1 ^root + test_bisection_diff 0 $_bisect_option a2 ^root + test_bisection_diff 0 $_bisect_option a3 ^root + test_bisection_diff 0 $_bisect_option b1 ^root + test_bisection_diff 0 $_bisect_option b2 ^root + test_bisection_diff 0 $_bisect_option b3 ^root + test_bisection_diff 0 $_bisect_option c1 ^root + test_bisection_diff 0 $_bisect_option c2 ^root + test_bisection_diff 0 $_bisect_option c3 ^root + test_bisection_diff 0 $_bisect_option E ^F + test_bisection_diff 0 $_bisect_option e1 ^F + test_bisection_diff 0 $_bisect_option e2 ^F + test_bisection_diff 0 $_bisect_option e3 ^F + test_bisection_diff 0 $_bisect_option e4 ^F + test_bisection_diff 0 $_bisect_option e5 ^F + test_bisection_diff 0 $_bisect_option e6 ^F + test_bisection_diff 0 $_bisect_option e7 ^F + test_bisection_diff 0 $_bisect_option f1 ^F + test_bisection_diff 0 $_bisect_option f2 ^F + test_bisection_diff 0 $_bisect_option f3 ^F + test_bisection_diff 0 $_bisect_option f4 ^F + test_bisection_diff 0 $_bisect_option E ^F + + test_bisection_diff 1 $_bisect_option V ^U + test_bisection_diff 0 $_bisect_option V ^U ^u1 ^u2 ^u3 + test_bisection_diff 0 $_bisect_option u1 ^U + test_bisection_diff 0 $_bisect_option u2 ^U + test_bisection_diff 0 $_bisect_option u3 ^U + test_bisection_diff 0 $_bisect_option u4 ^U + test_bisection_diff 0 $_bisect_option u5 ^U + +# +# the following illustrates Linus' binary bug blatt idea. +# +# assume the bug is actually at l3, but you don't know that - all you know is that l3 is broken +# and it wasn't broken before +# +# keep bisecting the list, advancing the "bad" head and accumulating "good" heads until +# the bisection point is the head - this is the bad point. +# + +test_output_expect_success "--bisect l5 ^root" 'git-rev-list $_bisect_option l5 ^root' <<EOF +c3 +EOF + +test_output_expect_success "$_bisect_option l5 ^root ^c3" 'git-rev-list $_bisect_option l5 ^root ^c3' <<EOF +b4 +EOF + +test_output_expect_success "$_bisect_option l5 ^root ^c3 ^b4" 'git-rev-list $_bisect_option l5 ^c3 ^b4' <<EOF +l3 +EOF + +test_output_expect_success "$_bisect_option l3 ^root ^c3 ^b4" 'git-rev-list $_bisect_option l3 ^root ^c3 ^b4' <<EOF +a4 +EOF + +test_output_expect_success "$_bisect_option l5 ^b3 ^a3 ^b4 ^a4" 'git-rev-list $_bisect_option l3 ^b3 ^a3 ^a4' <<EOF +l3 +EOF + +# +# if l3 is bad, then l4 is bad too - so advance the bad pointer by making b4 the known bad head +# + +test_output_expect_success "$_bisect_option l4 ^a2 ^a3 ^b ^a4" 'git-rev-list $_bisect_option l4 ^a2 ^a3 ^a4' <<EOF +l3 +EOF + +test_output_expect_success "$_bisect_option l3 ^a2 ^a3 ^b ^a4" 'git-rev-list $_bisect_option l3 ^a2 ^a3 ^a4' <<EOF +l3 +EOF + +# found! + +# +# as another example, let's consider a4 to be the bad head, in which case +# + +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git-rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF +c2 +EOF + +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2" 'git-rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2' <<EOF +c3 +EOF + +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3" 'git-rev-list $_bisect_option a4 ^a2 ^a3 ^b4 ^c2 ^c3' <<EOF +a4 +EOF + +# found! + +# +# or consider c3 to be the bad head +# + +test_output_expect_success "$_bisect_option a4 ^a2 ^a3 ^b4" 'git-rev-list $_bisect_option a4 ^a2 ^a3 ^b4' <<EOF +c2 +EOF + +test_output_expect_success "$_bisect_option c3 ^a2 ^a3 ^b4 ^c2" 'git-rev-list $_bisect_option c3 ^a2 ^a3 ^b4 ^c2' <<EOF +c3 +EOF + +# found! + +} + +test_sequence "--bisect" + +# +# +test_done diff --git a/t/t6003-rev-list-topo-order.sh b/t/t6003-rev-list-topo-order.sh new file mode 100755 index 0000000000..d99a9ad39e --- /dev/null +++ b/t/t6003-rev-list-topo-order.sh @@ -0,0 +1,408 @@ +#!/bin/sh +# +# Copyright (c) 2005 Jon Seymour +# + +test_description='Tests git-rev-list --topo-order functionality' + +. ./test-lib.sh +. ../t6000lib.sh # t6xxx specific functions + +list_duplicates() +{ + "$@" | sort | uniq -d +} + +date >path0 +git-update-index --add path0 +save_tag tree git-write-tree +on_committer_date "1971-08-16 00:00:00" hide_error save_tag root unique_commit root tree +on_committer_date "1971-08-16 00:00:01" save_tag l0 unique_commit l0 tree -p root +on_committer_date "1971-08-16 00:00:02" save_tag l1 unique_commit l1 tree -p l0 +on_committer_date "1971-08-16 00:00:03" save_tag l2 unique_commit l2 tree -p l1 +on_committer_date "1971-08-16 00:00:04" save_tag a0 unique_commit a0 tree -p l2 +on_committer_date "1971-08-16 00:00:05" save_tag a1 unique_commit a1 tree -p a0 +on_committer_date "1971-08-16 00:00:06" save_tag b1 unique_commit b1 tree -p a0 +on_committer_date "1971-08-16 00:00:07" save_tag c1 unique_commit c1 tree -p b1 +on_committer_date "1971-08-16 00:00:08" as_author foobar@example.com save_tag b2 unique_commit b2 tree -p b1 +on_committer_date "1971-08-16 00:00:09" save_tag b3 unique_commit b3 tree -p b2 +on_committer_date "1971-08-16 00:00:10" save_tag c2 unique_commit c2 tree -p c1 -p b2 +on_committer_date "1971-08-16 00:00:11" save_tag c3 unique_commit c3 tree -p c2 +on_committer_date "1971-08-16 00:00:12" save_tag a2 unique_commit a2 tree -p a1 +on_committer_date "1971-08-16 00:00:13" save_tag a3 unique_commit a3 tree -p a2 +on_committer_date "1971-08-16 00:00:14" save_tag b4 unique_commit b4 tree -p b3 -p a3 +on_committer_date "1971-08-16 00:00:15" save_tag a4 unique_commit a4 tree -p a3 -p b4 -p c3 +on_committer_date "1971-08-16 00:00:16" save_tag l3 unique_commit l3 tree -p a4 +on_committer_date "1971-08-16 00:00:17" save_tag l4 unique_commit l4 tree -p l3 +on_committer_date "1971-08-16 00:00:18" save_tag l5 unique_commit l5 tree -p l4 +on_committer_date "1971-08-16 00:00:19" save_tag m1 unique_commit m1 tree -p a4 -p c3 +on_committer_date "1971-08-16 00:00:20" save_tag m2 unique_commit m2 tree -p c3 -p a4 +on_committer_date "1971-08-16 00:00:21" hide_error save_tag alt_root unique_commit alt_root tree +on_committer_date "1971-08-16 00:00:22" save_tag r0 unique_commit r0 tree -p alt_root +on_committer_date "1971-08-16 00:00:23" save_tag r1 unique_commit r1 tree -p r0 +on_committer_date "1971-08-16 00:00:24" save_tag l5r1 unique_commit l5r1 tree -p l5 -p r1 +on_committer_date "1971-08-16 00:00:25" save_tag r1l5 unique_commit r1l5 tree -p r1 -p l5 + + +# +# note: as of 20/6, it isn't possible to create duplicate parents, so this +# can't be tested. +# +#on_committer_date "1971-08-16 00:00:20" save_tag m3 unique_commit m3 tree -p c3 -p a4 -p c3 +hide_error save_tag e1 as_author e@example.com unique_commit e1 tree +save_tag e2 as_author e@example.com unique_commit e2 tree -p e1 +save_tag f1 as_author f@example.com unique_commit f1 tree -p e1 +save_tag e3 as_author e@example.com unique_commit e3 tree -p e2 +save_tag f2 as_author f@example.com unique_commit f2 tree -p f1 +save_tag e4 as_author e@example.com unique_commit e4 tree -p e3 -p f2 +save_tag e5 as_author e@example.com unique_commit e5 tree -p e4 +save_tag f3 as_author f@example.com unique_commit f3 tree -p f2 +save_tag f4 as_author f@example.com unique_commit f4 tree -p f3 +save_tag e6 as_author e@example.com unique_commit e6 tree -p e5 -p f4 +save_tag f5 as_author f@example.com unique_commit f5 tree -p f4 +save_tag f6 as_author f@example.com unique_commit f6 tree -p f5 -p e6 +save_tag e7 as_author e@example.com unique_commit e7 tree -p e6 +save_tag e8 as_author e@example.com unique_commit e8 tree -p e7 +save_tag e9 as_author e@example.com unique_commit e9 tree -p e8 +save_tag f7 as_author f@example.com unique_commit f7 tree -p f6 +save_tag f8 as_author f@example.com unique_commit f8 tree -p f7 +save_tag f9 as_author f@example.com unique_commit f9 tree -p f8 +save_tag e10 as_author e@example.com unique_commit e1 tree -p e9 -p f8 + +hide_error save_tag g0 unique_commit g0 tree +save_tag g1 unique_commit g1 tree -p g0 +save_tag h1 unique_commit g2 tree -p g0 +save_tag g2 unique_commit g3 tree -p g1 -p h1 +save_tag h2 unique_commit g4 tree -p g2 +save_tag g3 unique_commit g5 tree -p g2 +save_tag g4 unique_commit g6 tree -p g3 -p h2 + +git-update-ref HEAD $(tag l5) + +test_output_expect_success 'rev-list has correct number of entries' 'git-rev-list HEAD | wc -l | tr -d \" \"' <<EOF +19 +EOF + +test_output_expect_success 'simple topo order' 'git-rev-list --topo-order HEAD' <<EOF +l5 +l4 +l3 +a4 +c3 +c2 +c1 +b4 +a3 +a2 +a1 +b3 +b2 +b1 +a0 +l2 +l1 +l0 +root +EOF + +test_output_expect_success 'two diamonds topo order (g6)' 'git-rev-list --topo-order g4' <<EOF +g4 +h2 +g3 +g2 +h1 +g1 +g0 +EOF + +test_output_expect_success 'multiple heads' 'git-rev-list --topo-order a3 b3 c3' <<EOF +a3 +a2 +a1 +c3 +c2 +c1 +b3 +b2 +b1 +a0 +l2 +l1 +l0 +root +EOF + +test_output_expect_success 'multiple heads, prune at a1' 'git-rev-list --topo-order a3 b3 c3 ^a1' <<EOF +a3 +a2 +c3 +c2 +c1 +b3 +b2 +b1 +EOF + +test_output_expect_success 'multiple heads, prune at l1' 'git-rev-list --topo-order a3 b3 c3 ^l1' <<EOF +a3 +a2 +a1 +c3 +c2 +c1 +b3 +b2 +b1 +a0 +l2 +EOF + +test_output_expect_success 'cross-epoch, head at l5, prune at l1' 'git-rev-list --topo-order l5 ^l1' <<EOF +l5 +l4 +l3 +a4 +c3 +c2 +c1 +b4 +a3 +a2 +a1 +b3 +b2 +b1 +a0 +l2 +EOF + +test_output_expect_success 'duplicated head arguments' 'git-rev-list --topo-order l5 l5 ^l1' <<EOF +l5 +l4 +l3 +a4 +c3 +c2 +c1 +b4 +a3 +a2 +a1 +b3 +b2 +b1 +a0 +l2 +EOF + +test_output_expect_success 'prune near topo' 'git-rev-list --topo-order a4 ^c3' <<EOF +a4 +b4 +a3 +a2 +a1 +b3 +EOF + +test_output_expect_success "head has no parent" 'git-rev-list --topo-order root' <<EOF +root +EOF + +test_output_expect_success "two nodes - one head, one base" 'git-rev-list --topo-order l0' <<EOF +l0 +root +EOF + +test_output_expect_success "three nodes one head, one internal, one base" 'git-rev-list --topo-order l1' <<EOF +l1 +l0 +root +EOF + +test_output_expect_success "linear prune l2 ^root" 'git-rev-list --topo-order l2 ^root' <<EOF +l2 +l1 +l0 +EOF + +test_output_expect_success "linear prune l2 ^l0" 'git-rev-list --topo-order l2 ^l0' <<EOF +l2 +l1 +EOF + +test_output_expect_success "linear prune l2 ^l1" 'git-rev-list --topo-order l2 ^l1' <<EOF +l2 +EOF + +test_output_expect_success "linear prune l5 ^a4" 'git-rev-list --topo-order l5 ^a4' <<EOF +l5 +l4 +l3 +EOF + +test_output_expect_success "linear prune l5 ^l3" 'git-rev-list --topo-order l5 ^l3' <<EOF +l5 +l4 +EOF + +test_output_expect_success "linear prune l5 ^l4" 'git-rev-list --topo-order l5 ^l4' <<EOF +l5 +EOF + +test_output_expect_success "max-count 10 - topo order" 'git-rev-list --topo-order --max-count=10 l5' <<EOF +l5 +l4 +l3 +a4 +c3 +c2 +c1 +b4 +a3 +a2 +EOF + +test_output_expect_success "max-count 10 - non topo order" 'git-rev-list --max-count=10 l5' <<EOF +l5 +l4 +l3 +a4 +b4 +a3 +a2 +c3 +c2 +b3 +EOF + +test_output_expect_success '--max-age=c3, no --topo-order' "git-rev-list --max-age=$(commit_date c3) l5" <<EOF +l5 +l4 +l3 +a4 +b4 +a3 +a2 +c3 +EOF + +# +# this test fails on --topo-order - a fix is required +# +#test_output_expect_success '--max-age=c3, --topo-order' "git-rev-list --topo-order --max-age=$(commit_date c3) l5" <<EOF +#l5 +#l4 +#l3 +#a4 +#c3 +#b4 +#a3 +#a2 +#EOF + +test_output_expect_success 'one specified head reachable from another a4, c3, --topo-order' "list_duplicates git-rev-list --topo-order a4 c3" <<EOF +EOF + +test_output_expect_success 'one specified head reachable from another c3, a4, --topo-order' "list_duplicates git-rev-list --topo-order c3 a4" <<EOF +EOF + +test_output_expect_success 'one specified head reachable from another a4, c3, no --topo-order' "list_duplicates git-rev-list a4 c3" <<EOF +EOF + +test_output_expect_success 'one specified head reachable from another c3, a4, no --topo-order' "list_duplicates git-rev-list c3 a4" <<EOF +EOF + +test_output_expect_success 'graph with c3 and a4 parents of head' "list_duplicates git-rev-list m1" <<EOF +EOF + +test_output_expect_success 'graph with a4 and c3 parents of head' "list_duplicates git-rev-list m2" <<EOF +EOF + +test_expect_success "head ^head --topo-order" 'git-rev-list --topo-order a3 ^a3' <<EOF +EOF + +test_expect_success "head ^head no --topo-order" 'git-rev-list a3 ^a3' <<EOF +EOF + +test_output_expect_success 'simple topo order (l5r1)' 'git-rev-list --topo-order l5r1' <<EOF +l5r1 +r1 +r0 +alt_root +l5 +l4 +l3 +a4 +c3 +c2 +c1 +b4 +a3 +a2 +a1 +b3 +b2 +b1 +a0 +l2 +l1 +l0 +root +EOF + +test_output_expect_success 'simple topo order (r1l5)' 'git-rev-list --topo-order r1l5' <<EOF +r1l5 +l5 +l4 +l3 +a4 +c3 +c2 +c1 +b4 +a3 +a2 +a1 +b3 +b2 +b1 +a0 +l2 +l1 +l0 +root +r1 +r0 +alt_root +EOF + +test_output_expect_success "don't print things unreachable from one branch" "git-rev-list a3 ^b3 --topo-order" <<EOF +a3 +a2 +a1 +EOF + +test_output_expect_success "--topo-order a4 l3" "git-rev-list --topo-order a4 l3" <<EOF +l3 +a4 +c3 +c2 +c1 +b4 +a3 +a2 +a1 +b3 +b2 +b1 +a0 +l2 +l1 +l0 +root +EOF + +# +# + +test_done diff --git a/t/t6004-rev-list-path-optim.sh b/t/t6004-rev-list-path-optim.sh new file mode 100755 index 0000000000..5182dbb158 --- /dev/null +++ b/t/t6004-rev-list-path-optim.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +test_description='git-rev-list trivial path optimization test' + +. ./test-lib.sh + +test_expect_success setup ' +echo Hello > a && +git add a && +git commit -m "Initial commit" a +' + +test_expect_success path-optimization ' + commit=$(echo "Unchanged tree" | git-commit-tree "HEAD^{tree}" -p HEAD) && + test $(git-rev-list $commit | wc -l) = 2 && + test $(git-rev-list $commit -- . | wc -l) = 1 +' + +test_done diff --git a/t/t6005-rev-list-count.sh b/t/t6005-rev-list-count.sh new file mode 100755 index 0000000000..334fccf58c --- /dev/null +++ b/t/t6005-rev-list-count.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +test_description='git-rev-list --max-count and --skip test' + +. ./test-lib.sh + +test_expect_success 'setup' ' + for n in 1 2 3 4 5 ; do \ + echo $n > a ; \ + git add a ; \ + git commit -m "$n" ; \ + done +' + +test_expect_success 'no options' ' + test $(git-rev-list HEAD | wc -l) = 5 +' + +test_expect_success '--max-count' ' + test $(git-rev-list HEAD --max-count=0 | wc -l) = 0 && + test $(git-rev-list HEAD --max-count=3 | wc -l) = 3 && + test $(git-rev-list HEAD --max-count=5 | wc -l) = 5 && + test $(git-rev-list HEAD --max-count=10 | wc -l) = 5 +' + +test_expect_success '--max-count all forms' ' + test $(git-rev-list HEAD --max-count=1 | wc -l) = 1 && + test $(git-rev-list HEAD -1 | wc -l) = 1 && + test $(git-rev-list HEAD -n1 | wc -l) = 1 && + test $(git-rev-list HEAD -n 1 | wc -l) = 1 +' + +test_expect_success '--skip' ' + test $(git-rev-list HEAD --skip=0 | wc -l) = 5 && + test $(git-rev-list HEAD --skip=3 | wc -l) = 2 && + test $(git-rev-list HEAD --skip=5 | wc -l) = 0 && + test $(git-rev-list HEAD --skip=10 | wc -l) = 0 +' + +test_expect_success '--skip --max-count' ' + test $(git-rev-list HEAD --skip=0 --max-count=0 | wc -l) = 0 && + test $(git-rev-list HEAD --skip=0 --max-count=10 | wc -l) = 5 && + test $(git-rev-list HEAD --skip=3 --max-count=0 | wc -l) = 0 && + test $(git-rev-list HEAD --skip=3 --max-count=1 | wc -l) = 1 && + test $(git-rev-list HEAD --skip=3 --max-count=2 | wc -l) = 2 && + test $(git-rev-list HEAD --skip=3 --max-count=10 | wc -l) = 2 && + test $(git-rev-list HEAD --skip=5 --max-count=10 | wc -l) = 0 && + test $(git-rev-list HEAD --skip=10 --max-count=10 | wc -l) = 0 +' + +test_done diff --git a/t/t6010-merge-base.sh b/t/t6010-merge-base.sh new file mode 100755 index 0000000000..b15920b852 --- /dev/null +++ b/t/t6010-merge-base.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Merge base computation. +' + +. ./test-lib.sh + +T=$(git-write-tree) + +M=1130000000 +Z=+0000 + +export GIT_COMMITTER_EMAIL=git@comm.iter.xz +export GIT_COMMITTER_NAME='C O Mmiter' +export GIT_AUTHOR_NAME='A U Thor' +export GIT_AUTHOR_EMAIL=git@au.thor.xz + +doit() { + OFFSET=$1; shift + NAME=$1; shift + PARENTS= + for P + do + PARENTS="${PARENTS}-p $P " + done + GIT_COMMITTER_DATE="$(($M + $OFFSET)) $Z" + GIT_AUTHOR_DATE=$GIT_COMMITTER_DATE + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE + commit=$(echo $NAME | git-commit-tree $T $PARENTS) + echo $commit >.git/refs/tags/$NAME + echo $commit +} + +# Setup... +E=$(doit 5 E) +D=$(doit 4 D $E) +F=$(doit 6 F $E) +C=$(doit 3 C $D) +B=$(doit 2 B $C) +A=$(doit 1 A $B) +G=$(doit 7 G $B $E) +H=$(doit 8 H $A $F) + +# Setup for second test to demonstrate that relying on timestamps in a +# distributed SCM to provide a _consistent_ partial ordering of commits +# leads to insanity. +# +# Relative +# Structure timestamps +# +# PL PR +4 +4 +# / \/ \ / \/ \ +# L2 C2 R2 +3 -1 +3 +# | | | | | | +# L1 C1 R1 +2 -2 +2 +# | | | | | | +# L0 C0 R0 +1 -3 +1 +# \ | / \ | / +# S 0 +# +# The left and right chains of commits can be of any length and complexity as +# long as all of the timestamps are greater than that of S. + +S=$(doit 0 S) + +C0=$(doit -3 C0 $S) +C1=$(doit -2 C1 $C0) +C2=$(doit -1 C2 $C1) + +L0=$(doit 1 L0 $S) +L1=$(doit 2 L1 $L0) +L2=$(doit 3 L2 $L1) + +R0=$(doit 1 R0 $S) +R1=$(doit 2 R1 $R0) +R2=$(doit 3 R2 $R1) + +PL=$(doit 4 PL $L2 $C2) +PR=$(doit 4 PR $C2 $R2) + +test_expect_success 'compute merge-base (single)' \ + 'MB=$(git-merge-base G H) && + expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/B"' + +test_expect_success 'compute merge-base (all)' \ + 'MB=$(git-merge-base --all G H) && + expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/B"' + +test_expect_success 'compute merge-base with show-branch' \ + 'MB=$(git-show-branch --merge-base G H) && + expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/B"' + +test_expect_success 'compute merge-base (single)' \ + 'MB=$(git-merge-base PL PR) && + expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/C2"' + +test_expect_success 'compute merge-base (all)' \ + 'MB=$(git-merge-base --all PL PR) && + expr "$(git-name-rev "$MB")" : "[0-9a-f]* tags/C2"' + +test_done diff --git a/t/t6020-merge-df.sh b/t/t6020-merge-df.sh new file mode 100755 index 0000000000..a19d49de28 --- /dev/null +++ b/t/t6020-merge-df.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# +# Copyright (c) 2005 Fredrik Kuivinen +# + +test_description='Test merge with directory/file conflicts' +. ./test-lib.sh + +test_expect_success 'prepare repository' \ +'echo "Hello" > init && +git add init && +git commit -m "Initial commit" && +git branch B && +mkdir dir && +echo "foo" > dir/foo && +git add dir/foo && +git commit -m "File: dir/foo" && +git checkout B && +echo "file dir" > dir && +git add dir && +git commit -m "File: dir"' + +test_expect_code 1 'Merge with d/f conflicts' 'git merge "merge msg" B master' + +test_done diff --git a/t/t6021-merge-criss-cross.sh b/t/t6021-merge-criss-cross.sh new file mode 100755 index 0000000000..499cafb882 --- /dev/null +++ b/t/t6021-merge-criss-cross.sh @@ -0,0 +1,92 @@ +#!/bin/sh +# +# Copyright (c) 2005 Fredrik Kuivinen +# + +# See http://marc.theaimsgroup.com/?l=git&m=111463358500362&w=2 for a +# nice description of what this is about. + + +test_description='Test criss-cross merge' +. ./test-lib.sh + +test_expect_success 'prepare repository' \ +'echo "1 +2 +3 +4 +5 +6 +7 +8 +9" > file && +git add file && +git commit -m "Initial commit" file && +git branch A && +git branch B && +git checkout A && +echo "1 +2 +3 +4 +5 +6 +7 +8 changed in B8, branch A +9" > file && +git commit -m "B8" file && +git checkout B && +echo "1 +2 +3 changed in C3, branch B +4 +5 +6 +7 +8 +9 +" > file && +git commit -m "C3" file && +git branch C3 && +git merge "pre E3 merge" B A && +echo "1 +2 +3 changed in E3, branch B. New file size +4 +5 +6 +7 +8 changed in B8, branch A +9 +" > file && +git commit -m "E3" file && +git checkout A && +git merge "pre D8 merge" A C3 && +echo "1 +2 +3 changed in C3, branch B +4 +5 +6 +7 +8 changed in D8, branch A. New file size 2 +9" > file && +git commit -m D8 file' + +test_expect_success 'Criss-cross merge' 'git merge "final merge" A B' + +cat > file-expect <<EOF +1 +2 +3 changed in E3, branch B. New file size +4 +5 +6 +7 +8 changed in D8, branch A. New file size 2 +9 +EOF + +test_expect_success 'Criss-cross merge result' 'cmp file file-expect' + +test_done diff --git a/t/t6022-merge-rename.sh b/t/t6022-merge-rename.sh new file mode 100755 index 0000000000..b608e202c1 --- /dev/null +++ b/t/t6022-merge-rename.sh @@ -0,0 +1,321 @@ +#!/bin/sh + +test_description='Merge-recursive merging renames' +. ./test-lib.sh + +test_expect_success setup \ +' +cat >A <<\EOF && +a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +b bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +c cccccccccccccccccccccccccccccccccccccccccccccccc +d dddddddddddddddddddddddddddddddddddddddddddddddd +e eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +f ffffffffffffffffffffffffffffffffffffffffffffffff +g gggggggggggggggggggggggggggggggggggggggggggggggg +h hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh +i iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii +j jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj +k kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +l llllllllllllllllllllllllllllllllllllllllllllllll +m mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +n nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn +o oooooooooooooooooooooooooooooooooooooooooooooooo +EOF + +cat >M <<\EOF && +A AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +B BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB +C CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +D DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD +E EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE +F FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +G GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +H HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH +I IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII +J JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ +K KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK +L LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL +M MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +N NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN +O OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO +EOF + +git add A M && +git commit -m "initial has A and M" && +git branch white && +git branch red && +git branch blue && +git branch yellow && + +sed -e "/^g /s/.*/g : master changes a line/" <A >A+ && +mv A+ A && +git commit -a -m "master updates A" && + +git checkout yellow && +rm -f M && +git commit -a -m "yellow removes M" && + +git checkout white && +sed -e "/^g /s/.*/g : white changes a line/" <A >B && +sed -e "/^G /s/.*/G : colored branch changes a line/" <M >N && +rm -f A M && +git update-index --add --remove A B M N && +git commit -m "white renames A->B, M->N" && + +git checkout red && +sed -e "/^g /s/.*/g : red changes a line/" <A >B && +sed -e "/^G /s/.*/G : colored branch changes a line/" <M >N && +rm -f A M && +git update-index --add --remove A B M N && +git commit -m "red renames A->B, M->N" && + +git checkout blue && +sed -e "/^g /s/.*/g : blue changes a line/" <A >C && +sed -e "/^G /s/.*/G : colored branch changes a line/" <M >N && +rm -f A M && +git update-index --add --remove A C M N && +git commit -m "blue renames A->C, M->N" && + +git checkout master' + +test_expect_success 'pull renaming branch into unrenaming one' \ +' + git show-branch + git pull . white && { + echo "BAD: should have conflicted" + return 1 + } + git ls-files -s + test "$(git ls-files -u B | wc -l)" -eq 3 || { + echo "BAD: should have left stages for B" + return 1 + } + test "$(git ls-files -s N | wc -l)" -eq 1 || { + echo "BAD: should have merged N" + return 1 + } + sed -ne "/^g/{ + p + q + }" B | grep master || { + echo "BAD: should have listed our change first" + return 1 + } + test "$(git diff white N | wc -l)" -eq 0 || { + echo "BAD: should have taken colored branch" + return 1 + } +' + +test_expect_success 'pull renaming branch into another renaming one' \ +' + rm -f B + git reset --hard + git checkout red + git pull . white && { + echo "BAD: should have conflicted" + return 1 + } + test "$(git ls-files -u B | wc -l)" -eq 3 || { + echo "BAD: should have left stages" + return 1 + } + test "$(git ls-files -s N | wc -l)" -eq 1 || { + echo "BAD: should have merged N" + return 1 + } + sed -ne "/^g/{ + p + q + }" B | grep red || { + echo "BAD: should have listed our change first" + return 1 + } + test "$(git diff white N | wc -l)" -eq 0 || { + echo "BAD: should have taken colored branch" + return 1 + } +' + +test_expect_success 'pull unrenaming branch into renaming one' \ +' + git reset --hard + git show-branch + git pull . master && { + echo "BAD: should have conflicted" + return 1 + } + test "$(git ls-files -u B | wc -l)" -eq 3 || { + echo "BAD: should have left stages" + return 1 + } + test "$(git ls-files -s N | wc -l)" -eq 1 || { + echo "BAD: should have merged N" + return 1 + } + sed -ne "/^g/{ + p + q + }" B | grep red || { + echo "BAD: should have listed our change first" + return 1 + } + test "$(git diff white N | wc -l)" -eq 0 || { + echo "BAD: should have taken colored branch" + return 1 + } +' + +test_expect_success 'pull conflicting renames' \ +' + git reset --hard + git show-branch + git pull . blue && { + echo "BAD: should have conflicted" + return 1 + } + test "$(git ls-files -u A | wc -l)" -eq 1 || { + echo "BAD: should have left a stage" + return 1 + } + test "$(git ls-files -u B | wc -l)" -eq 1 || { + echo "BAD: should have left a stage" + return 1 + } + test "$(git ls-files -u C | wc -l)" -eq 1 || { + echo "BAD: should have left a stage" + return 1 + } + test "$(git ls-files -s N | wc -l)" -eq 1 || { + echo "BAD: should have merged N" + return 1 + } + sed -ne "/^g/{ + p + q + }" B | grep red || { + echo "BAD: should have listed our change first" + return 1 + } + test "$(git diff white N | wc -l)" -eq 0 || { + echo "BAD: should have taken colored branch" + return 1 + } +' + +test_expect_success 'interference with untracked working tree file' ' + + git reset --hard + git show-branch + echo >A this file should not matter + git pull . white && { + echo "BAD: should have conflicted" + return 1 + } + test -f A || { + echo "BAD: should have left A intact" + return 1 + } +' + +test_expect_success 'interference with untracked working tree file' ' + + git reset --hard + git checkout white + git show-branch + rm -f A + echo >A this file should not matter + git pull . red && { + echo "BAD: should have conflicted" + return 1 + } + test -f A || { + echo "BAD: should have left A intact" + return 1 + } +' + +test_expect_success 'interference with untracked working tree file' ' + + git reset --hard + rm -f A M + git checkout -f master + git tag -f anchor + git show-branch + git pull . yellow || { + echo "BAD: should have cleanly merged" + return 1 + } + test -f M && { + echo "BAD: should have removed M" + return 1 + } + git reset --hard anchor +' + +test_expect_success 'updated working tree file should prevent the merge' ' + + git reset --hard + rm -f A M + git checkout -f master + git tag -f anchor + git show-branch + echo >>M one line addition + cat M >M.saved + git pull . yellow && { + echo "BAD: should have complained" + return 1 + } + diff M M.saved || { + echo "BAD: should have left M intact" + return 1 + } + rm -f M.saved +' + +test_expect_success 'updated working tree file should prevent the merge' ' + + git reset --hard + rm -f A M + git checkout -f master + git tag -f anchor + git show-branch + echo >>M one line addition + cat M >M.saved + git update-index M + git pull . yellow && { + echo "BAD: should have complained" + return 1 + } + diff M M.saved || { + echo "BAD: should have left M intact" + return 1 + } + rm -f M.saved +' + +test_expect_success 'interference with untracked working tree file' ' + + git reset --hard + rm -f A M + git checkout -f yellow + git tag -f anchor + git show-branch + echo >M this file should not matter + git pull . master || { + echo "BAD: should have cleanly merged" + return 1 + } + test -f M || { + echo "BAD: should have left M intact" + return 1 + } + git ls-files -s | grep M && { + echo "BAD: M must be untracked in the result" + return 1 + } + git reset --hard anchor +' + +test_done diff --git a/t/t6023-merge-file.sh b/t/t6023-merge-file.sh new file mode 100644 index 0000000000..f3cd3dba4d --- /dev/null +++ b/t/t6023-merge-file.sh @@ -0,0 +1,138 @@ +#!/bin/sh + +test_description='RCS merge replacement: merge-file' +. ./test-lib.sh + +cat > orig.txt << EOF +Dominus regit me, +et nihil mihi deerit. +In loco pascuae ibi me collocavit, +super aquam refectionis educavit me; +animam meam convertit, +deduxit me super semitas jusitiae, +propter nomen suum. +EOF + +cat > new1.txt << EOF +Dominus regit me, +et nihil mihi deerit. +In loco pascuae ibi me collocavit, +super aquam refectionis educavit me; +animam meam convertit, +deduxit me super semitas jusitiae, +propter nomen suum. +Nam et si ambulavero in medio umbrae mortis, +non timebo mala, quoniam tu mecum es: +virga tua et baculus tuus ipsa me consolata sunt. +EOF + +cat > new2.txt << EOF +Dominus regit me, et nihil mihi deerit. +In loco pascuae ibi me collocavit, +super aquam refectionis educavit me; +animam meam convertit, +deduxit me super semitas jusitiae, +propter nomen suum. +EOF + +cat > new3.txt << EOF +DOMINUS regit me, +et nihil mihi deerit. +In loco pascuae ibi me collocavit, +super aquam refectionis educavit me; +animam meam convertit, +deduxit me super semitas jusitiae, +propter nomen suum. +EOF + +cat > new4.txt << EOF +Dominus regit me, et nihil mihi deerit. +In loco pascuae ibi me collocavit, +super aquam refectionis educavit me; +animam meam convertit, +deduxit me super semitas jusitiae, +EOF +printf "propter nomen suum." >> new4.txt + +cp new1.txt test.txt +test_expect_success "merge without conflict" \ + "git-merge-file test.txt orig.txt new2.txt" + +cp new1.txt test2.txt +test_expect_success "merge without conflict (missing LF at EOF)" \ + "git-merge-file test2.txt orig.txt new2.txt" + +test_expect_success "merge result added missing LF" \ + "diff -u test.txt test2.txt" + +cp test.txt backup.txt +test_expect_failure "merge with conflicts" \ + "git-merge-file test.txt orig.txt new3.txt" + +cat > expect.txt << EOF +<<<<<<< test.txt +Dominus regit me, et nihil mihi deerit. +======= +DOMINUS regit me, +et nihil mihi deerit. +>>>>>>> new3.txt +In loco pascuae ibi me collocavit, +super aquam refectionis educavit me; +animam meam convertit, +deduxit me super semitas jusitiae, +propter nomen suum. +Nam et si ambulavero in medio umbrae mortis, +non timebo mala, quoniam tu mecum es: +virga tua et baculus tuus ipsa me consolata sunt. +EOF + +test_expect_success "expected conflict markers" "diff -u test.txt expect.txt" + +cp backup.txt test.txt +test_expect_failure "merge with conflicts, using -L" \ + "git-merge-file -L 1 -L 2 test.txt orig.txt new3.txt" + +cat > expect.txt << EOF +<<<<<<< 1 +Dominus regit me, et nihil mihi deerit. +======= +DOMINUS regit me, +et nihil mihi deerit. +>>>>>>> new3.txt +In loco pascuae ibi me collocavit, +super aquam refectionis educavit me; +animam meam convertit, +deduxit me super semitas jusitiae, +propter nomen suum. +Nam et si ambulavero in medio umbrae mortis, +non timebo mala, quoniam tu mecum es: +virga tua et baculus tuus ipsa me consolata sunt. +EOF + +test_expect_success "expected conflict markers, with -L" \ + "diff -u test.txt expect.txt" + +sed "s/ tu / TU /" < new1.txt > new5.txt +test_expect_failure "conflict in removed tail" \ + "git-merge-file -p orig.txt new1.txt new5.txt > out" + +cat > expect << EOF +Dominus regit me, +et nihil mihi deerit. +In loco pascuae ibi me collocavit, +super aquam refectionis educavit me; +animam meam convertit, +deduxit me super semitas jusitiae, +propter nomen suum. +<<<<<<< orig.txt +======= +Nam et si ambulavero in medio umbrae mortis, +non timebo mala, quoniam TU mecum es: +virga tua et baculus tuus ipsa me consolata sunt. +>>>>>>> new5.txt +EOF + +test_expect_success "expected conflict markers" "diff -u expect out" + +test_done + diff --git a/t/t6023-merge-rename-nocruft.sh b/t/t6023-merge-rename-nocruft.sh new file mode 100755 index 0000000000..65be95fbaa --- /dev/null +++ b/t/t6023-merge-rename-nocruft.sh @@ -0,0 +1,139 @@ +#!/bin/sh + +test_description='Merge-recursive merging renames' +. ./test-lib.sh + +test_expect_success setup \ +' +cat >A <<\EOF && +a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +b bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +c cccccccccccccccccccccccccccccccccccccccccccccccc +d dddddddddddddddddddddddddddddddddddddddddddddddd +e eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +f ffffffffffffffffffffffffffffffffffffffffffffffff +g gggggggggggggggggggggggggggggggggggggggggggggggg +h hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh +i iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii +j jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj +k kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +l llllllllllllllllllllllllllllllllllllllllllllllll +m mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm +n nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn +o oooooooooooooooooooooooooooooooooooooooooooooooo +EOF + +cat >M <<\EOF && +A AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +B BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB +C CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +D DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD +E EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE +F FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +G GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG +H HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH +I IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII +J JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ +K KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK +L LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL +M MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +N NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN +O OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO +EOF + +git add A M && +git commit -m "initial has A and M" && +git branch white && +git branch red && +git branch blue && + +git checkout white && +sed -e "/^g /s/.*/g : white changes a line/" <A >B && +sed -e "/^G /s/.*/G : colored branch changes a line/" <M >N && +rm -f A M && +git update-index --add --remove A B M N && +git commit -m "white renames A->B, M->N" && + +git checkout red && +echo created by red >R && +git update-index --add R && +git commit -m "red creates R" && + +git checkout blue && +sed -e "/^o /s/.*/g : blue changes a line/" <A >B && +rm -f A && +mv B A && +git update-index A && +git commit -m "blue modify A" && + +git checkout master' + +# This test broke in 65ac6e9c3f47807cb603af07a6a9e1a43bc119ae +test_expect_success 'merge white into red (A->B,M->N)' \ +' + git checkout -b red-white red && + git merge white && + git write-tree >/dev/null || { + echo "BAD: merge did not complete" + return 1 + } + + test -f B || { + echo "BAD: B does not exist in working directory" + return 1 + } + test -f N || { + echo "BAD: N does not exist in working directory" + return 1 + } + test -f R || { + echo "BAD: R does not exist in working directory" + return 1 + } + + test -f A && { + echo "BAD: A still exists in working directory" + return 1 + } + test -f M && { + echo "BAD: M still exists in working directory" + return 1 + } + return 0 +' + +# This test broke in 8371234ecaaf6e14fe3f2082a855eff1bbd79ae9 +test_expect_success 'merge blue into white (A->B, mod A, A untracked)' \ +' + git checkout -b white-blue white && + echo dirty >A && + git merge blue && + git write-tree >/dev/null || { + echo "BAD: merge did not complete" + return 1 + } + + test -f A || { + echo "BAD: A does not exist in working directory" + return 1 + } + test `cat A` = dirty || { + echo "BAD: A content is wrong" + return 1 + } + test -f B || { + echo "BAD: B does not exist in working directory" + return 1 + } + test -f N || { + echo "BAD: N does not exist in working directory" + return 1 + } + test -f M && { + echo "BAD: M still exists in working directory" + return 1 + } + return 0 +' + +test_done diff --git a/t/t6024-recursive-merge.sh b/t/t6024-recursive-merge.sh new file mode 100644 index 0000000000..31b96257b4 --- /dev/null +++ b/t/t6024-recursive-merge.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +test_description='Test merge without common ancestors' +. ./test-lib.sh + +# This scenario is based on a real-world repository of Shawn Pearce. + +# 1 - A - D - F +# \ X / +# B X +# X \ +# 2 - C - E - G + +GIT_COMMITTER_DATE="2006-12-12 23:28:00 +0100" +export GIT_COMMITTER_DATE + +test_expect_success "setup tests" ' +echo 1 > a1 && +git add a1 && +GIT_AUTHOR_DATE="2006-12-12 23:00:00" git commit -m 1 a1 && + +git checkout -b A master && +echo A > a1 && +GIT_AUTHOR_DATE="2006-12-12 23:00:01" git commit -m A a1 && + +git checkout -b B master && +echo B > a1 && +GIT_AUTHOR_DATE="2006-12-12 23:00:02" git commit -m B a1 && + +git checkout -b D A && +git-rev-parse B > .git/MERGE_HEAD && +echo D > a1 && +git update-index a1 && +GIT_AUTHOR_DATE="2006-12-12 23:00:03" git commit -m D && + +git symbolic-ref HEAD refs/heads/other && +echo 2 > a1 && +GIT_AUTHOR_DATE="2006-12-12 23:00:04" git commit -m 2 a1 && + +git checkout -b C && +echo C > a1 && +GIT_AUTHOR_DATE="2006-12-12 23:00:05" git commit -m C a1 && + +git checkout -b E C && +git-rev-parse B > .git/MERGE_HEAD && +echo E > a1 && +git update-index a1 && +GIT_AUTHOR_DATE="2006-12-12 23:00:06" git commit -m E && + +git checkout -b G E && +git-rev-parse A > .git/MERGE_HEAD && +echo G > a1 && +git update-index a1 && +GIT_AUTHOR_DATE="2006-12-12 23:00:07" git commit -m G && + +git checkout -b F D && +git-rev-parse C > .git/MERGE_HEAD && +echo F > a1 && +git update-index a1 && +GIT_AUTHOR_DATE="2006-12-12 23:00:08" git commit -m F +' + +test_expect_failure "combined merge conflicts" "git merge -m final G" + +cat > expect << EOF +<<<<<<< HEAD:a1 +F +======= +G +>>>>>>> G:a1 +EOF + +test_expect_success "result contains a conflict" "diff -u expect a1" + +git ls-files --stage > out +cat > expect << EOF +100644 da056ce14a2241509897fa68bb2b3b6e6194ef9e 1 a1 +100644 cf84443e49e1b366fac938711ddf4be2d4d1d9e9 2 a1 +100644 fd7923529855d0b274795ae3349c5e0438333979 3 a1 +EOF + +test_expect_success "virtual trees were processed" "diff -u expect out" + +test_done diff --git a/t/t6101-rev-parse-parents.sh b/t/t6101-rev-parse-parents.sh new file mode 100755 index 0000000000..7d354a1fae --- /dev/null +++ b/t/t6101-rev-parse-parents.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# +# Copyright (c) 2005 Johannes Schindelin +# + +test_description='Test git-rev-parse with different parent options' + +. ./test-lib.sh +. ../t6000lib.sh # t6xxx specific functions + +date >path0 +git-update-index --add path0 +save_tag tree git-write-tree +hide_error save_tag start unique_commit "start" tree +save_tag second unique_commit "second" tree -p start +hide_error save_tag start2 unique_commit "start2" tree +save_tag two_parents unique_commit "next" tree -p second -p start2 +save_tag final unique_commit "final" tree -p two_parents + +test_expect_success 'start is valid' 'git-rev-parse start | grep "^[0-9a-f]\{40\}$"' +test_expect_success 'start^0' "test $(cat .git/refs/tags/start) = $(git-rev-parse start^0)" +test_expect_success 'start^1 not valid' "if git-rev-parse --verify start^1; then false; else :; fi" +test_expect_success 'second^1 = second^' "test $(git-rev-parse second^1) = $(git-rev-parse second^)" +test_expect_success 'final^1^1^1' "test $(git-rev-parse start) = $(git-rev-parse final^1^1^1)" +test_expect_success 'final^1^1^1 = final^^^' "test $(git-rev-parse final^1^1^1) = $(git-rev-parse final^^^)" +test_expect_success 'final^1^2' "test $(git-rev-parse start2) = $(git-rev-parse final^1^2)" +test_expect_success 'final^1^2 != final^1^1' "test $(git-rev-parse final^1^2) != $(git-rev-parse final^1^1)" +test_expect_success 'final^1^3 not valid' "if git-rev-parse --verify final^1^3; then false; else :; fi" +test_expect_failure '--verify start2^1' 'git-rev-parse --verify start2^1' +test_expect_success '--verify start2^0' 'git-rev-parse --verify start2^0' + +test_done + diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh new file mode 100755 index 0000000000..3e9edda1ca --- /dev/null +++ b/t/t6120-describe.sh @@ -0,0 +1,97 @@ +#!/bin/sh + +test_description='test describe + + B + .--------------o----o----o----x + / / / + o----o----o----o----o----. / + \ A c / + .------------o---o---o + D e +' +. ./test-lib.sh + +check_describe () { + expect="$1" + shift + R=$(git describe "$@") && + test_expect_success "describe $*" ' + case "$R" in + $expect) echo happy ;; + *) echo "Oops - $R is not $expect"; + false ;; + esac + ' +} + +test_expect_success setup ' + + test_tick && + echo one >file && git-add file && git-commit -m initial && + one=$(git-rev-parse HEAD) && + + test_tick && + echo two >file && git-add file && git-commit -m second && + two=$(git-rev-parse HEAD) && + + test_tick && + echo three >file && git-add file && git-commit -m third && + + test_tick && + echo A >file && git-add file && git-commit -m A && + test_tick && + git-tag -a -m A A && + + test_tick && + echo c >file && git-add file && git-commit -m c && + test_tick && + git-tag c && + + git reset --hard $two && + test_tick && + echo B >side && git-add side && git-commit -m B && + test_tick && + git-tag -a -m B B && + + test_tick && + git-merge -m Merged c && + merged=$(git-rev-parse HEAD) && + + git reset --hard $two && + test_tick && + echo D >another && git-add another && git-commit -m D && + test_tick && + git-tag -a -m D D && + + test_tick && + echo DD >another && git commit -a -m another && + + test_tick && + git-tag e && + + test_tick && + echo DDD >another && git commit -a -m "yet another" && + + test_tick && + git-merge -m Merged $merged && + + test_tick && + echo X >file && echo X >side && git-add file side && + git-commit -m x + +' + +check_describe A-* HEAD +check_describe A-* HEAD^ +check_describe D-* HEAD^^ +check_describe A-* HEAD^^2 +check_describe B HEAD^^2^ + +check_describe A-* --tags HEAD +check_describe A-* --tags HEAD^ +check_describe D-* --tags HEAD^^ +check_describe A-* --tags HEAD^^2 +check_describe B --tags HEAD^^2^ + +test_done diff --git a/t/t6200-fmt-merge-msg.sh b/t/t6200-fmt-merge-msg.sh new file mode 100755 index 0000000000..ea14023616 --- /dev/null +++ b/t/t6200-fmt-merge-msg.sh @@ -0,0 +1,163 @@ +#!/bin/sh +# +# Copyright (c) 2006, Junio C Hamano +# + +test_description='fmt-merge-msg test' + +. ./test-lib.sh + +datestamp=1151939923 +setdate () { + GIT_COMMITTER_DATE="$datestamp +0200" + GIT_AUTHOR_DATE="$datestamp +0200" + datestamp=`expr "$datestamp" + 1` + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE +} + +test_expect_success setup ' + echo one >one && + git add one && + setdate && + git commit -m "Initial" && + + echo uno >one && + echo dos >two && + git add two && + setdate && + git commit -a -m "Second" && + + git checkout -b left && + + echo $datestamp >one && + setdate && + git commit -a -m "Common #1" && + + echo $datestamp >one && + setdate && + git commit -a -m "Common #2" && + + git branch right && + + echo $datestamp >two && + setdate && + git commit -a -m "Left #3" && + + echo $datestamp >two && + setdate && + git commit -a -m "Left #4" && + + echo $datestamp >two && + setdate && + git commit -a -m "Left #5" && + + git checkout right && + + echo $datestamp >three && + git add three && + setdate && + git commit -a -m "Right #3" && + + echo $datestamp >three && + setdate && + git commit -a -m "Right #4" && + + echo $datestamp >three && + setdate && + git commit -a -m "Right #5" && + + git show-branch +' + +cat >expected <<\EOF +Merge branch 'left' +EOF + +test_expect_success 'merge-msg test #1' ' + + git checkout master && + git fetch . left && + + git fmt-merge-msg <.git/FETCH_HEAD >actual && + diff -u actual expected +' + +cat >expected <<\EOF +Merge branch 'left' of ../trash +EOF + +test_expect_success 'merge-msg test #2' ' + + git checkout master && + git fetch ../trash left && + + git fmt-merge-msg <.git/FETCH_HEAD >actual && + diff -u actual expected +' + +cat >expected <<\EOF +Merge branch 'left' + +* left: + Left #5 + Left #4 + Left #3 + Common #2 + Common #1 +EOF + +test_expect_success 'merge-msg test #3' ' + + git config merge.summary true && + + git checkout master && + setdate && + git fetch . left && + + git fmt-merge-msg <.git/FETCH_HEAD >actual && + diff -u actual expected +' + +cat >expected <<\EOF +Merge branches 'left' and 'right' + +* left: + Left #5 + Left #4 + Left #3 + Common #2 + Common #1 + +* right: + Right #5 + Right #4 + Right #3 + Common #2 + Common #1 +EOF + +test_expect_success 'merge-msg test #4' ' + + git config merge.summary true && + + git checkout master && + setdate && + git fetch . left right && + + git fmt-merge-msg <.git/FETCH_HEAD >actual && + diff -u actual expected +' + +test_expect_success 'merge-msg test #5' ' + + git config merge.summary yes && + + git checkout master && + setdate && + git fetch . left right && + + git fmt-merge-msg <.git/FETCH_HEAD >actual && + diff -u actual expected +' + +test_done diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh new file mode 100755 index 0000000000..344033249c --- /dev/null +++ b/t/t7001-mv.sh @@ -0,0 +1,121 @@ +#!/bin/sh + +test_description='git-mv in subdirs' +. ./test-lib.sh + +test_expect_success \ + 'prepare reference tree' \ + 'mkdir path0 path1 && + cp ../../COPYING path0/COPYING && + git-add path0/COPYING && + git-commit -m add -a' + +test_expect_success \ + 'moving the file out of subdirectory' \ + 'cd path0 && git-mv COPYING ../path1/COPYING' + +# in path0 currently +test_expect_success \ + 'commiting the change' \ + 'cd .. && git-commit -m move-out -a' + +test_expect_success \ + 'checking the commit' \ + 'git-diff-tree -r -M --name-status HEAD^ HEAD | \ + grep -E "^R100.+path0/COPYING.+path1/COPYING"' + +test_expect_success \ + 'moving the file back into subdirectory' \ + 'cd path0 && git-mv ../path1/COPYING COPYING' + +# in path0 currently +test_expect_success \ + 'commiting the change' \ + 'cd .. && git-commit -m move-in -a' + +test_expect_success \ + 'checking the commit' \ + 'git-diff-tree -r -M --name-status HEAD^ HEAD | \ + grep -E "^R100.+path1/COPYING.+path0/COPYING"' + +test_expect_success \ + 'adding another file' \ + 'cp ../../README path0/README && + git-add path0/README && + git-commit -m add2 -a' + +test_expect_success \ + 'moving whole subdirectory' \ + 'git-mv path0 path2' + +test_expect_success \ + 'commiting the change' \ + 'git-commit -m dir-move -a' + +test_expect_success \ + 'checking the commit' \ + 'git-diff-tree -r -M --name-status HEAD^ HEAD | \ + grep -E "^R100.+path0/COPYING.+path2/COPYING" && + git-diff-tree -r -M --name-status HEAD^ HEAD | \ + grep -E "^R100.+path0/README.+path2/README"' + +test_expect_success \ + 'succeed when source is a prefix of destination' \ + 'git-mv path2/COPYING path2/COPYING-renamed' + +test_expect_success \ + 'moving whole subdirectory into subdirectory' \ + 'git-mv path2 path1' + +test_expect_success \ + 'commiting the change' \ + 'git-commit -m dir-move -a' + +test_expect_success \ + 'checking the commit' \ + 'git-diff-tree -r -M --name-status HEAD^ HEAD | \ + grep -E "^R100.+path2/COPYING.+path1/path2/COPYING" && + git-diff-tree -r -M --name-status HEAD^ HEAD | \ + grep -E "^R100.+path2/README.+path1/path2/README"' + +test_expect_failure \ + 'do not move directory over existing directory' \ + 'mkdir path0 && mkdir path0/path2 && git-mv path2 path0' + +test_expect_success \ + 'move into "."' \ + 'git-mv path1/path2/ .' + +test_expect_success "Michael Cassar's test case" ' + rm -fr .git papers partA && + git init && + mkdir -p papers/unsorted papers/all-papers partA && + echo a > papers/unsorted/Thesis.pdf && + echo b > partA/outline.txt && + echo c > papers/unsorted/_another && + git add papers partA && + T1=`git write-tree` && + + git mv papers/unsorted/Thesis.pdf papers/all-papers/moo-blah.pdf && + + T=`git write-tree` && + git ls-tree -r $T | grep partA/outline.txt || { + git ls-tree -r $T + (exit 1) + } +' + +rm -fr papers partA path? + +test_expect_success "Sergey Vlasov's test case" ' + rm -fr .git && + git init && + mkdir ab && + date >ab.c && + date >ab/d && + git add ab.c ab && + git commit -m 'initial' && + git mv ab a +' + +test_done diff --git a/t/t7002-grep.sh b/t/t7002-grep.sh new file mode 100755 index 0000000000..6bfb899ed1 --- /dev/null +++ b/t/t7002-grep.sh @@ -0,0 +1,112 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='git grep various. +' + +. ./test-lib.sh + +test_expect_success setup ' + { + echo foo mmap bar + echo foo_mmap bar + echo foo_mmap bar mmap + echo foo mmap bar_mmap + echo foo_mmap bar mmap baz + } >file && + echo x x xx x >x && + echo y yy >y && + echo zzz > z && + mkdir t && + echo test >t/t && + git add file x y z t/t && + git commit -m initial +' + +for H in HEAD '' +do + case "$H" in + HEAD) HC='HEAD:' L='HEAD' ;; + '') HC= L='in working tree' ;; + esac + + test_expect_success "grep -w $L" ' + { + echo ${HC}file:1:foo mmap bar + echo ${HC}file:3:foo_mmap bar mmap + echo ${HC}file:4:foo mmap bar_mmap + echo ${HC}file:5:foo_mmap bar mmap baz + } >expected && + git grep -n -w -e mmap $H >actual && + diff expected actual + ' + + test_expect_success "grep -w $L (x)" ' + { + echo ${HC}x:1:x x xx x + } >expected && + git grep -n -w -e "x xx* x" $H >actual && + diff expected actual + ' + + test_expect_success "grep -w $L (y-1)" ' + { + echo ${HC}y:1:y yy + } >expected && + git grep -n -w -e "^y" $H >actual && + diff expected actual + ' + + test_expect_success "grep -w $L (y-2)" ' + : >expected && + if git grep -n -w -e "^y y" $H >actual + then + echo should not have matched + cat actual + false + else + diff expected actual + fi + ' + + test_expect_success "grep -w $L (z)" ' + : >expected && + if git grep -n -w -e "^z" $H >actual + then + echo should not have matched + cat actual + false + else + diff expected actual + fi + ' + + test_expect_success "grep $L (t-1)" ' + echo "${HC}t/t:1:test" >expected && + git grep -n -e test $H >actual && + diff expected actual + ' + + test_expect_success "grep $L (t-2)" ' + echo "${HC}t:1:test" >expected && + ( + cd t && + git grep -n -e test $H + ) >actual && + diff expected actual + ' + + test_expect_success "grep $L (t-3)" ' + echo "${HC}t/t:1:test" >expected && + ( + cd t && + git grep --full-name -n -e test $H + ) >actual && + diff expected actual + ' + +done + +test_done diff --git a/t/t7101-reset.sh b/t/t7101-reset.sh new file mode 100755 index 0000000000..a9191407f2 --- /dev/null +++ b/t/t7101-reset.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# +# Copyright (c) 2006 Shawn Pearce +# + +test_description='git-reset should cull empty subdirs' +. ./test-lib.sh + +test_expect_success \ + 'creating initial files' \ + 'mkdir path0 && + cp ../../COPYING path0/COPYING && + git-add path0/COPYING && + git-commit -m add -a' + +test_expect_success \ + 'creating second files' \ + 'mkdir path1 && + mkdir path1/path2 && + cp ../../COPYING path1/path2/COPYING && + cp ../../COPYING path1/COPYING && + cp ../../COPYING COPYING && + cp ../../COPYING path0/COPYING-TOO && + git-add path1/path2/COPYING && + git-add path1/COPYING && + git-add COPYING && + git-add path0/COPYING-TOO && + git-commit -m change -a' + +test_expect_success \ + 'resetting tree HEAD^' \ + 'git-reset --hard HEAD^' + +test_expect_success \ + 'checking initial files exist after rewind' \ + 'test -d path0 && + test -f path0/COPYING' + +test_expect_failure \ + 'checking lack of path1/path2/COPYING' \ + 'test -f path1/path2/COPYING' + +test_expect_failure \ + 'checking lack of path1/COPYING' \ + 'test -f path1/COPYING' + +test_expect_failure \ + 'checking lack of COPYING' \ + 'test -f COPYING' + +test_expect_failure \ + 'checking checking lack of path1/COPYING-TOO' \ + 'test -f path0/COPYING-TOO' + +test_expect_failure \ + 'checking lack of path1/path2' \ + 'test -d path1/path2' + +test_expect_failure \ + 'checking lack of path1' \ + 'test -d path1' + +test_done diff --git a/t/t7201-co.sh b/t/t7201-co.sh new file mode 100755 index 0000000000..867bbd26cb --- /dev/null +++ b/t/t7201-co.sh @@ -0,0 +1,132 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +test_description='git-checkout tests.' + +. ./test-lib.sh + +fill () { + for i + do + echo "$i" + done +} + + +test_expect_success setup ' + + fill 1 2 3 4 5 6 7 8 >one && + fill a b c d e >two && + git add one two && + git commit -m "Initial A one, A two" && + + git checkout -b renamer && + rm -f one && + fill 1 3 4 5 6 7 8 >uno && + git add uno && + fill a b c d e f >two && + git commit -a -m "Renamer R one->uno, M two" && + + git checkout -b side master && + fill 1 2 3 4 5 6 7 >one && + fill A B C D E >three && + rm -f two && + git update-index --add --remove one two three && + git commit -m "Side M one, D two, A three" && + + git checkout master +' + +test_expect_success "checkout from non-existing branch" ' + + git checkout -b delete-me master && + rm .git/refs/heads/delete-me && + test refs/heads/delete-me = "$(git symbolic-ref HEAD)" && + git checkout master && + test refs/heads/master = "$(git symbolic-ref HEAD)" +' + +test_expect_success "checkout with dirty tree without -m" ' + + fill 0 1 2 3 4 5 6 7 8 >one && + if git checkout side + then + echo Not happy + false + else + echo "happy - failed correctly" + fi + +' + +test_expect_success "checkout -m with dirty tree" ' + + git checkout -f master && + git clean && + + fill 0 1 2 3 4 5 6 7 8 >one && + git checkout -m side && + + test "$(git symbolic-ref HEAD)" = "refs/heads/side" && + + fill "M one" "A three" "D two" >expect.master && + git diff --name-status master >current.master && + diff expect.master current.master && + + fill "M one" >expect.side && + git diff --name-status side >current.side && + diff expect.side current.side && + + : >expect.index && + git diff --cached >current.index && + diff expect.index current.index +' + +test_expect_success "checkout -m with dirty tree, renamed" ' + + git checkout -f master && git clean && + + fill 1 2 3 4 5 7 8 >one && + if git checkout renamer + then + echo Not happy + false + else + echo "happy - failed correctly" + fi && + + git checkout -m renamer && + fill 1 3 4 5 7 8 >expect && + diff expect uno && + ! test -f one && + git diff --cached >current && + ! test -s current + +' + +test_expect_success 'checkout -m with merge conflict' ' + + git checkout -f master && git clean && + + fill 1 T 3 4 5 6 S 8 >one && + if git checkout renamer + then + echo Not happy + false + else + echo "happy - failed correctly" + fi && + + git checkout -m renamer && + + git diff master:one :3:uno | + sed -e "1,/^@@/d" -e "/^ /d" -e "s/^-/d/" -e "s/^+/a/" >current && + fill d2 aT d7 aS >expect && + diff current expect && + git diff --cached two >current && + ! test -s current +' + +test_done diff --git a/t/t8001-annotate.sh b/t/t8001-annotate.sh new file mode 100755 index 0000000000..3a6490e8f8 --- /dev/null +++ b/t/t8001-annotate.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +test_description='git-annotate' +. ./test-lib.sh + +PROG='git annotate' +. ../annotate-tests.sh + +test_expect_success \ + 'Annotating an old revision works' \ + '[ $(git annotate file master | awk "{print \$3}" | grep -c "^A$") -eq 2 ] && \ + [ $(git annotate file master | awk "{print \$3}" | grep -c "^B$") -eq 2 ]' + + +test_done diff --git a/t/t8002-blame.sh b/t/t8002-blame.sh new file mode 100755 index 0000000000..9777393996 --- /dev/null +++ b/t/t8002-blame.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +test_description='git-blame' +. ./test-lib.sh + +PROG='git blame -c' +. ../annotate-tests.sh + +test_done diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh new file mode 100755 index 0000000000..e9ea33c18d --- /dev/null +++ b/t/t9001-send-email.sh @@ -0,0 +1,44 @@ +#!/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' ' + patches=`git format-patch -n HEAD^1` +' + +test_expect_success 'Send patches' ' + git send-email -from="Example <nobody@example.com>" --to=nobody@example.com --smtp-server="$(pwd)/fake.sendmail" $patches 2>errors +' + +cat >expected <<\EOF +!nobody@example.com! +!author@example.com! +EOF +test_expect_success \ + 'Verify commandline' \ + 'diff commandline expected' + +test_done diff --git a/t/t9100-git-svn-basic.sh b/t/t9100-git-svn-basic.sh new file mode 100755 index 0000000000..040da92756 --- /dev/null +++ b/t/t9100-git-svn-basic.sh @@ -0,0 +1,218 @@ +#!/bin/sh +# +# Copyright (c) 2006 Eric Wong +# + +test_description='git-svn basic tests' +GIT_SVN_LC_ALL=$LC_ALL + +case "$LC_ALL" in +*.UTF-8) + have_utf8=t + ;; +*) + have_utf8= + ;; +esac + +. ./lib-git-svn.sh + +echo 'define NO_SVN_TESTS to skip git-svn tests' + +test_expect_success \ + 'initialize git-svn' " + mkdir import && + cd import && + echo foo > foo && + ln -s foo foo.link + mkdir -p dir/a/b/c/d/e && + echo 'deep dir' > dir/a/b/c/d/e/file && + mkdir bar && + echo 'zzz' > bar/zzz && + echo '#!/bin/sh' > exec.sh && + chmod +x exec.sh && + svn import -m 'import for git-svn' . $svnrepo >/dev/null && + cd .. && + rm -rf import && + git-svn init $svnrepo" + +test_expect_success \ + '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' +test_expect_success "$name" " + 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 && + git commit -m '$name' && + git-svn set-tree --find-copies-harder --rmdir \ + remotes/git-svn..mybranch && + svn up '$SVN_TREE' && + test -d '$SVN_TREE'/dir && test ! -d '$SVN_TREE'/dir/a" + + +name='detect node change from file to directory #1' +test_expect_failure "$name" " + mkdir dir/new_file && + mv dir/file dir/new_file/file && + mv dir/new_file dir/file && + git update-index --remove dir/file && + git update-index --add dir/file/file && + git commit -m '$name' && + git-svn set-tree --find-copies-harder --rmdir \ + remotes/git-svn..mybranch" || true + + +name='detect node change from directory to file #1' +test_expect_failure "$name" " + rm -rf dir '$GIT_DIR'/index && + git checkout -f -b mybranch2 remotes/git-svn && + mv bar/zzz zzz && + rm -rf bar && + mv zzz bar && + git update-index --remove -- bar/zzz && + git update-index --add -- bar && + git commit -m '$name' && + git-svn set-tree --find-copies-harder --rmdir \ + remotes/git-svn..mybranch2" || true + + +name='detect node change from file to directory #2' +test_expect_failure "$name" " + rm -f '$GIT_DIR'/index && + git checkout -f -b mybranch3 remotes/git-svn && + rm bar/zzz && + git-update-index --remove bar/zzz && + mkdir bar/zzz && + echo yyy > bar/zzz/yyy && + git-update-index --add bar/zzz/yyy && + git commit -m '$name' && + git-svn set-tree --find-copies-harder --rmdir \ + remotes/git-svn..mybranch3" || true + + +name='detect node change from directory to file #2' +test_expect_failure "$name" " + rm -f '$GIT_DIR'/index && + git checkout -f -b mybranch4 remotes/git-svn && + rm -rf dir && + git update-index --remove -- dir/file && + touch dir && + echo asdf > dir && + git update-index --add -- dir && + git commit -m '$name' && + git-svn set-tree --find-copies-harder --rmdir \ + remotes/git-svn..mybranch4" || true + + +name='remove executable bit from a file' +test_expect_success "$name" " + rm -f '$GIT_DIR'/index && + git checkout -f -b mybranch5 remotes/git-svn && + chmod -x exec.sh && + git update-index exec.sh && + git commit -m '$name' && + git-svn set-tree --find-copies-harder --rmdir \ + remotes/git-svn..mybranch5 && + svn up '$SVN_TREE' && + test ! -x '$SVN_TREE'/exec.sh" + + +name='add executable bit back file' +test_expect_success "$name" " + chmod +x exec.sh && + git update-index exec.sh && + git commit -m '$name' && + git-svn set-tree --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)' +test_expect_success "$name" " + rm exec.sh && + ln -s bar/zzz exec.sh && + git update-index exec.sh && + git commit -m '$name' && + git-svn set-tree --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' + +test_expect_success "$name" " + chmod +x bar/zzz && + ln -s bar/zzz exec-2.sh && + git update-index --add bar/zzz exec-2.sh && + git commit -m '$name' && + git-svn set-tree --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' +test_expect_success "$name" " + echo git help > help || true && + rm exec-2.sh && + cp help exec-2.sh && + git update-index exec-2.sh && + git commit -m '$name' && + git-svn set-tree --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" + +if test "$have_utf8" = t +then + name="commit with UTF-8 message: locale: $GIT_SVN_LC_ALL" + LC_ALL="$GIT_SVN_LC_ALL" + export LC_ALL + test_expect_success "$name" " + echo '# hello' >> exec-2.sh && + git update-index exec-2.sh && + git commit -m 'éïâˆ' && + git-svn set-tree 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 +export GIT_SVN_ID +test_expect_success "$name" \ + "git-svn init $svnrepo && git-svn fetch && + git-rev-list --pretty=raw remotes/git-svn | grep ^tree | uniq > a && + git-rev-list --pretty=raw remotes/alt | grep ^tree | uniq > b && + diff -u a b" + +name='check imported tree checksums expected tree checksums' +rm -f expected +if test "$have_utf8" = t +then + echo tree bf522353586b1b883488f2bc73dab0d9f774b9a9 > expected +fi +cat >> expected <<\EOF +tree 83654bb36f019ae4fe77a0171f81075972087624 +tree 031b8d557afc6fea52894eaebb45bec52f1ba6d1 +tree 0b094cbff17168f24c302e297f55bfac65eb8bd3 +tree d667270a1f7b109f5eb3aaea21ede14b56bfdd6e +tree 56a30b966619b863674f5978696f4a3594f2fca9 +tree d667270a1f7b109f5eb3aaea21ede14b56bfdd6e +tree 8f51f74cf0163afc9ad68a4b1537288c4558b5a4 +EOF + +echo tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 >> expected + +test_expect_success "$name" "diff -u a expected" + +test_done diff --git a/t/t9101-git-svn-props.sh b/t/t9101-git-svn-props.sh new file mode 100755 index 0000000000..a2c4dc324a --- /dev/null +++ b/t/t9101-git-svn-props.sh @@ -0,0 +1,121 @@ +#!/bin/sh +# +# Copyright (c) 2006 Eric Wong +# + +test_description='git-svn property tests' +. ./lib-git-svn.sh + +mkdir import + +a_crlf= +a_lf= +a_cr= +a_ne_crlf= +a_ne_lf= +a_ne_cr= +a_empty= +a_empty_lf= +a_empty_cr= +a_empty_crlf= + +cd import + cat >> kw.c <<\EOF +/* Somebody prematurely put a keyword into this file */ +/* $Id$ */ +EOF + + printf "Hello\r\nWorld\r\n" > crlf + a_crlf=`git-hash-object -w crlf` + printf "Hello\rWorld\r" > cr + a_cr=`git-hash-object -w cr` + printf "Hello\nWorld\n" > lf + a_lf=`git-hash-object -w lf` + + printf "Hello\r\nWorld" > ne_crlf + a_ne_crlf=`git-hash-object -w ne_crlf` + printf "Hello\nWorld" > ne_lf + a_ne_lf=`git-hash-object -w ne_lf` + printf "Hello\rWorld" > ne_cr + a_ne_cr=`git-hash-object -w ne_cr` + + touch empty + a_empty=`git-hash-object -w empty` + printf "\n" > empty_lf + a_empty_lf=`git-hash-object -w empty_lf` + printf "\r" > empty_cr + a_empty_cr=`git-hash-object -w empty_cr` + printf "\r\n" > empty_crlf + a_empty_crlf=`git-hash-object -w empty_crlf` + + svn import -m 'import for git-svn' . "$svnrepo" >/dev/null +cd .. + +rm -rf import +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" && + echo Hello world >> kw.c && + svn commit -m "Modified file, but still not yet an Id" && + svn propset svn:keywords Id kw.c && + svn commit -m "Propset Id" + cd ..' + +test_expect_success 'initialize git-svn' "git-svn init $svnrepo" +test_expect_success 'fetch revisions from svn' 'git-svn fetch' + +name='test svn:keywords ignoring' +test_expect_success "$name" \ + 'git checkout -b mybranch remotes/git-svn && + echo Hi again >> kw.c && + git commit -a -m "test keywords ignoring" && + git-svn set-tree 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'" + +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" + cd ..' + +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" + +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" +done + + +cd test_wc + printf '$Id$\rHello\rWorld\r' > cr + 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` + 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"' +cd .. +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`" + +test_expect_success 'CRLF + $Id$' "test '$a_cr' = '$b_cr'" +test_expect_success 'CRLF + $Id$ (no newline)' "test '$a_ne_cr' = '$b_ne_cr'" + +test_done diff --git a/t/t9102-git-svn-deep-rmdir.sh b/t/t9102-git-svn-deep-rmdir.sh new file mode 100755 index 0000000000..4e0808380f --- /dev/null +++ b/t/t9102-git-svn-deep-rmdir.sh @@ -0,0 +1,30 @@ +#!/bin/sh +test_description='git-svn rmdir' +. ./lib-git-svn.sh + +test_expect_success 'initialize repo' " + mkdir import && + cd import && + mkdir -p deeply/nested/directory/number/1 && + mkdir -p deeply/nested/directory/number/2 && + echo foo > deeply/nested/directory/number/1/file && + echo foo > deeply/nested/directory/number/2/another && + svn import -m 'import for git-svn' . $svnrepo && + cd .. + " + +test_expect_success 'mirror via git-svn' " + git-svn init $svnrepo && + git-svn fetch && + git checkout -f -b test-rmdir remotes/git-svn + " + +test_expect_success 'Try a commit on rmdir' " + git rm -f deeply/nested/directory/number/2/another && + git commit -a -m 'remove another' && + git-svn set-tree --rmdir HEAD && + svn ls -R $svnrepo | grep ^deeply/nested/directory/number/1 + " + + +test_done diff --git a/t/t9103-git-svn-graft-branches.sh b/t/t9103-git-svn-graft-branches.sh new file mode 100755 index 0000000000..4e55778a47 --- /dev/null +++ b/t/t9103-git-svn-graft-branches.sh @@ -0,0 +1,60 @@ +#!/bin/sh +test_description='git-svn graft-branches' +. ./lib-git-svn.sh + +svnrepo="$svnrepo/test-git-svn" + +test_expect_success 'initialize repo' " + mkdir import && + cd import && + mkdir -p trunk branches tags && + echo hello > trunk/readme && + svn import -m 'import for git-svn' . $svnrepo && + cd .. && + svn cp -m 'tag a' $svnrepo/trunk $svnrepo/tags/a && + svn cp -m 'branch a' $svnrepo/trunk $svnrepo/branches/a && + svn co $svnrepo wc && + cd wc && + echo feedme >> branches/a/readme && + svn commit -m hungry && + cd trunk && + svn merge -r3:4 $svnrepo/branches/a && + svn commit -m 'merge with a' && + cd ../.. && + git-svn multi-init $svnrepo -T trunk -b branches -t tags && + git-svn multi-fetch + " + +r1=`git-rev-list remotes/trunk | tail -n1` +r2=`git-rev-list remotes/tags/a | tail -n1` +r3=`git-rev-list remotes/a | tail -n1` +r4=`git-rev-parse remotes/a` +r5=`git-rev-parse remotes/trunk` + +test_expect_success 'test graft-branches regexes and copies' " + test -n "$r1" && + test -n "$r2" && + test -n "$r3" && + test -n "$r4" && + test -n "$r5" && + git-svn graft-branches && + grep '^$r2 $r1' $GIT_DIR/info/grafts && + grep '^$r3 $r1' $GIT_DIR/info/grafts && + grep '^$r5 ' $GIT_DIR/info/grafts | grep '$r4' | grep '$r1' + " + +test_debug 'gitk --all & sleep 1' + +test_expect_success 'test graft-branches with tree-joins' " + rm $GIT_DIR/info/grafts && + git-svn graft-branches --no-default-regex --no-graft-copy -B && + grep '^$r3 ' $GIT_DIR/info/grafts | grep '$r1' | grep '$r2' && + grep '^$r2 $r1' $GIT_DIR/info/grafts && + grep '^$r5 ' $GIT_DIR/info/grafts | grep '$r1' | grep '$r4' + " + +# the result of this is kinda funky, we have a strange history and +# this is just a test :) +test_debug 'gitk --all &' + +test_done diff --git a/t/t9104-git-svn-follow-parent.sh b/t/t9104-git-svn-follow-parent.sh new file mode 100755 index 0000000000..8d2e2fec39 --- /dev/null +++ b/t/t9104-git-svn-follow-parent.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# +# Copyright (c) 2006 Eric Wong +# + +test_description='git-svn --follow-parent fetching' +. ./lib-git-svn.sh + +test_expect_success 'initialize repo' " + mkdir import && + cd import && + mkdir -p trunk && + echo hello > trunk/readme && + svn import -m 'initial' . $svnrepo && + cd .. && + svn co $svnrepo wc && + cd wc && + echo world >> trunk/readme && + svn commit -m 'another commit' && + svn up && + svn mv -m 'rename to thunk' trunk thunk && + svn up && + echo goodbye >> thunk/readme && + svn commit -m 'bye now' && + cd .. + " + +test_expect_success 'init and fetch --follow-parent a moved directory' " + git-svn init -i thunk $svnrepo/thunk && + git-svn fetch --follow-parent -i thunk && + git-rev-parse --verify refs/remotes/trunk && + test '$?' -eq '0' + " + +test_debug 'gitk --all &' + +test_done diff --git a/t/t9105-git-svn-commit-diff.sh b/t/t9105-git-svn-commit-diff.sh new file mode 100755 index 0000000000..6323c7e3ac --- /dev/null +++ b/t/t9105-git-svn-commit-diff.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# +# Copyright (c) 2006 Eric Wong +test_description='git-svn commit-diff' +. ./lib-git-svn.sh + +test_expect_success 'initialize repo' " + mkdir import && + cd import && + echo hello > readme && + svn import -m 'initial' . $svnrepo && + cd .. && + echo hello > readme && + git update-index --add readme && + git commit -a -m 'initial' && + echo world >> readme && + git commit -a -m 'another' + " + +head=`git rev-parse --verify HEAD^0` +prev=`git rev-parse --verify HEAD^1` + +# the internals of the commit-diff command are the same as the regular +# commit, so only a basic test of functionality is needed since we've +# already tested commit extensively elsewhere + +test_expect_success 'test the commit-diff command' " + test -n '$prev' && test -n '$head' && + git-svn commit-diff -r1 '$prev' '$head' '$svnrepo' && + svn co $svnrepo wc && + cmp readme wc/readme + " + +test_done diff --git a/t/t9106-git-svn-commit-diff-clobber.sh b/t/t9106-git-svn-commit-diff-clobber.sh new file mode 100755 index 0000000000..59b6425ce4 --- /dev/null +++ b/t/t9106-git-svn-commit-diff-clobber.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# +# Copyright (c) 2006 Eric Wong +test_description='git-svn commit-diff clobber' +. ./lib-git-svn.sh + +test_expect_success 'initialize repo' " + mkdir import && + cd import && + echo initial > file && + svn import -m 'initial' . $svnrepo && + cd .. && + echo initial > file && + git update-index --add file && + git commit -a -m 'initial' + " +test_expect_success 'commit change from svn side' " + svn co $svnrepo t.svn && + cd t.svn && + echo second line from svn >> file && + svn commit -m 'second line from svn' && + cd .. && + rm -rf t.svn + " + +test_expect_failure 'commit conflicting change from git' " + echo second line from git >> file && + git commit -a -m 'second line from git' && + git-svn commit-diff -r1 HEAD~1 HEAD $svnrepo + " || true + +test_expect_success 'commit complementing change from git' " + git reset --hard HEAD~1 && + echo second line from svn >> file && + git commit -a -m 'second line from svn' && + echo third line from git >> file && + git commit -a -m 'third line from git' && + git-svn commit-diff -r2 HEAD~1 HEAD $svnrepo + " + +test_expect_failure 'dcommit fails to commit because of conflict' " + git-svn init $svnrepo && + git-svn fetch && + git reset --hard refs/remotes/git-svn && + svn co $svnrepo t.svn && + cd t.svn && + echo fourth line from svn >> file && + svn commit -m 'fourth line from svn' && + cd .. && + rm -rf t.svn && + echo 'fourth line from git' >> file && + git commit -a -m 'fourth line from git' && + git-svn dcommit + " || true + +test_expect_success 'dcommit does the svn equivalent of an index merge' " + git reset --hard refs/remotes/git-svn && + echo 'index merge' > file2 && + git update-index --add file2 && + git commit -a -m 'index merge' && + echo 'more changes' >> file2 && + git update-index file2 && + git commit -a -m 'more changes' && + git-svn dcommit + " + +test_done diff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh new file mode 100755 index 0000000000..4efa0c926c --- /dev/null +++ b/t/t9200-git-cvsexportcommit.sh @@ -0,0 +1,235 @@ +#!/bin/sh +# +# Copyright (c) Robin Rosenberg +# +test_description='CVS export comit. ' + +. ./test-lib.sh + +cvs >/dev/null 2>&1 +if test $? -ne 1 +then + test_expect_success 'skipping git-cvsexportcommit tests, cvs not found' : + test_done + exit +fi + +CVSROOT=$(pwd)/cvsroot +CVSWORK=$(pwd)/cvswork +GIT_DIR=$(pwd)/.git +export CVSROOT CVSWORK GIT_DIR + +rm -rf "$CVSROOT" "$CVSWORK" +mkdir "$CVSROOT" && +cvs init && +cvs -Q co -d "$CVSWORK" . && +echo >empty && +git add empty && +git commit -q -a -m "Initial" 2>/dev/null || +exit 1 + +test_expect_success \ + 'New file' \ + 'mkdir A B C D E F && + echo hello1 >A/newfile1.txt && + echo hello2 >B/newfile2.txt && + cp ../test9200a.png C/newfile3.png && + cp ../test9200a.png D/newfile4.png && + git add A/newfile1.txt && + git add B/newfile2.txt && + git add C/newfile3.png && + git add D/newfile4.png && + git commit -a -m "Test: New file" && + id=$(git rev-list --max-count=1 HEAD) && + (cd "$CVSWORK" && + git cvsexportcommit -c $id && + test "$(echo $(sort A/CVS/Entries|cut -d/ -f2,3,5))" = "newfile1.txt/1.1/" && + test "$(echo $(sort B/CVS/Entries|cut -d/ -f2,3,5))" = "newfile2.txt/1.1/" && + test "$(echo $(sort C/CVS/Entries|cut -d/ -f2,3,5))" = "newfile3.png/1.1/-kb" && + test "$(echo $(sort D/CVS/Entries|cut -d/ -f2,3,5))" = "newfile4.png/1.1/-kb" && + diff A/newfile1.txt ../A/newfile1.txt && + diff B/newfile2.txt ../B/newfile2.txt && + diff C/newfile3.png ../C/newfile3.png && + diff D/newfile4.png ../D/newfile4.png + )' + +test_expect_success \ + 'Remove two files, add two and update two' \ + 'echo Hello1 >>A/newfile1.txt && + rm -f B/newfile2.txt && + rm -f C/newfile3.png && + echo Hello5 >E/newfile5.txt && + cp ../test9200b.png D/newfile4.png && + cp ../test9200a.png F/newfile6.png && + git add E/newfile5.txt && + git add F/newfile6.png && + git commit -a -m "Test: Remove, add and update" && + id=$(git rev-list --max-count=1 HEAD) && + (cd "$CVSWORK" && + git cvsexportcommit -c $id && + test "$(echo $(sort A/CVS/Entries|cut -d/ -f2,3,5))" = "newfile1.txt/1.2/" && + test "$(echo $(sort B/CVS/Entries|cut -d/ -f2,3,5))" = "" && + test "$(echo $(sort C/CVS/Entries|cut -d/ -f2,3,5))" = "" && + test "$(echo $(sort D/CVS/Entries|cut -d/ -f2,3,5))" = "newfile4.png/1.2/-kb" && + test "$(echo $(sort E/CVS/Entries|cut -d/ -f2,3,5))" = "newfile5.txt/1.1/" && + test "$(echo $(sort F/CVS/Entries|cut -d/ -f2,3,5))" = "newfile6.png/1.1/-kb" && + diff A/newfile1.txt ../A/newfile1.txt && + diff D/newfile4.png ../D/newfile4.png && + diff E/newfile5.txt ../E/newfile5.txt && + diff F/newfile6.png ../F/newfile6.png + )' + +# Should fail (but only on the git-cvsexportcommit stage) +test_expect_success \ + 'Fail to change binary more than one generation old' \ + 'cat F/newfile6.png >>D/newfile4.png && + git commit -a -m "generatiion 1" && + cat F/newfile6.png >>D/newfile4.png && + git commit -a -m "generation 2" && + id=$(git rev-list --max-count=1 HEAD) && + (cd "$CVSWORK" && + ! git cvsexportcommit -c $id + )' + +#test_expect_success \ +# 'Fail to remove binary file more than one generation old' \ +# 'git reset --hard HEAD^ && +# cat F/newfile6.png >>D/newfile4.png && +# git commit -a -m "generation 2 (again)" && +# rm -f D/newfile4.png && +# git commit -a -m "generation 3" && +# id=$(git rev-list --max-count=1 HEAD) && +# (cd "$CVSWORK" && +# ! git cvsexportcommit -c $id +# )' + +# We reuse the state from two tests back here + +# This test is here because a patch for only binary files will +# fail with gnu patch, so cvsexportcommit must handle that. +test_expect_success \ + 'Remove only binary files' \ + 'git reset --hard HEAD^^ && + rm -f D/newfile4.png && + git commit -a -m "test: remove only a binary file" && + id=$(git rev-list --max-count=1 HEAD) && + (cd "$CVSWORK" && + git cvsexportcommit -c $id && + test "$(echo $(sort A/CVS/Entries|cut -d/ -f2,3,5))" = "newfile1.txt/1.2/" && + test "$(echo $(sort B/CVS/Entries|cut -d/ -f2,3,5))" = "" && + test "$(echo $(sort C/CVS/Entries|cut -d/ -f2,3,5))" = "" && + test "$(echo $(sort D/CVS/Entries|cut -d/ -f2,3,5))" = "" && + test "$(echo $(sort E/CVS/Entries|cut -d/ -f2,3,5))" = "newfile5.txt/1.1/" && + test "$(echo $(sort F/CVS/Entries|cut -d/ -f2,3,5))" = "newfile6.png/1.1/-kb" && + diff A/newfile1.txt ../A/newfile1.txt && + diff E/newfile5.txt ../E/newfile5.txt && + diff F/newfile6.png ../F/newfile6.png + )' + +test_expect_success \ + 'Remove only a text file' \ + 'rm -f A/newfile1.txt && + git commit -a -m "test: remove only a binary file" && + id=$(git rev-list --max-count=1 HEAD) && + (cd "$CVSWORK" && + git cvsexportcommit -c $id && + test "$(echo $(sort A/CVS/Entries|cut -d/ -f2,3,5))" = "" && + test "$(echo $(sort B/CVS/Entries|cut -d/ -f2,3,5))" = "" && + test "$(echo $(sort C/CVS/Entries|cut -d/ -f2,3,5))" = "" && + test "$(echo $(sort D/CVS/Entries|cut -d/ -f2,3,5))" = "" && + test "$(echo $(sort E/CVS/Entries|cut -d/ -f2,3,5))" = "newfile5.txt/1.1/" && + test "$(echo $(sort F/CVS/Entries|cut -d/ -f2,3,5))" = "newfile6.png/1.1/-kb" && + diff E/newfile5.txt ../E/newfile5.txt && + diff F/newfile6.png ../F/newfile6.png + )' + +test_expect_success \ + 'New file with spaces in file name' \ + 'mkdir "G g" && + echo ok then >"G g/with spaces.txt" && + git add "G g/with spaces.txt" && \ + cp ../test9200a.png "G g/with spaces.png" && \ + git add "G g/with spaces.png" && + git commit -a -m "With spaces" && + id=$(git rev-list --max-count=1 HEAD) && + (cd "$CVSWORK" && + git-cvsexportcommit -c $id && + test "$(echo $(sort "G g/CVS/Entries"|cut -d/ -f2,3,5))" = "with spaces.png/1.1/-kb with spaces.txt/1.1/" + )' + +test_expect_success \ + 'Update file with spaces in file name' \ + 'echo Ok then >>"G g/with spaces.txt" && + cat ../test9200a.png >>"G g/with spaces.png" && \ + git add "G g/with spaces.png" && + git commit -a -m "Update with spaces" && + id=$(git rev-list --max-count=1 HEAD) && + (cd "$CVSWORK" && + git-cvsexportcommit -c $id + test "$(echo $(sort "G g/CVS/Entries"|cut -d/ -f2,3,5))" = "with spaces.png/1.2/-kb with spaces.txt/1.2/" + )' + +# Some filesystems mangle pathnames with UTF-8 characters -- +# check and skip +if p="Ã…/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/Ã¥/ä/ö" && + mkdir -p "tst/$p" && + date >"tst/$p/day" && + found=$(find tst -type f -print) && + test "z$found" = "ztst/$p/day" && + rm -fr tst +then + +# This test contains UTF-8 characters +test_expect_success \ + 'File with non-ascii file name' \ + 'mkdir -p Ã…/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/Ã¥/ä/ö && + echo Foo >Ã…/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/Ã¥/ä/ö/gÃ¥rdetsÃ¥gÃ¥rdet.txt && + git add Ã…/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/Ã¥/ä/ö/gÃ¥rdetsÃ¥gÃ¥rdet.txt && + cp ../test9200a.png Ã…/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/Ã¥/ä/ö/gÃ¥rdetsÃ¥gÃ¥rdet.png && + git add Ã…/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/Ã¥/ä/ö/gÃ¥rdetsÃ¥gÃ¥rdet.png && + git commit -a -m "GÃ¥r det sÃ¥ gÃ¥r det" && \ + id=$(git rev-list --max-count=1 HEAD) && + (cd "$CVSWORK" && + git-cvsexportcommit -v -c $id && + test "$(echo $(sort Ã…/goo/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/Ã¥/ä/ö/CVS/Entries|cut -d/ -f2,3,5))" = "gÃ¥rdetsÃ¥gÃ¥rdet.png/1.1/-kb gÃ¥rdetsÃ¥gÃ¥rdet.txt/1.1/" + )' + +fi + +rm -fr tst + +test_expect_success \ + 'Mismatching patch should fail' \ + 'date >>"E/newfile5.txt" && + git add "E/newfile5.txt" && + git commit -a -m "Update one" && + date >>"E/newfile5.txt" && + git add "E/newfile5.txt" && + git commit -a -m "Update two" && + id=$(git rev-list --max-count=1 HEAD) && + (cd "$CVSWORK" && + ! git-cvsexportcommit -c $id + )' + +case "$(git repo-config --bool core.filemode)" in +false) + ;; +*) +test_expect_success \ + 'Retain execute bit' \ + 'mkdir G && + echo executeon >G/on && + chmod +x G/on && + echo executeoff >G/off && + git add G/on && + git add G/off && + git commit -a -m "Execute test" && + (cd "$CVSWORK" && + git-cvsexportcommit -c HEAD + test -x G/on && + ! test -x G/off + )' + ;; +esac + +test_done diff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh new file mode 100755 index 0000000000..8d28211294 --- /dev/null +++ b/t/t9300-fast-import.sh @@ -0,0 +1,436 @@ +#!/bin/sh +# +# Copyright (c) 2007 Shawn Pearce +# + +test_description='test git-fast-import utility' +. ./test-lib.sh +. ../diff-lib.sh ;# test-lib chdir's into trash + +file2_data='file2 +second line of EOF' + +file3_data='EOF +in 3rd file + END' + +file4_data=abcd +file4_len=4 + +file5_data='an inline file. + we should see it later.' + +file6_data='#!/bin/sh +echo "$@"' + +### +### series A +### + +test_tick +cat >input <<INPUT_END +blob +mark :2 +data <<EOF +$file2_data +EOF + +blob +mark :3 +data <<END +$file3_data +END + +blob +mark :4 +data $file4_len +$file4_data +commit refs/heads/master +mark :5 +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +initial +COMMIT + +M 644 :2 file2 +M 644 :3 file3 +M 755 :4 file4 + +INPUT_END +test_expect_success \ + 'A: create pack from stdin' \ + 'git-fast-import --export-marks=marks.out <input && + git-whatchanged master' +test_expect_success \ + 'A: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' + +cat >expect <<EOF +author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + +initial +EOF +test_expect_success \ + 'A: verify commit' \ + 'git-cat-file commit master | sed 1d >actual && + diff -u expect actual' + +cat >expect <<EOF +100644 blob file2 +100644 blob file3 +100755 blob file4 +EOF +test_expect_success \ + 'A: verify tree' \ + 'git-cat-file -p master^{tree} | sed "s/ [0-9a-f]* / /" >actual && + diff -u expect actual' + +echo "$file2_data" >expect +test_expect_success \ + 'A: verify file2' \ + 'git-cat-file blob master:file2 >actual && diff -u expect actual' + +echo "$file3_data" >expect +test_expect_success \ + 'A: verify file3' \ + 'git-cat-file blob master:file3 >actual && diff -u expect actual' + +printf "$file4_data" >expect +test_expect_success \ + 'A: verify file4' \ + 'git-cat-file blob master:file4 >actual && diff -u expect actual' + +cat >expect <<EOF +:2 `git-rev-parse --verify master:file2` +:3 `git-rev-parse --verify master:file3` +:4 `git-rev-parse --verify master:file4` +:5 `git-rev-parse --verify master^0` +EOF +test_expect_success \ + 'A: verify marks output' \ + 'diff -u expect marks.out' + +### +### series B +### + +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +mark :1 +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +corrupt +COMMIT + +from refs/heads/master +M 755 0000000000000000000000000000000000000001 zero1 + +INPUT_END +test_expect_failure \ + 'B: fail on invalid blob sha1' \ + 'git-fast-import <input' +rm -f .git/objects/pack_* .git/objects/index_* + +### +### series C +### + +newf=`echo hi newf | git-hash-object -w --stdin` +oldf=`git-rev-parse --verify master:file2` +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +second +COMMIT + +from refs/heads/master +M 644 $oldf file2/oldf +M 755 $newf file2/newf +D file3 + +INPUT_END +test_expect_success \ + 'C: incremental import create pack from stdin' \ + 'git-fast-import <input && + git-whatchanged branch' +test_expect_success \ + 'C: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' +test_expect_success \ + 'C: validate reuse existing blob' \ + 'test $newf = `git-rev-parse --verify branch:file2/newf` + test $oldf = `git-rev-parse --verify branch:file2/oldf`' + +cat >expect <<EOF +parent `git-rev-parse --verify master^0` +author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + +second +EOF +test_expect_success \ + 'C: verify commit' \ + 'git-cat-file commit branch | sed 1d >actual && + diff -u expect actual' + +cat >expect <<EOF +:000000 100755 0000000000000000000000000000000000000000 f1fb5da718392694d0076d677d6d0e364c79b0bc A file2/newf +:100644 100644 7123f7f44e39be127c5eb701e5968176ee9d78b1 7123f7f44e39be127c5eb701e5968176ee9d78b1 R100 file2 file2/oldf +:100644 000000 0d92e9f3374ae2947c23aa477cbc68ce598135f1 0000000000000000000000000000000000000000 D file3 +EOF +git-diff-tree -M -r master branch >actual +test_expect_success \ + 'C: validate rename result' \ + 'compare_diff_raw expect actual' + +### +### series D +### + +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +third +COMMIT + +from refs/heads/branch^0 +M 644 inline newdir/interesting +data <<EOF +$file5_data +EOF + +M 755 inline newdir/exec.sh +data <<EOF +$file6_data +EOF + +INPUT_END +test_expect_success \ + 'D: inline data in commit' \ + 'git-fast-import <input && + git-whatchanged branch' +test_expect_success \ + 'D: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' + +cat >expect <<EOF +:000000 100755 0000000000000000000000000000000000000000 35a59026a33beac1569b1c7f66f3090ce9c09afc A newdir/exec.sh +:000000 100644 0000000000000000000000000000000000000000 046d0371e9220107917db0d0e030628de8a1de9b A newdir/interesting +EOF +git-diff-tree -M -r branch^ branch >actual +test_expect_success \ + 'D: validate new files added' \ + 'compare_diff_raw expect actual' + +echo "$file5_data" >expect +test_expect_success \ + 'D: verify file5' \ + 'git-cat-file blob branch:newdir/interesting >actual && + diff -u expect actual' + +echo "$file6_data" >expect +test_expect_success \ + 'D: verify file6' \ + 'git-cat-file blob branch:newdir/exec.sh >actual && + diff -u expect actual' + +### +### series E +### + +cat >input <<INPUT_END +commit refs/heads/branch +author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> Tue Feb 6 11:22:18 2007 -0500 +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> Tue Feb 6 12:35:02 2007 -0500 +data <<COMMIT +RFC 2822 type date +COMMIT + +from refs/heads/branch^0 + +INPUT_END +test_expect_failure \ + 'E: rfc2822 date, --date-format=raw' \ + 'git-fast-import --date-format=raw <input' +test_expect_success \ + 'E: rfc2822 date, --date-format=rfc2822' \ + 'git-fast-import --date-format=rfc2822 <input' +test_expect_success \ + 'E: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' + +cat >expect <<EOF +author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> 1170778938 -0500 +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1170783302 -0500 + +RFC 2822 type date +EOF +test_expect_success \ + 'E: verify commit' \ + 'git-cat-file commit branch | sed 1,2d >actual && + diff -u expect actual' + +### +### series F +### + +old_branch=`git-rev-parse --verify branch^0` +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +losing things already? +COMMIT + +from refs/heads/branch~1 + +reset refs/heads/other +from refs/heads/branch + +INPUT_END +test_expect_success \ + 'F: non-fast-forward update skips' \ + 'if git-fast-import <input + then + echo BAD gfi did not fail + return 1 + else + if test $old_branch = `git-rev-parse --verify branch^0` + then + : branch unaffected and failure returned + return 0 + else + echo BAD gfi changed branch $old_branch + return 1 + fi + fi + ' +test_expect_success \ + 'F: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' + +cat >expect <<EOF +tree `git-rev-parse branch~1^{tree}` +parent `git-rev-parse branch~1` +author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + +losing things already? +EOF +test_expect_success \ + 'F: verify other commit' \ + 'git-cat-file commit other >actual && + diff -u expect actual' + +### +### series G +### + +old_branch=`git-rev-parse --verify branch^0` +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +losing things already? +COMMIT + +from refs/heads/branch~1 + +INPUT_END +test_expect_success \ + 'G: non-fast-forward update forced' \ + 'git-fast-import --force <input' +test_expect_success \ + 'G: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' +test_expect_success \ + 'G: branch changed, but logged' \ + 'test $old_branch != `git-rev-parse --verify branch^0` && + test $old_branch = `git-rev-parse --verify branch@{1}`' + +### +### series H +### + +test_tick +cat >input <<INPUT_END +commit refs/heads/H +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +third +COMMIT + +from refs/heads/branch^0 +M 644 inline i-will-die +data <<EOF +this file will never exist. +EOF + +deleteall +M 644 inline h/e/l/lo +data <<EOF +$file5_data +EOF + +INPUT_END +test_expect_success \ + 'H: deletall, add 1' \ + 'git-fast-import <input && + git-whatchanged H' +test_expect_success \ + 'H: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' + +cat >expect <<EOF +:100755 000000 f1fb5da718392694d0076d677d6d0e364c79b0bc 0000000000000000000000000000000000000000 D file2/newf +:100644 000000 7123f7f44e39be127c5eb701e5968176ee9d78b1 0000000000000000000000000000000000000000 D file2/oldf +:100755 000000 85df50785d62d3b05ab03d9cbf7e4a0b49449730 0000000000000000000000000000000000000000 D file4 +:100644 100644 fcf778cda181eaa1cbc9e9ce3a2e15ee9f9fe791 fcf778cda181eaa1cbc9e9ce3a2e15ee9f9fe791 R100 newdir/interesting h/e/l/lo +:100755 000000 e74b7d465e52746be2b4bae983670711e6e66657 0000000000000000000000000000000000000000 D newdir/exec.sh +EOF +git-diff-tree -M -r H^ H >actual +test_expect_success \ + 'H: validate old files removed, new files added' \ + 'compare_diff_raw expect actual' + +echo "$file5_data" >expect +test_expect_success \ + 'H: verify file' \ + 'git-cat-file blob H:h/e/l/lo >actual && + diff -u expect actual' + +### +### series I +### + +cat >input <<INPUT_END +commit refs/heads/export-boundary +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +we have a border. its only 40 characters wide. +COMMIT + +from refs/heads/branch + +INPUT_END +test_expect_success \ + 'I: export-pack-edges' \ + 'git-fast-import --export-pack-edges=edges.list <input' + +cat >expect <<EOF +.git/objects/pack/pack-.pack: `git-rev-parse --verify export-boundary` +EOF +test_expect_success \ + 'I: verify edge list' \ + 'sed -e s/pack-.*pack/pack-.pack/ edges.list >actual && + diff -u expect actual' + +test_done diff --git a/t/test-lib.sh b/t/test-lib.sh new file mode 100755 index 0000000000..37822fc13d --- /dev/null +++ b/t/test-lib.sh @@ -0,0 +1,290 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +# For repeatability, reset the environment to known value. +LANG=C +LC_ALL=C +PAGER=cat +TZ=UTC +export LANG LC_ALL PAGER TZ +EDITOR=: +VISUAL=: +unset AUTHOR_DATE +unset AUTHOR_EMAIL +unset AUTHOR_NAME +unset COMMIT_AUTHOR_EMAIL +unset COMMIT_AUTHOR_NAME +unset GIT_ALTERNATE_OBJECT_DIRECTORIES +unset GIT_AUTHOR_DATE +GIT_AUTHOR_EMAIL=author@example.com +GIT_AUTHOR_NAME='A U Thor' +unset GIT_COMMITTER_DATE +GIT_COMMITTER_EMAIL=committer@example.com +GIT_COMMITTER_NAME='C O Mitter' +unset GIT_DIFF_OPTS +unset GIT_DIR +unset GIT_EXTERNAL_DIFF +unset GIT_INDEX_FILE +unset GIT_OBJECT_DIRECTORY +unset SHA1_FILE_DIRECTORIES +unset SHA1_FILE_DIRECTORY +GIT_MERGE_VERBOSITY=5 +export GIT_MERGE_VERBOSITY +export GIT_AUTHOR_EMAIL GIT_AUTHOR_NAME +export GIT_COMMITTER_EMAIL GIT_COMMITTER_NAME +export EDITOR VISUAL + +case $(echo $GIT_TRACE |tr "[A-Z]" "[a-z]") in + 1|2|true) + echo "* warning: Some tests will not work if GIT_TRACE" \ + "is set as to trace on STDERR ! *" + echo "* warning: Please set GIT_TRACE to something" \ + "other than 1, 2 or true ! *" + ;; +esac + +# Each test should start with something like this, after copyright notices: +# +# test_description='Description of this test... +# This test checks if command xyzzy does the right thing... +# ' +# . ./test-lib.sh + +error () { + echo "* error: $*" + trap - exit + exit 1 +} + +say () { + echo "* $*" +} + +test "${test_description}" != "" || +error "Test script did not set test_description." + +while test "$#" -ne 0 +do + case "$1" in + -d|--d|--de|--deb|--debu|--debug) + debug=t; shift ;; + -i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate) + immediate=t; shift ;; + -h|--h|--he|--hel|--help) + echo "$test_description" + exit 0 ;; + -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) + verbose=t; shift ;; + --no-python) + # noop now... + shift ;; + *) + break ;; + esac +done + +exec 5>&1 +if test "$verbose" = "t" +then + exec 4>&2 3>&1 +else + exec 4>/dev/null 3>/dev/null +fi + +test_failure=0 +test_count=0 + +trap 'echo >&5 "FATAL: Unexpected exit with code $?"; exit 1' exit + +test_tick () { + if test -z "${test_tick+set}" + then + test_tick=1112911993 + else + test_tick=$(($test_tick + 60)) + fi + GIT_COMMITTER_DATE="$test_tick -0700" + GIT_AUTHOR_DATE="$test_tick -0700" + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE +} + +# You are not expected to call test_ok_ and test_failure_ directly, use +# the text_expect_* functions instead. + +test_ok_ () { + test_count=$(expr "$test_count" + 1) + say " ok $test_count: $@" +} + +test_failure_ () { + test_count=$(expr "$test_count" + 1) + test_failure=$(expr "$test_failure" + 1); + say "FAIL $test_count: $1" + shift + echo "$@" | sed -e 's/^/ /' + test "$immediate" = "" || { trap - exit; exit 1; } +} + + +test_debug () { + test "$debug" = "" || eval "$1" +} + +test_run_ () { + eval >&3 2>&4 "$1" + eval_ret="$?" + return 0 +} + +test_skip () { + this_test=$(expr "./$0" : '.*/\(t[0-9]*\)-[^/]*$') + this_test="$this_test.$(expr "$test_count" + 1)" + to_skip= + for skp in $GIT_SKIP_TESTS + do + case "$this_test" in + $skp) + to_skip=t + esac + done + case "$to_skip" in + t) + say >&3 "skipping test: $@" + test_count=$(expr "$test_count" + 1) + say "skip $test_count: $1" + : true + ;; + *) + false + ;; + esac +} + +test_expect_failure () { + test "$#" = 2 || + error "bug in the test script: not 2 parameters to test-expect-failure" + if ! test_skip "$@" + then + say >&3 "expecting failure: $2" + test_run_ "$2" + if [ "$?" = 0 -a "$eval_ret" != 0 -a "$eval_ret" -lt 129 ] + then + test_ok_ "$1" + else + test_failure_ "$@" + fi + fi + echo >&3 "" +} + +test_expect_success () { + test "$#" = 2 || + error "bug in the test script: not 2 parameters to test-expect-success" + if ! test_skip "$@" + then + say >&3 "expecting success: $2" + test_run_ "$2" + if [ "$?" = 0 -a "$eval_ret" = 0 ] + then + test_ok_ "$1" + else + test_failure_ "$@" + fi + fi + echo >&3 "" +} + +test_expect_code () { + test "$#" = 3 || + error "bug in the test script: not 3 parameters to test-expect-code" + if ! test_skip "$@" + then + say >&3 "expecting exit code $1: $3" + test_run_ "$3" + if [ "$?" = 0 -a "$eval_ret" = "$1" ] + then + test_ok_ "$2" + else + test_failure_ "$@" + fi + fi + echo >&3 "" +} + +# Most tests can use the created repository, but some amy need to create more. +# Usage: test_create_repo <directory> +test_create_repo () { + test "$#" = 1 || + error "bug in the test script: not 1 parameter to test-create-repo" + owd=`pwd` + repo="$1" + mkdir "$repo" + cd "$repo" || error "Cannot setup test environment" + "$GIT_EXEC_PATH/git" init --template=$GIT_EXEC_PATH/templates/blt/ >/dev/null 2>&1 || + error "cannot run git init -- have you built things yet?" + mv .git/hooks .git/hooks-disabled + cd "$owd" +} + +test_done () { + trap - exit + case "$test_failure" in + 0) + # We could: + # cd .. && rm -fr trash + # but that means we forbid any tests that use their own + # subdirectory from calling test_done without coming back + # to where they started from. + # The Makefile provided will clean this test area so + # we will leave things as they are. + + say "passed all $test_count test(s)" + exit 0 ;; + + *) + say "failed $test_failure among $test_count test(s)" + exit 1 ;; + + esac +} + +# Test the binaries we have just built. The tests are kept in +# t/ subdirectory and are run in trash subdirectory. +PATH=$(pwd)/..:$PATH +GIT_EXEC_PATH=$(pwd)/.. +GIT_TEMPLATE_DIR=$(pwd)/../templates/blt +HOME=$(pwd)/trash +export PATH GIT_EXEC_PATH GIT_TEMPLATE_DIR HOME + +GITPERLLIB=$(pwd)/../perl/blib/lib:$(pwd)/../perl/blib/arch/auto/Git +export GITPERLLIB +test -d ../templates/blt || { + error "You haven't built things yet, have you?" +} + +# Test repository +test=trash +rm -fr "$test" +test_create_repo $test +cd "$test" + +this_test=$(expr "./$0" : '.*/\(t[0-9]*\)-[^/]*$') +for skp in $GIT_SKIP_TESTS +do + to_skip= + for skp in $GIT_SKIP_TESTS + do + case "$this_test" in + $skp) + to_skip=t + esac + done + case "$to_skip" in + t) + say >&3 "skipping test $this_test altogether" + say "skip all tests in $this_test" + test_done + esac +done diff --git a/t/test4012.png b/t/test4012.png Binary files differnew file mode 100644 index 0000000000..7b181d15ce --- /dev/null +++ b/t/test4012.png diff --git a/t/test9200a.png b/t/test9200a.png Binary files differnew file mode 100644 index 0000000000..7b181d15ce --- /dev/null +++ b/t/test9200a.png diff --git a/t/test9200b.png b/t/test9200b.png Binary files differnew file mode 100644 index 0000000000..ac22ccbd3e --- /dev/null +++ b/t/test9200b.png @@ -0,0 +1,107 @@ +#include "cache.h" +#include "tag.h" + +const char *tag_type = "tag"; + +struct object *deref_tag(struct object *o, const char *warn, int warnlen) +{ + while (o && o->type == OBJ_TAG) + o = parse_object(((struct tag *)o)->tagged->sha1); + if (!o && warn) { + if (!warnlen) + warnlen = strlen(warn); + error("missing object referenced by '%.*s'", warnlen, warn); + } + return o; +} + +struct tag *lookup_tag(const unsigned char *sha1) +{ + struct object *obj = lookup_object(sha1); + if (!obj) { + struct tag *ret = alloc_tag_node(); + created_object(sha1, &ret->object); + ret->object.type = OBJ_TAG; + return ret; + } + if (!obj->type) + obj->type = OBJ_TAG; + if (obj->type != OBJ_TAG) { + error("Object %s is a %s, not a tree", + sha1_to_hex(sha1), typename(obj->type)); + return NULL; + } + return (struct tag *) obj; +} + +int parse_tag_buffer(struct tag *item, void *data, unsigned long size) +{ + int typelen, taglen; + unsigned char object[20]; + const char *type_line, *tag_line, *sig_line; + char type[20]; + + if (item->object.parsed) + return 0; + item->object.parsed = 1; + + if (size < 64) + return -1; + if (memcmp("object ", data, 7) || get_sha1_hex((char *) data + 7, object)) + return -1; + + type_line = (char *) data + 48; + if (memcmp("\ntype ", type_line-1, 6)) + return -1; + + tag_line = strchr(type_line, '\n'); + if (!tag_line || memcmp("tag ", ++tag_line, 4)) + return -1; + + sig_line = strchr(tag_line, '\n'); + if (!sig_line) + return -1; + sig_line++; + + typelen = tag_line - type_line - strlen("type \n"); + if (typelen >= 20) + return -1; + memcpy(type, type_line + 5, typelen); + type[typelen] = '\0'; + taglen = sig_line - tag_line - strlen("tag \n"); + item->tag = xmalloc(taglen + 1); + memcpy(item->tag, tag_line + 4, taglen); + item->tag[taglen] = '\0'; + + item->tagged = lookup_object_type(object, type); + if (item->tagged && track_object_refs) { + struct object_refs *refs = alloc_object_refs(1); + refs->ref[0] = item->tagged; + set_object_refs(&item->object, refs); + } + + return 0; +} + +int parse_tag(struct tag *item) +{ + char type[20]; + void *data; + unsigned long size; + int ret; + + if (item->object.parsed) + return 0; + data = read_sha1_file(item->object.sha1, type, &size); + if (!data) + return error("Could not read %s", + sha1_to_hex(item->object.sha1)); + if (strcmp(type, tag_type)) { + free(data); + return error("Object %s not a tag", + sha1_to_hex(item->object.sha1)); + } + ret = parse_tag_buffer(item, data, size); + free(data); + return ret; +} @@ -0,0 +1,20 @@ +#ifndef TAG_H +#define TAG_H + +#include "object.h" + +extern const char *tag_type; + +struct tag { + struct object object; + struct object *tagged; + char *tag; + char *signature; /* not actually implemented */ +}; + +extern struct tag *lookup_tag(const unsigned char *sha1); +extern int parse_tag_buffer(struct tag *item, void *data, unsigned long size); +extern int parse_tag(struct tag *item); +extern struct object *deref_tag(struct object *, const char *, int); + +#endif /* TAG_H */ @@ -0,0 +1,25 @@ +#define TYPEFLAG_AUTO '\0' +#define TYPEFLAG_REG '0' +#define TYPEFLAG_LNK '2' +#define TYPEFLAG_DIR '5' +#define TYPEFLAG_GLOBAL_HEADER 'g' +#define TYPEFLAG_EXT_HEADER 'x' + +struct ustar_header { + char name[100]; /* 0 */ + char mode[8]; /* 100 */ + char uid[8]; /* 108 */ + char gid[8]; /* 116 */ + char size[12]; /* 124 */ + char mtime[12]; /* 136 */ + char chksum[8]; /* 148 */ + char typeflag[1]; /* 156 */ + char linkname[100]; /* 157 */ + char magic[6]; /* 257 */ + char version[2]; /* 263 */ + char uname[32]; /* 265 */ + char gname[32]; /* 297 */ + char devmajor[8]; /* 329 */ + char devminor[8]; /* 337 */ + char prefix[155]; /* 345 */ +}; diff --git a/templates/.gitignore b/templates/.gitignore new file mode 100644 index 0000000000..6759ecbf98 --- /dev/null +++ b/templates/.gitignore @@ -0,0 +1,2 @@ +blt +boilerplates.made diff --git a/templates/Makefile b/templates/Makefile new file mode 100644 index 0000000000..0eeee43feb --- /dev/null +++ b/templates/Makefile @@ -0,0 +1,46 @@ +# make and install sample templates + +INSTALL ?= install +TAR ?= tar +prefix ?= $(HOME) +template_dir ?= $(prefix)/share/git-core/templates/ +# DESTDIR= + +# Shell quote (do not use $(call) to accommodate ancient setups); +DESTDIR_SQ = $(subst ','\'',$(DESTDIR)) +template_dir_SQ = $(subst ','\'',$(template_dir)) + +all: boilerplates.made custom + +# Put templates that can be copied straight from the source +# in a file direc--tory--file in the source. They will be +# just copied to the destination. + +bpsrc = $(filter-out %~,$(wildcard *--*)) +boilerplates.made : $(bpsrc) + ls *--* 2>/dev/null | \ + while read boilerplate; \ + do \ + case "$$boilerplate" in *~) continue ;; esac && \ + dst=`echo "$$boilerplate" | sed -e 's|^this|.|;s|--|/|g'` && \ + dir=`expr "$$dst" : '\(.*\)/'` && \ + mkdir -p blt/$$dir && \ + case "$$boilerplate" in \ + *--) ;; \ + *) cp $$boilerplate blt/$$dst ;; \ + esac || exit; \ + done || exit + date >$@ + +# If you need build-tailored templates, build them into blt/ +# directory yourself here. +custom: + : no custom templates yet + +clean: + rm -rf blt boilerplates.made + +install: all + $(INSTALL) -d -m755 '$(DESTDIR_SQ)$(template_dir_SQ)' + (cd blt && $(TAR) cf - .) | \ + (cd '$(DESTDIR_SQ)$(template_dir_SQ)' && $(TAR) xf -) diff --git a/templates/branches-- b/templates/branches-- new file mode 100644 index 0000000000..fae88709a6 --- /dev/null +++ b/templates/branches-- @@ -0,0 +1 @@ +: this is just to ensure the directory exists. diff --git a/templates/hooks--applypatch-msg b/templates/hooks--applypatch-msg new file mode 100644 index 0000000000..02de1ef84c --- /dev/null +++ b/templates/hooks--applypatch-msg @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, make this file executable. + +. git-sh-setup +test -x "$GIT_DIR/hooks/commit-msg" && + exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} +: diff --git a/templates/hooks--commit-msg b/templates/hooks--commit-msg new file mode 100644 index 0000000000..9b04f2d69c --- /dev/null +++ b/templates/hooks--commit-msg @@ -0,0 +1,22 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by git-commit with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, make this file executable. + +# Uncomment the below to add a Signed-off-by line to the message. +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} + diff --git a/templates/hooks--post-commit b/templates/hooks--post-commit new file mode 100644 index 0000000000..8be6f34ad9 --- /dev/null +++ b/templates/hooks--post-commit @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script that is called after a successful +# commit is made. +# +# To enable this hook, make this file executable. + +: Nothing diff --git a/templates/hooks--post-update b/templates/hooks--post-update new file mode 100644 index 0000000000..bcba8937bb --- /dev/null +++ b/templates/hooks--post-update @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, make this file executable by "chmod +x post-update". + +exec git-update-server-info diff --git a/templates/hooks--pre-applypatch b/templates/hooks--pre-applypatch new file mode 100644 index 0000000000..5f56ce8053 --- /dev/null +++ b/templates/hooks--pre-applypatch @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, make this file executable. + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} +: + diff --git a/templates/hooks--pre-commit b/templates/hooks--pre-commit new file mode 100644 index 0000000000..723a9ef210 --- /dev/null +++ b/templates/hooks--pre-commit @@ -0,0 +1,71 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by git-commit with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, make this file executable. + +# This is slightly modified from Andrew Morton's Perfect Patch. +# Lines you introduce should not have trailing whitespace. +# Also check for an indentation that has SP before a TAB. + +if git-rev-parse --verify HEAD 2>/dev/null +then + git-diff-index -p -M --cached HEAD +else + # NEEDSWORK: we should produce a diff with an empty tree here + # if we want to do the same verification for the initial import. + : +fi | +perl -e ' + my $found_bad = 0; + my $filename; + my $reported_filename = ""; + my $lineno; + sub bad_line { + my ($why, $line) = @_; + if (!$found_bad) { + print STDERR "*\n"; + print STDERR "* You have some suspicious patch lines:\n"; + print STDERR "*\n"; + $found_bad = 1; + } + if ($reported_filename ne $filename) { + print STDERR "* In $filename\n"; + $reported_filename = $filename; + } + print STDERR "* $why (line $lineno)\n"; + print STDERR "$filename:$lineno:$line\n"; + } + while (<>) { + if (m|^diff --git a/(.*) b/\1$|) { + $filename = $1; + next; + } + if (/^@@ -\S+ \+(\d+)/) { + $lineno = $1 - 1; + next; + } + if (/^ /) { + $lineno++; + next; + } + if (s/^\+//) { + $lineno++; + chomp; + if (/\s$/) { + bad_line("trailing whitespace", $_); + } + if (/^\s* /) { + bad_line("indent SP followed by a TAB", $_); + } + if (/^(?:[<>=]){7}/) { + bad_line("unresolved merge conflict", $_); + } + } + } + exit($found_bad); +' + diff --git a/templates/hooks--pre-rebase b/templates/hooks--pre-rebase new file mode 100644 index 0000000000..981c454cda --- /dev/null +++ b/templates/hooks--pre-rebase @@ -0,0 +1,150 @@ +#!/bin/sh +# +# Copyright (c) 2006 Junio C Hamano +# + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` +fi + +case "$basebranch,$topic" in +master,refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Is topic fully merged to master? +not_in_master=`git-rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git-rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git-rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git-rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up-to-date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git-rev-list --pretty=oneline ^${publish} "$topic"` + perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +exit 0 + +################################################################ + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git-rev-list ^master ^topic next + git-rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git-rev-list master..topic + + if this is empty, it is fully merged to "master". diff --git a/templates/hooks--update b/templates/hooks--update new file mode 100644 index 0000000000..d4253cbcfb --- /dev/null +++ b/templates/hooks--update @@ -0,0 +1,285 @@ +#!/bin/sh +# +# An example hook script to mail out commit update information. +# It can also blocks tags that aren't annotated. +# Called by git-receive-pack with arguments: refname sha1-old sha1-new +# +# To enable this hook, make this file executable by "chmod +x update". +# +# Config +# ------ +# hooks.mailinglist +# This is the list that all pushes will go to; leave it blank to not send +# emails frequently. The log email will list every log entry in full between +# the old ref value and the new ref value. +# hooks.announcelist +# This is the list that all pushes of annotated tags will go to. Leave it +# blank to just use the mailinglist field. The announce emails list the +# short log summary of the changes since the last annotated tag +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# +# Notes +# ----- +# All emails have their subjects prefixed with "[SCM]" to aid filtering. +# All emails include the headers "X-Git-Refname", "X-Git-Oldrev", +# "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and info. + +# --- Constants +EMAILPREFIX="[SCM] " +LOGBEGIN="- Log -----------------------------------------------------------------" +LOGEND="-----------------------------------------------------------------------" +DATEFORMAT="%F %R %z" + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 <ref> <oldrev> <newrev>)" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "Usage: $0 <ref> <oldrev> <newrev>" >&2 + exit 1 +fi + +# --- Config +projectdesc=$(cat $GIT_DIR/description) +recipients=$(git-repo-config hooks.mailinglist) +announcerecipients=$(git-repo-config hooks.announcelist) +allowunannotated=$(git-repo-config --bool hooks.allowunannotated) + +# --- Check types +newrev_type=$(git-cat-file -t "$newrev") + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + refname_type="tag" + short_refname=${refname##refs/tags/} + if [ $allowunannotated != "true" ]; then + echo "*** The un-annotated tag, $short_refname is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + refname_type="annotated tag" + short_refname=${refname##refs/tags/} + # change recipients + if [ -n "$announcerecipients" ]; then + recipients="$announcerecipients" + fi + ;; + refs/heads/*,commit) + # branch + refname_type="branch" + short_refname=${refname##refs/heads/} + ;; + refs/remotes/*,commit) + # tracking branch + refname_type="tracking branch" + short_refname=${refname##refs/remotes/} + # Should this even be allowed? + echo "*** Push-update of tracking branch, $refname. No email generated." >&2 + exit 0 + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update, \"$newrev_type\", to ref $refname" >&2 + exit 1 + ;; +esac + +# Check if we've got anyone to send to +if [ -z "$recipients" ]; then + # If the email isn't sent, then at least give the user some idea of what command + # would generate the email at a later date + echo "*** No recipients found - no email will be sent, but the push will continue" >&2 + echo "*** for $0 $1 $2 $3" >&2 + exit 0 +fi + +# --- Email parameters +committer=$(git show --pretty=full -s $newrev | grep "^Commit: " | sed -e "s/^Commit: //") +describe=$(git describe $newrev 2>/dev/null) +if [ -z "$describe" ]; then + describe=$newrev +fi + +# --- Email (all stdout will be the email) +( +# Generate header +cat <<-EOF +From: $committer +To: $recipients +Subject: ${EMAILPREFIX}$projectdesc $refname_type, $short_refname now at $describe +X-Git-Refname: $refname +X-Git-Reftype: $refname_type +X-Git-Oldrev: $oldrev +X-Git-Newrev: $newrev + +Hello, + +This is an automated email from the git hooks/update script, it was +generated because a ref change was pushed to the repository. + +Updating $refname_type, $short_refname, +EOF + +case "$refname_type" in + "tracking branch"|branch) + if expr "$oldrev" : '0*$' >/dev/null + then + # If the old reference is "0000..0000" then this is a new branch + # and so oldrev is not valid + echo " as a new $refname_type" + echo " to $newrev ($newrev_type)" + echo "" + echo $LOGBEGIN + # This shows all log entries that are not already covered by + # another ref - i.e. commits that are now accessible from this + # ref that were previously not accessible + git-rev-list --pretty $newref $(git-rev-parse --not --all) + echo $LOGEND + else + # oldrev is valid + oldrev_type=$(git-cat-file -t "$oldrev") + + # Now the problem is for cases like this: + # * --- * --- * --- * (oldrev) + # \ + # * --- * --- * (newrev) + # i.e. there is no guarantee that newrev is a strict subset + # of oldrev - (would have required a force, but that's allowed). + # So, we can't simply say rev-list $oldrev..$newrev. Instead + # we find the common base of the two revs and list from there + baserev=$(git-merge-base $oldrev $newrev) + + # Commit with a parent + for rev in $(git-rev-list $newrev ^$baserev) + do + revtype=$(git-cat-file -t "$rev") + echo " via $rev ($revtype)" + done + if [ "$baserev" = "$oldrev" ]; then + echo " from $oldrev ($oldrev_type)" + else + echo " based on $baserev" + echo " from $oldrev ($oldrev_type)" + echo "" + echo "This ref update crossed a branch point; i.e. the old rev is not a strict subset" + echo "of the new rev. This occurs, when you --force push a change in a situation" + echo "like this:" + echo "" + echo " * -- * -- B -- O -- O -- O ($oldrev)" + echo " \\" + echo " N -- N -- N ($newrev)" + echo "" + echo "Therefore, we assume that you've already had alert emails for all of the O" + echo "revisions, and now give you all the revisions in the N branch from the common" + echo "base, B ($baserev), up to the new revision." + fi + echo "" + echo $LOGBEGIN + git-rev-list --pretty $newrev ^$baserev + echo $LOGEND + echo "" + echo "Diffstat:" + git-diff-tree --no-color --stat -M -C --find-copies-harder $newrev ^$baserev + fi + ;; + "annotated tag") + # Should we allow changes to annotated tags? + if expr "$oldrev" : '0*$' >/dev/null + then + # If the old reference is "0000..0000" then this is a new atag + # and so oldrev is not valid + echo " to $newrev ($newrev_type)" + else + echo " to $newrev ($newrev_type)" + echo " from $oldrev" + fi + + # If this tag succeeds another, then show which tag it replaces + prevtag=$(git describe $newrev^ 2>/dev/null | sed 's/-g.*//') + if [ -n "$prevtag" ]; then + echo " replaces $prevtag" + fi + + # Read the tag details + eval $(git cat-file tag $newrev | \ + sed -n '4s/tagger \([^>]*>\)[^0-9]*\([0-9]*\).*/tagger="\1" ts="\2"/p') + tagged=$(date --date="1970-01-01 00:00:00 +0000 $ts seconds" +"$DATEFORMAT") + + echo " tagged by $tagger" + echo " on $tagged" + + echo "" + echo $LOGBEGIN + echo "" + + if [ -n "$prevtag" ]; then + git rev-list --pretty=short "$prevtag..$newrev" | git shortlog + else + git rev-list --pretty=short $newrev | git shortlog + fi + + echo $LOGEND + echo "" + ;; + *) + # By default, unannotated tags aren't allowed in; if + # they are though, it's debatable whether we would even want an + # email to be generated; however, I don't want to add another config + # option just for that. + # + # Unannotated tags are more about marking a point than releasing + # a version; therefore we don't do the shortlog summary that we + # do for annotated tags above - we simply show that the point has + # been marked, and print the log message for the marked point for + # reference purposes + # + # Note this section also catches any other reference type (although + # there aren't any) and deals with them in the same way. + if expr "$oldrev" : '0*$' >/dev/null + then + # If the old reference is "0000..0000" then this is a new tag + # and so oldrev is not valid + echo " as a new $refname_type" + echo " to $newrev ($newrev_type)" + else + echo " to $newrev ($newrev_type)" + echo " from $oldrev" + fi + echo "" + echo $LOGBEGIN + git-show --no-color --root -s $newrev + echo $LOGEND + echo "" + ;; +esac + +# Footer +cat <<-EOF + +hooks/update +--- +Git Source Code Management System +$0 $1 \\ + $2 \\ + $3 +EOF +#) | cat >&2 +) | /usr/sbin/sendmail -t + +# --- Finished +exit 0 diff --git a/templates/info--exclude b/templates/info--exclude new file mode 100644 index 0000000000..2c87b72dff --- /dev/null +++ b/templates/info--exclude @@ -0,0 +1,6 @@ +# git-ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/templates/this--description b/templates/this--description new file mode 100644 index 0000000000..c6f25e80b8 --- /dev/null +++ b/templates/this--description @@ -0,0 +1 @@ +Unnamed repository; edit this file to name it for gitweb. diff --git a/test-date.c b/test-date.c new file mode 100644 index 0000000000..62e8f2387a --- /dev/null +++ b/test-date.c @@ -0,0 +1,20 @@ +#include "cache.h" + +int main(int argc, char **argv) +{ + int i; + + for (i = 1; i < argc; i++) { + char result[100]; + time_t t; + + memcpy(result, "bad", 4); + parse_date(argv[i], result, sizeof(result)); + t = strtoul(result, NULL, 0); + printf("%s -> %s -> %s", argv[i], result, ctime(&t)); + + t = approxidate(argv[i]); + printf("%s -> %s\n", argv[i], ctime(&t)); + } + return 0; +} diff --git a/test-delta.c b/test-delta.c new file mode 100644 index 0000000000..16595ef0a9 --- /dev/null +++ b/test-delta.c @@ -0,0 +1,77 @@ +/* + * test-delta.c: test code to exercise diff-delta.c and patch-delta.c + * + * (C) 2005 Nicolas Pitre <nico@cam.org> + * + * This code is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#include "git-compat-util.h" +#include "delta.h" + +static const char usage[] = + "test-delta (-d|-p) <from_file> <data_file> <out_file>"; + +int main(int argc, char *argv[]) +{ + int fd; + struct stat st; + void *from_buf, *data_buf, *out_buf; + unsigned long from_size, data_size, out_size; + + if (argc != 5 || (strcmp(argv[1], "-d") && strcmp(argv[1], "-p"))) { + fprintf(stderr, "Usage: %s\n", usage); + return 1; + } + + fd = open(argv[2], O_RDONLY); + if (fd < 0 || fstat(fd, &st)) { + perror(argv[2]); + return 1; + } + from_size = st.st_size; + from_buf = mmap(NULL, from_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (from_buf == MAP_FAILED) { + perror(argv[2]); + close(fd); + return 1; + } + close(fd); + + fd = open(argv[3], O_RDONLY); + if (fd < 0 || fstat(fd, &st)) { + perror(argv[3]); + return 1; + } + data_size = st.st_size; + data_buf = mmap(NULL, data_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (data_buf == MAP_FAILED) { + perror(argv[3]); + close(fd); + return 1; + } + close(fd); + + if (argv[1][1] == 'd') + out_buf = diff_delta(from_buf, from_size, + data_buf, data_size, + &out_size, 0); + else + out_buf = patch_delta(from_buf, from_size, + data_buf, data_size, + &out_size); + if (!out_buf) { + fprintf(stderr, "delta operation failed (returned NULL)\n"); + return 1; + } + + fd = open (argv[4], O_WRONLY|O_CREAT|O_TRUNC, 0666); + if (fd < 0 || write_in_full(fd, out_buf, out_size) != out_size) { + perror(argv[4]); + return 1; + } + + return 0; +} diff --git a/test-sha1.c b/test-sha1.c new file mode 100644 index 0000000000..78d7e983a7 --- /dev/null +++ b/test-sha1.c @@ -0,0 +1,47 @@ +#include "cache.h" + +int main(int ac, char **av) +{ + SHA_CTX ctx; + unsigned char sha1[20]; + unsigned bufsz = 8192; + char *buffer; + + if (ac == 2) + bufsz = strtoul(av[1], NULL, 10) * 1024 * 1024; + + if (!bufsz) + bufsz = 8192; + + while ((buffer = malloc(bufsz)) == NULL) { + fprintf(stderr, "bufsz %u is too big, halving...\n", bufsz); + bufsz /= 2; + if (bufsz < 1024) + die("OOPS"); + } + + SHA1_Init(&ctx); + + while (1) { + ssize_t sz, this_sz; + char *cp = buffer; + unsigned room = bufsz; + this_sz = 0; + while (room) { + sz = xread(0, cp, room); + if (sz == 0) + break; + if (sz < 0) + die("test-sha1: %s", strerror(errno)); + this_sz += sz; + cp += sz; + room -= sz; + } + if (this_sz == 0) + break; + SHA1_Update(&ctx, buffer, this_sz); + } + SHA1_Final(sha1, &ctx); + puts(sha1_to_hex(sha1)); + exit(0); +} diff --git a/test-sha1.sh b/test-sha1.sh new file mode 100755 index 0000000000..640856af5a --- /dev/null +++ b/test-sha1.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +dd if=/dev/zero bs=1048576 count=100 2>/dev/null | +/usr/bin/time ./test-sha1 >/dev/null + +while read expect cnt pfx +do + case "$expect" in '#'*) continue ;; esac + actual=` + { + test -z "$pfx" || echo "$pfx" + dd if=/dev/zero bs=1048576 count=$cnt 2>/dev/null | + tr '[\0]' '[g]' + } | ./test-sha1 $cnt + ` + if test "$expect" = "$actual" + then + echo "OK: $expect $cnt $pfx" + else + echo >&2 "OOPS: $cnt" + echo >&2 "expect: $expect" + echo >&2 "actual: $actual" + exit 1 + fi +done <<EOF +da39a3ee5e6b4b0d3255bfef95601890afd80709 0 +3f786850e387550fdab836ed7e6dc881de23001b 0 a +5277cbb45a15902137d332d97e89cf8136545485 0 ab +03cfd743661f07975fa2f1220c5194cbaff48451 0 abc +3330b4373640f9e4604991e73c7e86bfd8da2dc3 0 abcd +ec11312386ad561674f724b8cca7cf1796e26d1d 0 abcde +bdc37c074ec4ee6050d68bc133c6b912f36474df 0 abcdef +69bca99b923859f2dc486b55b87f49689b7358c7 0 abcdefg +e414af7161c9554089f4106d6f1797ef14a73666 0 abcdefgh +0707f2970043f9f7c22029482db27733deaec029 0 abcdefghi +a4dd8aa74a5636728fe52451636e2e17726033aa 1 +9986b45e2f4d7086372533bb6953a8652fa3644a 1 frotz +23d8d4f788e8526b4877548a32577543cbaaf51f 10 +8cd23f822ab44c7f481b8c92d591f6d1fcad431c 10 frotz +f3b5604a4e604899c1233edb3bf1cc0ede4d8c32 512 +b095bd837a371593048136e429e9ac4b476e1bb3 512 frotz +08fa81d6190948de5ccca3966340cc48c10cceac 1200 xyzzy +e33a291f42c30a159733dd98b8b3e4ff34158ca0 4090 4G +#a3bf783bc20caa958f6cb24dd140a7b21984838d 9999 nitfol +EOF + +exit + +# generating test vectors +# inputs are number of megabytes followed by some random string to prefix. + +while read cnt pfx +do + actual=` + { + test -z "$pfx" || echo "$pfx" + dd if=/dev/zero bs=1048576 count=$cnt 2>/dev/null | + tr '[\0]' '[g]' + } | sha1sum | + sed -e 's/ .*//' + ` + echo "$actual $cnt $pfx" +done <<EOF +0 +0 a +0 ab +0 abc +0 abcd +0 abcde +0 abcdef +0 abcdefg +0 abcdefgh +0 abcdefghi +1 +1 frotz +10 +10 frotz +512 +512 frotz +1200 xyzzy +4090 4G +9999 nitfol +EOF diff --git a/trace.c b/trace.c new file mode 100644 index 0000000000..27fef868c4 --- /dev/null +++ b/trace.c @@ -0,0 +1,150 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2004 Oswald Buddenhagen <ossi@users.sf.net> + * Copyright (C) 2004 Theodore Y. Ts'o <tytso@mit.edu> + * Copyright (C) 2006 Mike McCormack + * Copyright (C) 2006 Christian Couder + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "cache.h" +#include "quote.h" + +/* Stolen from "imap-send.c". */ +static int git_vasprintf(char **strp, const char *fmt, va_list ap) +{ + int len; + char tmp[1024]; + + if ((len = vsnprintf(tmp, sizeof(tmp), fmt, ap)) < 0 || + !(*strp = xmalloc(len + 1))) + return -1; + if (len >= (int)sizeof(tmp)) + vsprintf(*strp, fmt, ap); + else + memcpy(*strp, tmp, len + 1); + return len; +} + +/* Stolen from "imap-send.c". */ +int nfvasprintf(char **str, const char *fmt, va_list va) +{ + int ret = git_vasprintf(str, fmt, va); + if (ret < 0) + die("Fatal: Out of memory\n"); + return ret; +} + +/* Get a trace file descriptor from GIT_TRACE env variable. */ +static int get_trace_fd(int *need_close) +{ + char *trace = getenv("GIT_TRACE"); + + if (!trace || !strcmp(trace, "") || + !strcmp(trace, "0") || !strcasecmp(trace, "false")) + return 0; + if (!strcmp(trace, "1") || !strcasecmp(trace, "true")) + return STDERR_FILENO; + if (strlen(trace) == 1 && isdigit(*trace)) + return atoi(trace); + if (*trace == '/') { + int fd = open(trace, O_WRONLY | O_APPEND | O_CREAT, 0666); + if (fd == -1) { + fprintf(stderr, + "Could not open '%s' for tracing: %s\n" + "Defaulting to tracing on stderr...\n", + trace, strerror(errno)); + return STDERR_FILENO; + } + *need_close = 1; + return fd; + } + + fprintf(stderr, "What does '%s' for GIT_TRACE means ?\n", trace); + fprintf(stderr, "If you want to trace into a file, " + "then please set GIT_TRACE to an absolute pathname " + "(starting with /).\n"); + fprintf(stderr, "Defaulting to tracing on stderr...\n"); + + return STDERR_FILENO; +} + +static const char err_msg[] = "Could not trace into fd given by " + "GIT_TRACE environment variable"; + +void trace_printf(const char *format, ...) +{ + char *trace_str; + va_list rest; + int need_close = 0; + int fd = get_trace_fd(&need_close); + + if (!fd) + return; + + va_start(rest, format); + nfvasprintf(&trace_str, format, rest); + va_end(rest); + + write_or_whine_pipe(fd, trace_str, strlen(trace_str), err_msg); + + free(trace_str); + + if (need_close) + close(fd); +} + +void trace_argv_printf(const char **argv, int count, const char *format, ...) +{ + char *argv_str, *format_str, *trace_str; + size_t argv_len, format_len, trace_len; + va_list rest; + int need_close = 0; + int fd = get_trace_fd(&need_close); + + if (!fd) + return; + + /* Get the argv string. */ + argv_str = sq_quote_argv(argv, count); + argv_len = strlen(argv_str); + + /* Get the formated string. */ + va_start(rest, format); + nfvasprintf(&format_str, format, rest); + va_end(rest); + + /* Allocate buffer for trace string. */ + format_len = strlen(format_str); + trace_len = argv_len + format_len + 1; /* + 1 for \n */ + trace_str = xmalloc(trace_len + 1); + + /* Copy everything into the trace string. */ + strncpy(trace_str, format_str, format_len); + strncpy(trace_str + format_len, argv_str, argv_len); + strcpy(trace_str + trace_len - 1, "\n"); + + write_or_whine_pipe(fd, trace_str, trace_len, err_msg); + + free(argv_str); + free(format_str); + free(trace_str); + + if (need_close) + close(fd); +} diff --git a/tree-diff.c b/tree-diff.c new file mode 100644 index 0000000000..37d235e06e --- /dev/null +++ b/tree-diff.c @@ -0,0 +1,268 @@ +/* + * Helper functions for tree diff generation + */ +#include "cache.h" +#include "diff.h" +#include "tree.h" + +static char *malloc_base(const char *base, const char *path, int pathlen) +{ + int baselen = strlen(base); + char *newbase = xmalloc(baselen + pathlen + 2); + memcpy(newbase, base, baselen); + memcpy(newbase + baselen, path, pathlen); + memcpy(newbase + baselen + pathlen, "/", 2); + return newbase; +} + +static void show_entry(struct diff_options *opt, const char *prefix, struct tree_desc *desc, + const char *base); + +static int compare_tree_entry(struct tree_desc *t1, struct tree_desc *t2, const char *base, struct diff_options *opt) +{ + unsigned mode1, mode2; + const char *path1, *path2; + const unsigned char *sha1, *sha2; + int cmp, pathlen1, pathlen2; + + sha1 = tree_entry_extract(t1, &path1, &mode1); + sha2 = tree_entry_extract(t2, &path2, &mode2); + + pathlen1 = strlen(path1); + pathlen2 = strlen(path2); + cmp = base_name_compare(path1, pathlen1, mode1, path2, pathlen2, mode2); + if (cmp < 0) { + show_entry(opt, "-", t1, base); + return -1; + } + if (cmp > 0) { + show_entry(opt, "+", t2, base); + return 1; + } + if (!opt->find_copies_harder && !hashcmp(sha1, sha2) && mode1 == mode2) + return 0; + + /* + * If the filemode has changed to/from a directory from/to a regular + * file, we need to consider it a remove and an add. + */ + if (S_ISDIR(mode1) != S_ISDIR(mode2)) { + show_entry(opt, "-", t1, base); + show_entry(opt, "+", t2, base); + return 0; + } + + if (opt->recursive && S_ISDIR(mode1)) { + int retval; + char *newbase = malloc_base(base, path1, pathlen1); + if (opt->tree_in_recursive) + opt->change(opt, mode1, mode2, + sha1, sha2, base, path1); + retval = diff_tree_sha1(sha1, sha2, newbase, opt); + free(newbase); + return retval; + } + + opt->change(opt, mode1, mode2, sha1, sha2, base, path1); + return 0; +} + +static int interesting(struct tree_desc *desc, const char *base, struct diff_options *opt) +{ + const char *path; + unsigned mode; + int i; + int baselen, pathlen; + + if (!opt->nr_paths) + return 1; + + (void)tree_entry_extract(desc, &path, &mode); + + pathlen = strlen(path); + baselen = strlen(base); + + for (i=0; i < opt->nr_paths; i++) { + const char *match = opt->paths[i]; + int matchlen = opt->pathlens[i]; + + if (baselen >= matchlen) { + /* If it doesn't match, move along... */ + if (strncmp(base, match, matchlen)) + continue; + + /* The base is a subdirectory of a path which was specified. */ + return 1; + } + + /* Does the base match? */ + if (strncmp(base, match, baselen)) + continue; + + match += baselen; + matchlen -= baselen; + + if (pathlen > matchlen) + continue; + + if (matchlen > pathlen) { + if (match[pathlen] != '/') + continue; + if (!S_ISDIR(mode)) + continue; + } + + if (strncmp(path, match, pathlen)) + continue; + + return 1; + } + return 0; /* No matches */ +} + +/* A whole sub-tree went away or appeared */ +static void show_tree(struct diff_options *opt, const char *prefix, struct tree_desc *desc, const char *base) +{ + while (desc->size) { + if (interesting(desc, base, opt)) + show_entry(opt, prefix, desc, base); + update_tree_entry(desc); + } +} + +/* A file entry went away or appeared */ +static void show_entry(struct diff_options *opt, const char *prefix, struct tree_desc *desc, + const char *base) +{ + unsigned mode; + const char *path; + const unsigned char *sha1 = tree_entry_extract(desc, &path, &mode); + + if (opt->recursive && S_ISDIR(mode)) { + char type[20]; + char *newbase = malloc_base(base, path, strlen(path)); + struct tree_desc inner; + void *tree; + + tree = read_sha1_file(sha1, type, &inner.size); + if (!tree || strcmp(type, tree_type)) + die("corrupt tree sha %s", sha1_to_hex(sha1)); + + inner.buf = tree; + show_tree(opt, prefix, &inner, newbase); + + free(tree); + free(newbase); + } else { + opt->add_remove(opt, prefix[0], mode, sha1, base, path); + } +} + +int diff_tree(struct tree_desc *t1, struct tree_desc *t2, const char *base, struct diff_options *opt) +{ + while (t1->size | t2->size) { + if (opt->nr_paths && t1->size && !interesting(t1, base, opt)) { + update_tree_entry(t1); + continue; + } + if (opt->nr_paths && t2->size && !interesting(t2, base, opt)) { + update_tree_entry(t2); + continue; + } + if (!t1->size) { + show_entry(opt, "+", t2, base); + update_tree_entry(t2); + continue; + } + if (!t2->size) { + show_entry(opt, "-", t1, base); + update_tree_entry(t1); + continue; + } + switch (compare_tree_entry(t1, t2, base, opt)) { + case -1: + update_tree_entry(t1); + continue; + case 0: + update_tree_entry(t1); + /* Fallthrough */ + case 1: + update_tree_entry(t2); + continue; + } + die("git-diff-tree: internal error"); + } + return 0; +} + +int diff_tree_sha1(const unsigned char *old, const unsigned char *new, const char *base, struct diff_options *opt) +{ + void *tree1, *tree2; + struct tree_desc t1, t2; + int retval; + + tree1 = read_object_with_reference(old, tree_type, &t1.size, NULL); + if (!tree1) + die("unable to read source tree (%s)", sha1_to_hex(old)); + tree2 = read_object_with_reference(new, tree_type, &t2.size, NULL); + if (!tree2) + die("unable to read destination tree (%s)", sha1_to_hex(new)); + t1.buf = tree1; + t2.buf = tree2; + retval = diff_tree(&t1, &t2, base, opt); + free(tree1); + free(tree2); + return retval; +} + +int diff_root_tree_sha1(const unsigned char *new, const char *base, struct diff_options *opt) +{ + int retval; + void *tree; + struct tree_desc empty, real; + + tree = read_object_with_reference(new, tree_type, &real.size, NULL); + if (!tree) + die("unable to read root tree (%s)", sha1_to_hex(new)); + real.buf = tree; + + empty.size = 0; + empty.buf = ""; + retval = diff_tree(&empty, &real, base, opt); + free(tree); + return retval; +} + +static int count_paths(const char **paths) +{ + int i = 0; + while (*paths++) + i++; + return i; +} + +void diff_tree_release_paths(struct diff_options *opt) +{ + free(opt->pathlens); +} + +void diff_tree_setup_paths(const char **p, struct diff_options *opt) +{ + opt->nr_paths = 0; + opt->pathlens = NULL; + opt->paths = NULL; + + if (p) { + int i; + + opt->paths = p; + opt->nr_paths = count_paths(p); + if (opt->nr_paths == 0) { + opt->pathlens = NULL; + return; + } + opt->pathlens = xmalloc(opt->nr_paths * sizeof(int)); + for (i=0; i < opt->nr_paths; i++) + opt->pathlens[i] = strlen(p[i]); + } +} diff --git a/tree-walk.c b/tree-walk.c new file mode 100644 index 0000000000..70f899957e --- /dev/null +++ b/tree-walk.c @@ -0,0 +1,218 @@ +#include "cache.h" +#include "tree-walk.h" +#include "tree.h" + +void *fill_tree_descriptor(struct tree_desc *desc, const unsigned char *sha1) +{ + unsigned long size = 0; + void *buf = NULL; + + if (sha1) { + buf = read_object_with_reference(sha1, tree_type, &size, NULL); + if (!buf) + die("unable to read tree %s", sha1_to_hex(sha1)); + } + desc->size = size; + desc->buf = buf; + return buf; +} + +static int entry_compare(struct name_entry *a, struct name_entry *b) +{ + return base_name_compare( + a->path, a->pathlen, a->mode, + b->path, b->pathlen, b->mode); +} + +static void entry_clear(struct name_entry *a) +{ + memset(a, 0, sizeof(*a)); +} + +static void entry_extract(struct tree_desc *t, struct name_entry *a) +{ + a->sha1 = tree_entry_extract(t, &a->path, &a->mode); + a->pathlen = strlen(a->path); +} + +void update_tree_entry(struct tree_desc *desc) +{ + const void *buf = desc->buf; + unsigned long size = desc->size; + int len = strlen(buf) + 1 + 20; + + if (size < len) + die("corrupt tree file"); + desc->buf = (char *) buf + len; + 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) +{ + const void *tree = desc->buf; + unsigned long size = desc->size; + int len = strlen(tree)+1; + const unsigned char *sha1 = (unsigned char *) tree + len; + const char *path; + unsigned int mode; + + path = get_mode(tree, &mode); + if (!path || size < len + 20) + die("corrupt tree file"); + *pathp = path; + *modep = canon_mode(mode); + return sha1; +} + +int tree_entry(struct tree_desc *desc, struct name_entry *entry) +{ + const void *tree = desc->buf; + const char *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 = (const unsigned char *) path; + + path += 20; + len = path - (char *) 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)); + + for (;;) { + unsigned long mask = 0; + int i, last; + + last = -1; + for (i = 0; i < n; i++) { + if (!t[i].size) + continue; + entry_extract(t+i, entry+i); + if (last >= 0) { + int cmp = entry_compare(entry+i, entry+last); + + /* + * Is the new name bigger than the old one? + * Ignore it + */ + if (cmp > 0) + continue; + /* + * Is the new name smaller than the old one? + * Ignore all old ones + */ + if (cmp < 0) + mask = 0; + } + mask |= 1ul << i; + last = i; + } + if (!mask) + break; + + /* + * Update the tree entries we've walked, and clear + * all the unused name-entries. + */ + for (i = 0; i < n; i++) { + if (mask & (1ul << i)) { + update_tree_entry(t+i); + continue; + } + entry_clear(entry + i); + } + callback(n, mask, entry, base); + } + free(entry); +} + +static int find_tree_entry(struct tree_desc *t, const char *name, unsigned char *result, unsigned *mode) +{ + int namelen = strlen(name); + while (t->size) { + const char *entry; + const unsigned char *sha1; + int entrylen, cmp; + + sha1 = tree_entry_extract(t, &entry, mode); + update_tree_entry(t); + entrylen = strlen(entry); + if (entrylen > namelen) + continue; + cmp = memcmp(name, entry, entrylen); + if (cmp > 0) + continue; + if (cmp < 0) + break; + if (entrylen == namelen) { + hashcpy(result, sha1); + return 0; + } + if (name[entrylen] != '/') + continue; + if (!S_ISDIR(*mode)) + break; + if (++entrylen == namelen) { + hashcpy(result, sha1); + return 0; + } + return get_tree_entry(sha1, name + entrylen, result, mode); + } + return -1; +} + +int get_tree_entry(const unsigned char *tree_sha1, const char *name, unsigned char *sha1, unsigned *mode) +{ + int retval; + void *tree; + struct tree_desc t; + unsigned char root[20]; + + tree = read_object_with_reference(tree_sha1, tree_type, &t.size, root); + if (!tree) + return -1; + + if (name[0] == '\0') { + hashcpy(sha1, root); + return 0; + } + + t.buf = tree; + retval = find_tree_entry(&t, name, sha1, mode); + free(tree); + return retval; +} + diff --git a/tree-walk.h b/tree-walk.h new file mode 100644 index 0000000000..e57befa4da --- /dev/null +++ b/tree-walk.h @@ -0,0 +1,30 @@ +#ifndef TREE_WALK_H +#define TREE_WALK_H + +struct tree_desc { + const void *buf; + unsigned long size; +}; + +struct name_entry { + const unsigned char *sha1; + const char *path; + unsigned int mode; + int pathlen; +}; + +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); + +void traverse_trees(int n, struct tree_desc *t, const char *base, traverse_callback_t callback); + +int get_tree_entry(const unsigned char *, const char *, unsigned char *, unsigned *); + +#endif diff --git a/tree.c b/tree.c new file mode 100644 index 0000000000..b6f02fecc4 --- /dev/null +++ b/tree.c @@ -0,0 +1,228 @@ +#include "cache.h" +#include "tree.h" +#include "blob.h" +#include "commit.h" +#include "tag.h" +#include "tree-walk.h" + +const char *tree_type = "tree"; + +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; + struct cache_entry *ce; + + if (S_ISDIR(mode)) + return READ_TREE_RECURSIVE; + + len = strlen(pathname); + size = cache_entry_size(baselen + len); + ce = xcalloc(1, size); + + ce->ce_mode = create_ce_mode(mode); + ce->ce_flags = create_ce_flags(baselen + len, stage); + memcpy(ce->name, base, baselen); + memcpy(ce->name + baselen, pathname, len+1); + hashcpy(ce->sha1, sha1); + return add_cache_entry(ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_SKIP_DFCHECK); +} + +static int match_tree_entry(const char *base, int baselen, const char *path, unsigned int mode, const char **paths) +{ + const char *match; + int pathlen; + + if (!paths) + return 1; + pathlen = strlen(path); + while ((match = *paths++) != NULL) { + int matchlen = strlen(match); + + if (baselen >= matchlen) { + /* If it doesn't match, move along... */ + if (strncmp(base, match, matchlen)) + continue; + /* The base is a subdirectory of a path which was specified. */ + return 1; + } + + /* Does the base match? */ + if (strncmp(base, match, baselen)) + continue; + + match += baselen; + matchlen -= baselen; + + if (pathlen > matchlen) + continue; + + if (matchlen > pathlen) { + if (match[pathlen] != '/') + continue; + if (!S_ISDIR(mode)) + continue; + } + + if (strncmp(path, match, pathlen)) + continue; + + return 1; + } + return 0; +} + +int read_tree_recursive(struct tree *tree, + const char *base, int baselen, + int stage, const char **match, + read_tree_fn_t fn) +{ + struct tree_desc desc; + struct name_entry entry; + + if (parse_tree(tree)) + return -1; + + 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(entry.sha1, base, baselen, entry.path, entry.mode, stage)) { + case 0: + continue; + case READ_TREE_RECURSIVE: + break;; + default: + return -1; + } + if (S_ISDIR(entry.mode)) { + int retval; + char *newbase; + + newbase = xmalloc(baselen + 1 + entry.pathlen); + memcpy(newbase, base, baselen); + memcpy(newbase + baselen, entry.path, entry.pathlen); + newbase[baselen + entry.pathlen] = '/'; + retval = read_tree_recursive(lookup_tree(entry.sha1), + newbase, + baselen + entry.pathlen + 1, + stage, match, fn); + free(newbase); + if (retval) + return -1; + continue; + } + } + return 0; +} + +int read_tree(struct tree *tree, int stage, const char **match) +{ + return read_tree_recursive(tree, "", 0, stage, match, read_one_entry); +} + +struct tree *lookup_tree(const unsigned char *sha1) +{ + struct object *obj = lookup_object(sha1); + if (!obj) { + struct tree *ret = alloc_tree_node(); + created_object(sha1, &ret->object); + ret->object.type = OBJ_TREE; + return ret; + } + if (!obj->type) + obj->type = OBJ_TREE; + if (obj->type != OBJ_TREE) { + error("Object %s is a %s, not a tree", + sha1_to_hex(sha1), typename(obj->type)); + return NULL; + } + return (struct tree *) obj; +} + +static void track_tree_refs(struct tree *item) +{ + 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); + } + + /* 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; + + 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); +} + +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; +} + +int parse_tree(struct tree *item) +{ + char type[20]; + void *buffer; + unsigned long size; + + if (item->object.parsed) + return 0; + buffer = read_sha1_file(item->object.sha1, type, &size); + if (!buffer) + return error("Could not read %s", + sha1_to_hex(item->object.sha1)); + if (strcmp(type, tree_type)) { + free(buffer); + return error("Object %s not a tree", + sha1_to_hex(item->object.sha1)); + } + return parse_tree_buffer(item, buffer, size); +} + +struct tree *parse_tree_indirect(const unsigned char *sha1) +{ + struct object *obj = parse_object(sha1); + do { + if (!obj) + return NULL; + if (obj->type == OBJ_TREE) + return (struct tree *) obj; + else if (obj->type == OBJ_COMMIT) + obj = &(((struct commit *) obj)->tree->object); + else if (obj->type == OBJ_TAG) + obj = ((struct tag *) obj)->tagged; + else + return NULL; + if (!obj->parsed) + parse_object(obj->sha1); + } while (1); +} diff --git a/tree.h b/tree.h new file mode 100644 index 0000000000..dd25c539ef --- /dev/null +++ b/tree.h @@ -0,0 +1,33 @@ +#ifndef TREE_H +#define TREE_H + +#include "object.h" + +extern const char *tree_type; + +struct tree { + struct object object; + void *buffer; + unsigned long size; +}; + +struct tree *lookup_tree(const unsigned char *sha1); + +int parse_tree_buffer(struct tree *item, void *buffer, unsigned long size); + +int parse_tree(struct tree *tree); + +/* Parses and returns the tree in the given ent, chasing tags and commits. */ +struct tree *parse_tree_indirect(const unsigned char *sha1); + +#define READ_TREE_RECURSIVE 1 +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, + int stage, const char **match, + read_tree_fn_t fn); + +extern int read_tree(struct tree *tree, int stage, const char **paths); + +#endif /* TREE_H */ diff --git a/unpack-file.c b/unpack-file.c new file mode 100644 index 0000000000..d24acc2a67 --- /dev/null +++ b/unpack-file.c @@ -0,0 +1,40 @@ +#include "cache.h" +#include "blob.h" + +static char *create_temp_file(unsigned char *sha1) +{ + static char path[50]; + void *buf; + char type[100]; + unsigned long size; + int fd; + + buf = read_sha1_file(sha1, type, &size); + if (!buf || strcmp(type, blob_type)) + die("unable to read blob object %s", sha1_to_hex(sha1)); + + strcpy(path, ".merge_file_XXXXXX"); + fd = mkstemp(path); + if (fd < 0) + die("unable to create temp-file"); + if (write_in_full(fd, buf, size) != size) + die("unable to write temp-file"); + close(fd); + return path; +} + +int main(int argc, char **argv) +{ + unsigned char sha1[20]; + + if (argc != 2) + usage("git-unpack-file <sha1>"); + if (get_sha1(argv[1], sha1)) + die("Not a valid object name %s", argv[1]); + + setup_git_directory(); + git_config(git_default_config); + + puts(create_temp_file(sha1)); + return 0; +} diff --git a/unpack-trees.c b/unpack-trees.c new file mode 100644 index 0000000000..2e2232cbb0 --- /dev/null +++ b/unpack-trees.c @@ -0,0 +1,811 @@ +#include "cache.h" +#include "dir.h" +#include "tree.h" +#include "tree-walk.h" +#include "cache-tree.h" +#include "unpack-trees.h" + +#define DBRT_DEBUG 1 + +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 *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; + + if (!tree->object.parsed) + parse_tree(tree); + + 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 len = len1 < len2 ? len1 : len2; + int ret = memcmp(name1, name2, len); + unsigned char c1, c2; + if (ret) + return ret; + c1 = name1[len]; + c2 = name2[len]; + if (!c1 && dir1) + c1 = '/'; + if (!c2 && dir2) + c2 = '/'; + ret = (c1 < c2) ? -1 : (c1 > c2) ? 1 : 0; + if (c1 && c2 && !ret) + ret = len1 - len2; + return ret; +} + +static int unpack_trees_rec(struct tree_entry_list **posns, int len, + const char *base, struct unpack_trees_options *o, + int *indpos, + struct tree_entry_list *df_conflict_list) +{ + int baselen = strlen(base); + int src_size = len + 1; + int i_stk = i_stk; + int retval = 0; + + if (o->dir) + i_stk = push_exclude_per_directory(o->dir, base, strlen(base)); + + do { + int i; + const char *first; + int firstdir = 0; + int pathlen; + unsigned ce_size; + struct tree_entry_list **subposns; + struct cache_entry **src; + int any_files = 0; + int any_dirs = 0; + char *cache_name; + int ce_stage; + + /* Find the first name in the input. */ + + first = NULL; + cache_name = NULL; + + /* Check the cache */ + if (o->merge && *indpos < active_nr) { + /* This is a bit tricky: */ + /* If the index has a subdirectory (with + * contents) as the first name, it'll get a + * filename like "foo/bar". But that's after + * "foo", so the entry in trees will get + * handled first, at which point we'll go into + * "foo", and deal with "bar" from the index, + * because the base will be "foo/". The only + * way we can actually have "foo/bar" first of + * all the things is if the trees don't + * contain "foo" at all, in which case we'll + * handle "foo/bar" without going into the + * directory, but that's fine (and will return + * an error anyway, with the added unknown + * file case. + */ + + cache_name = active_cache[*indpos]->name; + if (strlen(cache_name) > baselen && + !memcmp(cache_name, base, baselen)) { + cache_name += baselen; + first = cache_name; + } else { + cache_name = NULL; + } + } + +#if DBRT_DEBUG > 1 + if (first) + printf("index %s\n", first); +#endif + for (i = 0; i < len; i++) { + if (!posns[i] || posns[i] == df_conflict_list) + continue; +#if DBRT_DEBUG > 1 + printf("%d %s\n", i + 1, posns[i]->name); +#endif + if (!first || entcmp(first, firstdir, + posns[i]->name, + posns[i]->directory) > 0) { + first = posns[i]->name; + firstdir = posns[i]->directory; + } + } + /* No name means we're done */ + if (!first) + goto leave_directory; + + pathlen = strlen(first); + ce_size = cache_entry_size(baselen + pathlen); + + src = xcalloc(src_size, sizeof(struct cache_entry *)); + + subposns = xcalloc(len, sizeof(struct tree_list_entry *)); + + if (cache_name && !strcmp(cache_name, first)) { + any_files = 1; + src[0] = active_cache[*indpos]; + remove_cache_entry_at(*indpos); + } + + for (i = 0; i < len; i++) { + struct cache_entry *ce; + + if (!posns[i] || + (posns[i] != df_conflict_list && + strcmp(first, posns[i]->name))) { + continue; + } + + if (posns[i] == df_conflict_list) { + src[i + o->merge] = o->df_conflict_entry; + continue; + } + + if (posns[i]->directory) { + struct tree *tree = lookup_tree(posns[i]->sha1); + any_dirs = 1; + parse_tree(tree); + subposns[i] = create_tree_entry_list(tree); + posns[i] = posns[i]->next; + src[i + o->merge] = o->df_conflict_entry; + continue; + } + + if (!o->merge) + ce_stage = 0; + else if (i + 1 < o->head_idx) + ce_stage = 1; + else if (i + 1 > o->head_idx) + ce_stage = 3; + else + ce_stage = 2; + + ce = xcalloc(1, ce_size); + ce->ce_mode = create_ce_mode(posns[i]->mode); + ce->ce_flags = create_ce_flags(baselen + pathlen, + ce_stage); + memcpy(ce->name, base, baselen); + memcpy(ce->name + baselen, first, pathlen + 1); + + any_files = 1; + + hashcpy(ce->sha1, posns[i]->sha1); + src[i + o->merge] = ce; + subposns[i] = df_conflict_list; + posns[i] = posns[i]->next; + } + if (any_files) { + if (o->merge) { + int ret; + +#if DBRT_DEBUG > 1 + printf("%s:\n", first); + for (i = 0; i < src_size; i++) { + printf(" %d ", i); + if (src[i]) + printf("%s\n", sha1_to_hex(src[i]->sha1)); + else + printf("\n"); + } +#endif + ret = o->fn(src, o); + +#if DBRT_DEBUG > 1 + printf("Added %d entries\n", ret); +#endif + *indpos += ret; + } else { + for (i = 0; i < src_size; i++) { + if (src[i]) { + add_cache_entry(src[i], ADD_CACHE_OK_TO_ADD|ADD_CACHE_SKIP_DFCHECK); + } + } + } + } + if (any_dirs) { + char *newbase = xmalloc(baselen + 2 + pathlen); + memcpy(newbase, base, baselen); + memcpy(newbase + baselen, first, pathlen); + newbase[baselen + pathlen] = '/'; + newbase[baselen + pathlen + 1] = '\0'; + if (unpack_trees_rec(subposns, len, newbase, o, + indpos, df_conflict_list)) { + retval = -1; + goto leave_directory; + } + free(newbase); + } + free(subposns); + free(src); + } while (1); + + leave_directory: + if (o->dir) + pop_exclude_per_directory(o->dir, i_stk); + return retval; +} + +/* Unlink the last component and attempt to remove leading + * directories, in case this unlink is the removal of the + * last entry in the directory -- empty directories are removed. + */ +static void unlink_entry(char *name) +{ + char *cp, *prev; + + if (unlink(name)) + return; + prev = NULL; + while (1) { + int status; + cp = strrchr(name, '/'); + if (prev) + *prev = '/'; + if (!cp) + break; + + *cp = 0; + status = rmdir(name); + if (status) { + *cp = '/'; + break; + } + prev = cp; + } +} + +static volatile sig_atomic_t progress_update; + +static void progress_interval(int signum) +{ + progress_update = 1; +} + +static void setup_progress_signal(void) +{ + struct sigaction sa; + struct itimerval v; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = progress_interval; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction(SIGALRM, &sa, NULL); + + v.it_interval.tv_sec = 1; + v.it_interval.tv_usec = 0; + v.it_value = v.it_interval; + setitimer(ITIMER_REAL, &v, NULL); +} + +static struct checkout state; +static void check_updates(struct cache_entry **src, int nr, + struct unpack_trees_options *o) +{ + unsigned short mask = htons(CE_UPDATE); + unsigned last_percent = 200, cnt = 0, total = 0; + + if (o->update && o->verbose_update) { + for (total = cnt = 0; cnt < nr; cnt++) { + struct cache_entry *ce = src[cnt]; + if (!ce->ce_mode || ce->ce_flags & mask) + total++; + } + + /* Don't bother doing this for very small updates */ + if (total < 250) + total = 0; + + if (total) { + fprintf(stderr, "Checking files out...\n"); + setup_progress_signal(); + progress_update = 1; + } + cnt = 0; + } + + while (nr--) { + struct cache_entry *ce = *src++; + + if (total) { + if (!ce->ce_mode || ce->ce_flags & mask) { + unsigned percent; + cnt++; + percent = (cnt * 100) / total; + if (percent != last_percent || + progress_update) { + fprintf(stderr, "%4u%% (%u/%u) done\r", + percent, cnt, total); + last_percent = percent; + progress_update = 0; + } + } + } + if (!ce->ce_mode) { + if (o->update) + unlink_entry(ce->name); + continue; + } + if (ce->ce_flags & mask) { + ce->ce_flags &= ~mask; + if (o->update) + checkout_entry(ce, &state, NULL); + } + } + if (total) { + signal(SIGALRM, SIG_IGN); + fputc('\n', stderr); + } +} + +int unpack_trees(struct object_list *trees, struct unpack_trees_options *o) +{ + int indpos = 0; + unsigned len = object_list_length(trees); + struct tree_entry_list **posns; + int i; + struct object_list *posn = trees; + struct tree_entry_list df_conflict_list; + static struct cache_entry *dfc; + + memset(&df_conflict_list, 0, sizeof(df_conflict_list)); + df_conflict_list.next = &df_conflict_list; + memset(&state, 0, sizeof(state)); + state.base_dir = ""; + state.force = 1; + state.quiet = 1; + state.refresh_cache = 1; + + o->merge_size = len; + + if (!dfc) + dfc = xcalloc(1, sizeof(struct cache_entry) + 1); + o->df_conflict_entry = dfc; + + if (len) { + posns = xmalloc(len * sizeof(struct tree_entry_list *)); + for (i = 0; i < len; i++) { + posns[i] = create_tree_entry_list((struct tree *) posn->item); + posn = posn->next; + } + if (unpack_trees_rec(posns, len, o->prefix ? o->prefix : "", + o, &indpos, &df_conflict_list)) + return -1; + } + + if (o->trivial_merges_only && o->nontrivial_merge) + die("Merge requires file-level merging"); + + check_updates(active_cache, active_nr, o); + return 0; +} + +/* Here come the merge functions */ + +static void reject_merge(struct cache_entry *ce) +{ + die("Entry '%s' would be overwritten by merge. Cannot merge.", + ce->name); +} + +static int same(struct cache_entry *a, struct cache_entry *b) +{ + if (!!a != !!b) + return 0; + if (!a && !b) + return 1; + return a->ce_mode == b->ce_mode && + !hashcmp(a->sha1, b->sha1); +} + + +/* + * When a CE gets turned into an unmerged entry, we + * want it to be up-to-date + */ +static void verify_uptodate(struct cache_entry *ce, + struct unpack_trees_options *o) +{ + struct stat st; + + if (o->index_only || o->reset) + return; + + if (!lstat(ce->name, &st)) { + unsigned changed = ce_match_stat(ce, &st, 1); + if (!changed) + return; + errno = 0; + } + if (o->reset) { + ce->ce_flags |= htons(CE_UPDATE); + return; + } + if (errno == ENOENT) + return; + 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, unless it is ignored. + */ +static void verify_absent(const char *path, const char *action, + struct unpack_trees_options *o) +{ + struct stat st; + + if (o->index_only || o->reset || !o->update) + return; + if (!lstat(path, &st) && !(o->dir && excluded(o->dir, path))) + die("Untracked working tree file '%s' " + "would be %s by merge.", path, action); +} + +static int merged_entry(struct cache_entry *merge, struct cache_entry *old, + struct unpack_trees_options *o) +{ + merge->ce_flags |= htons(CE_UPDATE); + if (old) { + /* + * See if we can re-use the old CE directly? + * That way we get the uptodate stat info. + * + * This also removes the UPDATE flag on + * a match. + */ + if (same(old, merge)) { + *merge = *old; + } else { + verify_uptodate(old, o); + invalidate_ce_path(old); + } + } + else { + verify_absent(merge->name, "overwritten", o); + invalidate_ce_path(merge); + } + + merge->ce_flags &= ~htons(CE_STAGEMASK); + add_cache_entry(merge, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); + return 1; +} + +static int deleted_entry(struct cache_entry *ce, struct cache_entry *old, + struct unpack_trees_options *o) +{ + if (old) + verify_uptodate(old, o); + else + verify_absent(ce->name, "removed", o); + ce->ce_mode = 0; + add_cache_entry(ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); + invalidate_ce_path(ce); + return 1; +} + +static int keep_entry(struct cache_entry *ce) +{ + add_cache_entry(ce, ADD_CACHE_OK_TO_ADD); + return 1; +} + +#if DBRT_DEBUG +static void show_stage_entry(FILE *o, + const char *label, const struct cache_entry *ce) +{ + if (!ce) + fprintf(o, "%s (missing)\n", label); + else + fprintf(o, "%s%06o %s %d\t%s\n", + label, + ntohl(ce->ce_mode), + sha1_to_hex(ce->sha1), + ce_stage(ce), + ce->name); +} +#endif + +int threeway_merge(struct cache_entry **stages, + struct unpack_trees_options *o) +{ + struct cache_entry *index; + struct cache_entry *head; + struct cache_entry *remote = stages[o->head_idx + 1]; + int count; + int head_match = 0; + int remote_match = 0; + const char *path = NULL; + + int df_conflict_head = 0; + int df_conflict_remote = 0; + + int any_anc_missing = 0; + int no_anc_exists = 1; + int i; + + for (i = 1; i < o->head_idx; i++) { + if (!stages[i]) + any_anc_missing = 1; + else { + if (!path) + path = stages[i]->name; + no_anc_exists = 0; + } + } + + index = stages[0]; + head = stages[o->head_idx]; + + if (head == o->df_conflict_entry) { + df_conflict_head = 1; + head = NULL; + } + + if (remote == o->df_conflict_entry) { + df_conflict_remote = 1; + remote = NULL; + } + + if (!path && index) + path = index->name; + if (!path && head) + path = head->name; + if (!path && remote) + path = remote->name; + + /* First, if there's a #16 situation, note that to prevent #13 + * and #14. + */ + if (!same(remote, head)) { + for (i = 1; i < o->head_idx; i++) { + if (same(stages[i], head)) { + head_match = i; + } + if (same(stages[i], remote)) { + remote_match = i; + } + } + } + + /* We start with cases where the index is allowed to match + * something other than the head: #14(ALT) and #2ALT, where it + * is permitted to match the result instead. + */ + /* #14, #14ALT, #2ALT */ + if (remote && !df_conflict_head && head_match && !remote_match) { + if (index && !same(index, remote) && !same(index, head)) + reject_merge(index); + return merged_entry(remote, index, o); + } + /* + * If we have an entry in the index cache, then we want to + * make sure that it matches head. + */ + if (index && !same(index, head)) { + reject_merge(index); + } + + if (head) { + /* #5ALT, #15 */ + if (same(head, remote)) + return merged_entry(head, index, o); + /* #13, #3ALT */ + if (!df_conflict_remote && remote_match && !head_match) + return merged_entry(head, index, o); + } + + /* #1 */ + if (!head && !remote && any_anc_missing) + return 0; + + /* Under the new "aggressive" rule, we resolve mostly trivial + * cases that we historically had git-merge-one-file resolve. + */ + if (o->aggressive) { + int head_deleted = !head && !df_conflict_head; + int remote_deleted = !remote && !df_conflict_remote; + /* + * Deleted in both. + * Deleted in one and unchanged in the other. + */ + if ((head_deleted && remote_deleted) || + (head_deleted && remote && remote_match) || + (remote_deleted && head && head_match)) { + if (index) + return deleted_entry(index, index, o); + else if (path && !head_deleted) + verify_absent(path, "removed", o); + return 0; + } + /* + * Added in both, identically. + */ + if (no_anc_exists && head && remote && same(head, remote)) + return merged_entry(head, index, o); + + } + + /* Below are "no merge" cases, which require that the index be + * up-to-date to avoid the files getting overwritten with + * conflict resolution files. + */ + if (index) { + verify_uptodate(index, o); + } + + o->nontrivial_merge = 1; + + /* #2, #3, #4, #6, #7, #9, #11. */ + count = 0; + if (!head_match || !remote_match) { + for (i = 1; i < o->head_idx; i++) { + if (stages[i]) { + keep_entry(stages[i]); + count++; + break; + } + } + } +#if DBRT_DEBUG + else { + fprintf(stderr, "read-tree: warning #16 detected\n"); + show_stage_entry(stderr, "head ", stages[head_match]); + show_stage_entry(stderr, "remote ", stages[remote_match]); + } +#endif + if (head) { count += keep_entry(head); } + if (remote) { count += keep_entry(remote); } + return count; +} + +/* + * Two-way merge. + * + * The rule is to "carry forward" what is in the index without losing + * information across a "fast forward", favoring a successful merge + * over a merge failure when it makes sense. For details of the + * "carry forward" rule, please see <Documentation/git-read-tree.txt>. + * + */ +int twoway_merge(struct cache_entry **src, + struct unpack_trees_options *o) +{ + struct cache_entry *current = src[0]; + struct cache_entry *oldtree = src[1], *newtree = src[2]; + + if (o->merge_size != 2) + return error("Cannot do a twoway merge of %d trees", + o->merge_size); + + if (current) { + if ((!oldtree && !newtree) || /* 4 and 5 */ + (!oldtree && newtree && + same(current, newtree)) || /* 6 and 7 */ + (oldtree && newtree && + same(oldtree, newtree)) || /* 14 and 15 */ + (oldtree && newtree && + !same(oldtree, newtree) && /* 18 and 19*/ + same(current, newtree))) { + return keep_entry(current); + } + else if (oldtree && !newtree && same(current, oldtree)) { + /* 10 or 11 */ + return deleted_entry(oldtree, current, o); + } + else if (oldtree && newtree && + same(current, oldtree) && !same(current, newtree)) { + /* 20 or 21 */ + return merged_entry(newtree, current, o); + } + else { + /* all other failures */ + if (oldtree) + reject_merge(oldtree); + if (current) + reject_merge(current); + if (newtree) + reject_merge(newtree); + return -1; + } + } + else if (newtree) + return merged_entry(newtree, current, o); + else + return deleted_entry(oldtree, current, o); +} + +/* + * Bind merge. + * + * Keep the index entries at stage0, collapse stage1 but make sure + * stage0 does not have anything there. + */ +int bind_merge(struct cache_entry **src, + struct unpack_trees_options *o) +{ + struct cache_entry *old = src[0]; + struct cache_entry *a = src[1]; + + if (o->merge_size != 1) + return error("Cannot do a bind merge of %d trees\n", + o->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, o); +} + +/* + * One-way merge. + * + * The rule is: + * - take the stat information from stage0, take the data from stage1 + */ +int oneway_merge(struct cache_entry **src, + struct unpack_trees_options *o) +{ + struct cache_entry *old = src[0]; + struct cache_entry *a = src[1]; + + if (o->merge_size != 1) + return error("Cannot do a oneway merge of %d trees", + o->merge_size); + + if (!a) + return deleted_entry(old, old, o); + if (old && same(old, a)) { + if (o->reset) { + struct stat st; + if (lstat(old->name, &st) || + ce_match_stat(old, &st, 1)) + old->ce_flags |= htons(CE_UPDATE); + } + return keep_entry(old); + } + return merged_entry(a, old, o); +} diff --git a/unpack-trees.h b/unpack-trees.h new file mode 100644 index 0000000000..191f7442f1 --- /dev/null +++ b/unpack-trees.h @@ -0,0 +1,36 @@ +#ifndef UNPACK_TREES_H +#define UNPACK_TREES_H + +struct unpack_trees_options; + +typedef int (*merge_fn_t)(struct cache_entry **src, + struct unpack_trees_options *options); + +struct unpack_trees_options { + int reset; + int merge; + int update; + int index_only; + int nontrivial_merge; + int trivial_merges_only; + int verbose_update; + int aggressive; + const char *prefix; + struct dir_struct *dir; + merge_fn_t fn; + + int head_idx; + int merge_size; + + struct cache_entry *df_conflict_entry; +}; + +extern int unpack_trees(struct object_list *trees, + struct unpack_trees_options *options); + +int threeway_merge(struct cache_entry **stages, struct unpack_trees_options *o); +int twoway_merge(struct cache_entry **src, struct unpack_trees_options *o); +int bind_merge(struct cache_entry **src, struct unpack_trees_options *o); +int oneway_merge(struct cache_entry **src, struct unpack_trees_options *o); + +#endif diff --git a/update-server-info.c b/update-server-info.c new file mode 100644 index 0000000000..0b6c3835bd --- /dev/null +++ b/update-server-info.c @@ -0,0 +1,25 @@ +#include "cache.h" + +static const char update_server_info_usage[] = +"git-update-server-info [--force]"; + +int main(int ac, char **av) +{ + int i; + int force = 0; + for (i = 1; i < ac; i++) { + if (av[i][0] == '-') { + if (!strcmp("--force", av[i]) || + !strcmp("-f", av[i])) + force = 1; + else + usage(update_server_info_usage); + } + } + if (i != ac) + usage(update_server_info_usage); + + setup_git_directory(); + + return !!update_server_info(force); +} diff --git a/upload-pack.c b/upload-pack.c new file mode 100644 index 0000000000..3648aae1a7 --- /dev/null +++ b/upload-pack.c @@ -0,0 +1,679 @@ +#include "cache.h" +#include "refs.h" +#include "pkt-line.h" +#include "sideband.h" +#include "tag.h" +#include "object.h" +#include "commit.h" +#include "exec_cmd.h" +#include "diff.h" +#include "revision.h" +#include "list-objects.h" + +static const char upload_pack_usage[] = "git-upload-pack [--strict] [--timeout=nn] <dir>"; + +/* bits #0..7 in revision.h, #8..10 in commit.c */ +#define THEY_HAVE (1u << 11) +#define OUR_REF (1u << 12) +#define WANTED (1u << 13) +#define COMMON_KNOWN (1u << 14) +#define REACHABLE (1u << 15) + +#define SHALLOW (1u << 16) +#define NOT_SHALLOW (1u << 17) +#define CLIENT_SHALLOW (1u << 18) + +static unsigned long oldest_have; + +static int multi_ack, nr_our_refs; +static int use_thin_pack, use_ofs_delta; +static struct object_array have_obj; +static struct object_array want_obj; +static unsigned int timeout; +/* 0 for no sideband, + * otherwise maximum packet size (up to 65520 bytes). + */ +static int use_sideband; + +static void reset_timeout(void) +{ + alarm(timeout); +} + +static int strip(char *line, int len) +{ + if (len && line[len-1] == '\n') + line[--len] = 0; + return len; +} + +static ssize_t send_client_data(int fd, const char *data, ssize_t sz) +{ + if (use_sideband) + return send_sideband(1, fd, data, sz, use_sideband); + if (fd == 3) + /* emergency quit */ + fd = 2; + if (fd == 2) { + /* XXX: are we happy to lose stuff here? */ + xwrite(fd, data, sz); + return sz; + } + return safe_write(fd, data, sz); +} + +FILE *pack_pipe = NULL; +static void show_commit(struct commit *commit) +{ + if (commit->object.flags & BOUNDARY) + fputc('-', pack_pipe); + if (fputs(sha1_to_hex(commit->object.sha1), pack_pipe) < 0) + die("broken output pipe"); + fputc('\n', pack_pipe); + fflush(pack_pipe); + free(commit->buffer); + commit->buffer = NULL; +} + +static void show_object(struct object_array_entry *p) +{ + /* An object with name "foo\n0000000..." can be used to + * confuse downstream git-pack-objects very badly. + */ + const char *ep = strchr(p->name, '\n'); + if (ep) { + fprintf(pack_pipe, "%s %.*s\n", sha1_to_hex(p->item->sha1), + (int) (ep - p->name), + p->name); + } + else + fprintf(pack_pipe, "%s %s\n", + sha1_to_hex(p->item->sha1), p->name); +} + +static void show_edge(struct commit *commit) +{ + fprintf(pack_pipe, "-%s\n", sha1_to_hex(commit->object.sha1)); +} + +static void create_pack_file(void) +{ + /* Pipes between rev-list to pack-objects, pack-objects to us + * and pack-objects error stream for progress bar. + */ + int lp_pipe[2], pu_pipe[2], pe_pipe[2]; + pid_t pid_rev_list, pid_pack_objects; + int create_full_pack = (nr_our_refs == want_obj.nr && !have_obj.nr); + char data[8193], progress[128]; + char abort_msg[] = "aborting due to possible repository " + "corruption on the remote side."; + int buffered = -1; + + if (pipe(lp_pipe) < 0) + die("git-upload-pack: unable to create pipe"); + pid_rev_list = fork(); + if (pid_rev_list < 0) + die("git-upload-pack: unable to fork git-rev-list"); + + if (!pid_rev_list) { + int i; + struct rev_info revs; + + pack_pipe = fdopen(lp_pipe[1], "w"); + + if (create_full_pack) + use_thin_pack = 0; /* no point doing it */ + init_revisions(&revs, NULL); + revs.tag_objects = 1; + revs.tree_objects = 1; + revs.blob_objects = 1; + if (use_thin_pack) + revs.edge_hint = 1; + + if (create_full_pack) { + const char *args[] = {"rev-list", "--all", NULL}; + setup_revisions(2, args, &revs, NULL); + } else { + for (i = 0; i < want_obj.nr; i++) { + struct object *o = want_obj.objects[i].item; + /* why??? */ + o->flags &= ~UNINTERESTING; + add_pending_object(&revs, o, NULL); + } + for (i = 0; i < have_obj.nr; i++) { + struct object *o = have_obj.objects[i].item; + o->flags |= UNINTERESTING; + add_pending_object(&revs, o, NULL); + } + setup_revisions(0, NULL, &revs, NULL); + } + prepare_revision_walk(&revs); + mark_edges_uninteresting(revs.commits, &revs, show_edge); + traverse_commit_list(&revs, show_commit, show_object); + exit(0); + } + + if (pipe(pu_pipe) < 0) + die("git-upload-pack: unable to create pipe"); + if (pipe(pe_pipe) < 0) + die("git-upload-pack: unable to create pipe"); + pid_pack_objects = fork(); + if (pid_pack_objects < 0) { + /* daemon sets things up to ignore TERM */ + kill(pid_rev_list, SIGKILL); + die("git-upload-pack: unable to fork git-pack-objects"); + } + if (!pid_pack_objects) { + dup2(lp_pipe[0], 0); + dup2(pu_pipe[1], 1); + dup2(pe_pipe[1], 2); + + close(lp_pipe[0]); + close(lp_pipe[1]); + close(pu_pipe[0]); + close(pu_pipe[1]); + close(pe_pipe[0]); + close(pe_pipe[1]); + execl_git_cmd("pack-objects", "--stdout", "--progress", + use_ofs_delta ? "--delta-base-offset" : NULL, + NULL); + kill(pid_rev_list, SIGKILL); + die("git-upload-pack: unable to exec git-pack-objects"); + } + + close(lp_pipe[0]); + close(lp_pipe[1]); + + /* We read from pe_pipe[0] to capture stderr output for + * progress bar, and pu_pipe[0] to capture the pack data. + */ + close(pe_pipe[1]); + close(pu_pipe[1]); + + while (1) { + const char *who; + struct pollfd pfd[2]; + pid_t pid; + int status; + ssize_t sz; + int pe, pu, pollsize; + + reset_timeout(); + + pollsize = 0; + pe = pu = -1; + + if (0 <= pu_pipe[0]) { + pfd[pollsize].fd = pu_pipe[0]; + pfd[pollsize].events = POLLIN; + pu = pollsize; + pollsize++; + } + if (0 <= pe_pipe[0]) { + pfd[pollsize].fd = pe_pipe[0]; + pfd[pollsize].events = POLLIN; + pe = pollsize; + pollsize++; + } + + if (pollsize) { + if (poll(pfd, pollsize, -1) < 0) { + if (errno != EINTR) { + error("poll failed, resuming: %s", + strerror(errno)); + sleep(1); + } + continue; + } + if (0 <= pu && (pfd[pu].revents & (POLLIN|POLLHUP))) { + /* Data ready; we keep the last byte + * to ourselves in case we detect + * broken rev-list, so that we can + * leave the stream corrupted. This + * is unfortunate -- unpack-objects + * would happily accept a valid pack + * data with trailing garbage, so + * appending garbage after we pass all + * the pack data is not good enough to + * signal breakage to downstream. + */ + char *cp = data; + ssize_t outsz = 0; + if (0 <= buffered) { + *cp++ = buffered; + outsz++; + } + sz = xread(pu_pipe[0], cp, + sizeof(data) - outsz); + if (0 < sz) + ; + else if (sz == 0) { + close(pu_pipe[0]); + pu_pipe[0] = -1; + } + else + goto fail; + sz += outsz; + if (1 < sz) { + buffered = data[sz-1] & 0xFF; + sz--; + } + else + buffered = -1; + sz = send_client_data(1, data, sz); + if (sz < 0) + goto fail; + } + if (0 <= pe && (pfd[pe].revents & (POLLIN|POLLHUP))) { + /* Status ready; we ship that in the side-band + * or dump to the standard error. + */ + sz = xread(pe_pipe[0], progress, + sizeof(progress)); + if (0 < sz) + send_client_data(2, progress, sz); + else if (sz == 0) { + close(pe_pipe[0]); + pe_pipe[0] = -1; + } + else + goto fail; + } + } + + /* See if the children are still there */ + if (pid_rev_list || pid_pack_objects) { + pid = waitpid(-1, &status, WNOHANG); + if (!pid) + continue; + who = ((pid == pid_rev_list) ? "git-rev-list" : + (pid == pid_pack_objects) ? "git-pack-objects" : + NULL); + if (!who) { + if (pid < 0) { + error("git-upload-pack: %s", + strerror(errno)); + goto fail; + } + error("git-upload-pack: we weren't " + "waiting for %d", pid); + continue; + } + if (!WIFEXITED(status) || WEXITSTATUS(status) > 0) { + error("git-upload-pack: %s died with error.", + who); + goto fail; + } + if (pid == pid_rev_list) + pid_rev_list = 0; + if (pid == pid_pack_objects) + pid_pack_objects = 0; + if (pid_rev_list || pid_pack_objects) + continue; + } + + /* both died happily */ + if (pollsize) + continue; + + /* flush the data */ + if (0 <= buffered) { + data[0] = buffered; + sz = send_client_data(1, data, 1); + if (sz < 0) + goto fail; + fprintf(stderr, "flushed.\n"); + } + if (use_sideband) + packet_flush(1); + return; + } + fail: + if (pid_pack_objects) + kill(pid_pack_objects, SIGKILL); + if (pid_rev_list) + kill(pid_rev_list, SIGKILL); + send_client_data(3, abort_msg, sizeof(abort_msg)); + die("git-upload-pack: %s", abort_msg); +} + +static int got_sha1(char *hex, unsigned char *sha1) +{ + struct object *o; + int we_knew_they_have = 0; + + if (get_sha1_hex(hex, sha1)) + die("git-upload-pack: expected SHA1 object, got '%s'", hex); + if (!has_sha1_file(sha1)) + return -1; + + o = lookup_object(sha1); + if (!(o && o->parsed)) + o = parse_object(sha1); + if (!o) + die("oops (%s)", sha1_to_hex(sha1)); + if (o->type == OBJ_COMMIT) { + struct commit_list *parents; + struct commit *commit = (struct commit *)o; + if (o->flags & THEY_HAVE) + we_knew_they_have = 1; + else + o->flags |= THEY_HAVE; + if (!oldest_have || (commit->date < oldest_have)) + oldest_have = commit->date; + for (parents = commit->parents; + parents; + parents = parents->next) + parents->item->object.flags |= THEY_HAVE; + } + if (!we_knew_they_have) { + add_object_array(o, NULL, &have_obj); + return 1; + } + return 0; +} + +static int reachable(struct commit *want) +{ + struct commit_list *work = NULL; + + insert_by_date(want, &work); + while (work) { + struct commit_list *list = work->next; + struct commit *commit = work->item; + free(work); + work = list; + + if (commit->object.flags & THEY_HAVE) { + want->object.flags |= COMMON_KNOWN; + break; + } + if (!commit->object.parsed) + parse_object(commit->object.sha1); + if (commit->object.flags & REACHABLE) + continue; + commit->object.flags |= REACHABLE; + if (commit->date < oldest_have) + continue; + for (list = commit->parents; list; list = list->next) { + struct commit *parent = list->item; + if (!(parent->object.flags & REACHABLE)) + insert_by_date(parent, &work); + } + } + want->object.flags |= REACHABLE; + clear_commit_marks(want, REACHABLE); + free_commit_list(work); + return (want->object.flags & COMMON_KNOWN); +} + +static int ok_to_give_up(void) +{ + int i; + + if (!have_obj.nr) + return 0; + + for (i = 0; i < want_obj.nr; i++) { + struct object *want = want_obj.objects[i].item; + + if (want->flags & COMMON_KNOWN) + continue; + want = deref_tag(want, "a want line", 0); + if (!want || want->type != OBJ_COMMIT) { + /* no way to tell if this is reachable by + * looking at the ancestry chain alone, so + * leave a note to ourselves not to worry about + * this object anymore. + */ + want_obj.objects[i].item->flags |= COMMON_KNOWN; + continue; + } + if (!reachable((struct commit *)want)) + return 0; + } + return 1; +} + +static int get_common_commits(void) +{ + static char line[1000]; + unsigned char sha1[20]; + char hex[41], last_hex[41]; + int len; + + track_object_refs = 0; + save_commit_buffer = 0; + + for(;;) { + len = packet_read_line(0, line, sizeof(line)); + reset_timeout(); + + if (!len) { + if (have_obj.nr == 0 || multi_ack) + packet_write(1, "NAK\n"); + continue; + } + len = strip(line, len); + if (!strncmp(line, "have ", 5)) { + switch (got_sha1(line+5, sha1)) { + case -1: /* they have what we do not */ + if (multi_ack && ok_to_give_up()) + packet_write(1, "ACK %s continue\n", + sha1_to_hex(sha1)); + break; + default: + memcpy(hex, sha1_to_hex(sha1), 41); + if (multi_ack) { + const char *msg = "ACK %s continue\n"; + packet_write(1, msg, hex); + memcpy(last_hex, hex, 41); + } + else if (have_obj.nr == 1) + packet_write(1, "ACK %s\n", hex); + break; + } + continue; + } + if (!strcmp(line, "done")) { + if (have_obj.nr > 0) { + if (multi_ack) + packet_write(1, "ACK %s\n", last_hex); + return 0; + } + packet_write(1, "NAK\n"); + return -1; + } + die("git-upload-pack: expected SHA1 list, got '%s'", line); + } +} + +static void receive_needs(void) +{ + struct object_array shallows = {0, 0, NULL}; + static char line[1000]; + int len, depth = 0; + + for (;;) { + struct object *o; + unsigned char sha1_buf[20]; + len = packet_read_line(0, line, sizeof(line)); + reset_timeout(); + if (!len) + break; + + if (!strncmp("shallow ", line, 8)) { + unsigned char sha1[20]; + struct object *object; + use_thin_pack = 0; + if (get_sha1(line + 8, sha1)) + die("invalid shallow line: %s", line); + object = parse_object(sha1); + if (!object) + die("did not find object for %s", line); + object->flags |= CLIENT_SHALLOW; + add_object_array(object, NULL, &shallows); + continue; + } + if (!strncmp("deepen ", line, 7)) { + char *end; + use_thin_pack = 0; + depth = strtol(line + 7, &end, 0); + if (end == line + 7 || depth <= 0) + die("Invalid deepen: %s", line); + continue; + } + if (strncmp("want ", line, 5) || + get_sha1_hex(line+5, sha1_buf)) + die("git-upload-pack: protocol error, " + "expected to get sha, not '%s'", line); + if (strstr(line+45, "multi_ack")) + multi_ack = 1; + if (strstr(line+45, "thin-pack")) + use_thin_pack = 1; + if (strstr(line+45, "ofs-delta")) + use_ofs_delta = 1; + if (strstr(line+45, "side-band-64k")) + use_sideband = LARGE_PACKET_MAX; + else if (strstr(line+45, "side-band")) + use_sideband = DEFAULT_PACKET_MAX; + + /* We have sent all our refs already, and the other end + * should have chosen out of them; otherwise they are + * asking for nonsense. + * + * Hmph. We may later want to allow "want" line that + * asks for something like "master~10" (symbolic)... + * would it make sense? I don't know. + */ + o = lookup_object(sha1_buf); + if (!o || !(o->flags & OUR_REF)) + die("git-upload-pack: not our ref %s", line+5); + if (!(o->flags & WANTED)) { + o->flags |= WANTED; + add_object_array(o, NULL, &want_obj); + } + } + if (depth == 0 && shallows.nr == 0) + return; + if (depth > 0) { + struct commit_list *result, *backup; + int i; + backup = result = get_shallow_commits(&want_obj, depth, + SHALLOW, NOT_SHALLOW); + while (result) { + struct object *object = &result->item->object; + if (!(object->flags & (CLIENT_SHALLOW|NOT_SHALLOW))) { + packet_write(1, "shallow %s", + sha1_to_hex(object->sha1)); + register_shallow(object->sha1); + } + result = result->next; + } + free_commit_list(backup); + for (i = 0; i < shallows.nr; i++) { + struct object *object = shallows.objects[i].item; + if (object->flags & NOT_SHALLOW) { + struct commit_list *parents; + packet_write(1, "unshallow %s", + sha1_to_hex(object->sha1)); + object->flags &= ~CLIENT_SHALLOW; + /* make sure the real parents are parsed */ + unregister_shallow(object->sha1); + object->parsed = 0; + parse_commit((struct commit *)object); + parents = ((struct commit *)object)->parents; + while (parents) { + add_object_array(&parents->item->object, + NULL, &want_obj); + parents = parents->next; + } + } + /* make sure commit traversal conforms to client */ + register_shallow(object->sha1); + } + packet_flush(1); + } else + if (shallows.nr > 0) { + int i; + for (i = 0; i < shallows.nr; i++) + register_shallow(shallows.objects[i].item->sha1); + } + free(shallows.objects); +} + +static int send_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + static const char *capabilities = "multi_ack thin-pack side-band" + " side-band-64k ofs-delta shallow"; + struct object *o = parse_object(sha1); + + if (!o) + die("git-upload-pack: cannot find object %s:", sha1_to_hex(sha1)); + + if (capabilities) + packet_write(1, "%s %s%c%s\n", sha1_to_hex(sha1), refname, + 0, capabilities); + else + packet_write(1, "%s %s\n", sha1_to_hex(sha1), refname); + capabilities = NULL; + if (!(o->flags & OUR_REF)) { + o->flags |= OUR_REF; + nr_our_refs++; + } + if (o->type == OBJ_TAG) { + o = deref_tag(o, refname, 0); + packet_write(1, "%s %s^{}\n", sha1_to_hex(o->sha1), refname); + } + return 0; +} + +static void upload_pack(void) +{ + reset_timeout(); + head_ref(send_ref, NULL); + for_each_ref(send_ref, NULL); + packet_flush(1); + receive_needs(); + if (want_obj.nr) { + get_common_commits(); + create_pack_file(); + } +} + +int main(int argc, char **argv) +{ + char *dir; + int i; + int strict = 0; + + for (i = 1; i < argc; i++) { + char *arg = argv[i]; + + if (arg[0] != '-') + break; + if (!strcmp(arg, "--strict")) { + strict = 1; + continue; + } + if (!strncmp(arg, "--timeout=", 10)) { + timeout = atoi(arg+10); + continue; + } + if (!strcmp(arg, "--")) { + i++; + break; + } + } + + if (i != argc-1) + usage(upload_pack_usage); + dir = argv[i]; + + if (!enter_repo(dir, strict)) + die("'%s': unable to chdir or not a git archive", dir); + if (is_repository_shallow()) + die("attempt to fetch/clone from a shallow repository"); + upload_pack(); + return 0; +} diff --git a/usage.c b/usage.c new file mode 100644 index 0000000000..4dc5c77633 --- /dev/null +++ b/usage.c @@ -0,0 +1,96 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "git-compat-util.h" + +static void report(const char *prefix, const char *err, va_list params) +{ + fputs(prefix, stderr); + vfprintf(stderr, err, params); + fputs("\n", stderr); +} + +static NORETURN void usage_builtin(const char *err) +{ + fprintf(stderr, "usage: %s\n", err); + exit(129); +} + +static NORETURN void die_builtin(const char *err, va_list params) +{ + report("fatal: ", err, params); + exit(128); +} + +static void error_builtin(const char *err, va_list params) +{ + report("error: ", err, params); +} + +static void warn_builtin(const char *warn, va_list params) +{ + report("warning: ", warn, params); +} + +/* If we are in a dlopen()ed .so write to a global variable would segfault + * (ugh), so keep things static. */ +static void (*usage_routine)(const char *err) NORETURN = usage_builtin; +static void (*die_routine)(const char *err, va_list params) NORETURN = die_builtin; +static void (*error_routine)(const char *err, va_list params) = error_builtin; +static void (*warn_routine)(const char *err, va_list params) = warn_builtin; + +void set_usage_routine(void (*routine)(const char *err) NORETURN) +{ + usage_routine = routine; +} + +void set_die_routine(void (*routine)(const char *err, va_list params) NORETURN) +{ + die_routine = routine; +} + +void set_error_routine(void (*routine)(const char *err, va_list params)) +{ + error_routine = routine; +} + +void set_warn_routine(void (*routine)(const char *warn, va_list params)) +{ + warn_routine = routine; +} + + +void usage(const char *err) +{ + usage_routine(err); +} + +void die(const char *err, ...) +{ + va_list params; + + va_start(params, err); + die_routine(err, params); + va_end(params); +} + +int error(const char *err, ...) +{ + va_list params; + + va_start(params, err); + error_routine(err, params); + va_end(params); + return -1; +} + +void warn(const char *warn, ...) +{ + va_list params; + + va_start(params, warn); + warn_routine(warn, params); + va_end(params); +} diff --git a/utf8.c b/utf8.c new file mode 100644 index 0000000000..7c80eeccb4 --- /dev/null +++ b/utf8.c @@ -0,0 +1,341 @@ +#include "git-compat-util.h" +#include "utf8.h" + +/* This code is originally from http://www.cl.cam.ac.uk/~mgk25/ucs/ */ + +struct interval { + int first; + int last; +}; + +/* auxiliary function for binary search in interval table */ +static int bisearch(wchar_t ucs, const struct interval *table, int max) { + int min = 0; + int mid; + + if (ucs < table[0].first || ucs > table[max].last) + return 0; + while (max >= min) { + mid = (min + max) / 2; + if (ucs > table[mid].last) + min = mid + 1; + else if (ucs < table[mid].first) + max = mid - 1; + else + return 1; + } + + return 0; +} + +/* The following two functions define the column width of an ISO 10646 + * character as follows: + * + * - The null character (U+0000) has a column width of 0. + * + * - Other C0/C1 control characters and DEL will lead to a return + * value of -1. + * + * - Non-spacing and enclosing combining characters (general + * category code Mn or Me in the Unicode database) have a + * column width of 0. + * + * - SOFT HYPHEN (U+00AD) has a column width of 1. + * + * - Other format characters (general category code Cf in the Unicode + * database) and ZERO WIDTH SPACE (U+200B) have a column width of 0. + * + * - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF) + * have a column width of 0. + * + * - Spacing characters in the East Asian Wide (W) or East Asian + * Full-width (F) category as defined in Unicode Technical + * Report #11 have a column width of 2. + * + * - All remaining characters (including all printable + * ISO 8859-1 and WGL4 characters, Unicode control characters, + * etc.) have a column width of 1. + * + * This implementation assumes that wchar_t characters are encoded + * in ISO 10646. + */ + +static int wcwidth(wchar_t ch) +{ + /* + * Sorted list of non-overlapping intervals of non-spacing characters, + * generated by + * "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c". + */ + static const struct interval combining[] = { + { 0x0300, 0x0357 }, { 0x035D, 0x036F }, { 0x0483, 0x0486 }, + { 0x0488, 0x0489 }, { 0x0591, 0x05A1 }, { 0x05A3, 0x05B9 }, + { 0x05BB, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 }, + { 0x05C4, 0x05C4 }, { 0x0600, 0x0603 }, { 0x0610, 0x0615 }, + { 0x064B, 0x0658 }, { 0x0670, 0x0670 }, { 0x06D6, 0x06E4 }, + { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED }, { 0x070F, 0x070F }, + { 0x0711, 0x0711 }, { 0x0730, 0x074A }, { 0x07A6, 0x07B0 }, + { 0x0901, 0x0902 }, { 0x093C, 0x093C }, { 0x0941, 0x0948 }, + { 0x094D, 0x094D }, { 0x0951, 0x0954 }, { 0x0962, 0x0963 }, + { 0x0981, 0x0981 }, { 0x09BC, 0x09BC }, { 0x09C1, 0x09C4 }, + { 0x09CD, 0x09CD }, { 0x09E2, 0x09E3 }, { 0x0A01, 0x0A02 }, + { 0x0A3C, 0x0A3C }, { 0x0A41, 0x0A42 }, { 0x0A47, 0x0A48 }, + { 0x0A4B, 0x0A4D }, { 0x0A70, 0x0A71 }, { 0x0A81, 0x0A82 }, + { 0x0ABC, 0x0ABC }, { 0x0AC1, 0x0AC5 }, { 0x0AC7, 0x0AC8 }, + { 0x0ACD, 0x0ACD }, { 0x0AE2, 0x0AE3 }, { 0x0B01, 0x0B01 }, + { 0x0B3C, 0x0B3C }, { 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 }, + { 0x0B4D, 0x0B4D }, { 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 }, + { 0x0BC0, 0x0BC0 }, { 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 }, + { 0x0C46, 0x0C48 }, { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 }, + { 0x0CBC, 0x0CBC }, { 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 }, + { 0x0CCC, 0x0CCD }, { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D }, + { 0x0DCA, 0x0DCA }, { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 }, + { 0x0E31, 0x0E31 }, { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E }, + { 0x0EB1, 0x0EB1 }, { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC }, + { 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 }, + { 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E }, + { 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 }, + { 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 }, + { 0x1032, 0x1032 }, { 0x1036, 0x1037 }, { 0x1039, 0x1039 }, + { 0x1058, 0x1059 }, { 0x1160, 0x11FF }, { 0x1712, 0x1714 }, + { 0x1732, 0x1734 }, { 0x1752, 0x1753 }, { 0x1772, 0x1773 }, + { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD }, { 0x17C6, 0x17C6 }, + { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD }, { 0x180B, 0x180D }, + { 0x18A9, 0x18A9 }, { 0x1920, 0x1922 }, { 0x1927, 0x1928 }, + { 0x1932, 0x1932 }, { 0x1939, 0x193B }, { 0x200B, 0x200F }, + { 0x202A, 0x202E }, { 0x2060, 0x2063 }, { 0x206A, 0x206F }, + { 0x20D0, 0x20EA }, { 0x302A, 0x302F }, { 0x3099, 0x309A }, + { 0xFB1E, 0xFB1E }, { 0xFE00, 0xFE0F }, { 0xFE20, 0xFE23 }, + { 0xFEFF, 0xFEFF }, { 0xFFF9, 0xFFFB }, { 0x1D167, 0x1D169 }, + { 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B }, + { 0x1D1AA, 0x1D1AD }, { 0xE0001, 0xE0001 }, + { 0xE0020, 0xE007F }, { 0xE0100, 0xE01EF } + }; + + /* test for 8-bit control characters */ + if (ch == 0) + return 0; + if (ch < 32 || (ch >= 0x7f && ch < 0xa0)) + return -1; + + /* binary search in table of non-spacing characters */ + if (bisearch(ch, combining, sizeof(combining) + / sizeof(struct interval) - 1)) + return 0; + + /* + * If we arrive here, ch is neither a combining nor a C0/C1 + * control character. + */ + + return 1 + + (ch >= 0x1100 && + /* Hangul Jamo init. consonants */ + (ch <= 0x115f || + ch == 0x2329 || ch == 0x232a || + /* CJK ... Yi */ + (ch >= 0x2e80 && ch <= 0xa4cf && + ch != 0x303f) || + /* Hangul Syllables */ + (ch >= 0xac00 && ch <= 0xd7a3) || + /* CJK Compatibility Ideographs */ + (ch >= 0xf900 && ch <= 0xfaff) || + /* CJK Compatibility Forms */ + (ch >= 0xfe30 && ch <= 0xfe6f) || + /* Fullwidth Forms */ + (ch >= 0xff00 && ch <= 0xff60) || + (ch >= 0xffe0 && ch <= 0xffe6) || + (ch >= 0x20000 && ch <= 0x2fffd) || + (ch >= 0x30000 && ch <= 0x3fffd))); +} + +/* + * This function returns the number of columns occupied by the character + * pointed to by the variable start. The pointer is updated to point at + * the next character. If it was not valid UTF-8, the pointer is set to NULL. + */ +int utf8_width(const char **start) +{ + unsigned char *s = (unsigned char *)*start; + wchar_t ch; + + if (*s < 0x80) { + /* 0xxxxxxx */ + ch = *s; + *start += 1; + } else if ((s[0] & 0xe0) == 0xc0) { + /* 110XXXXx 10xxxxxx */ + if ((s[1] & 0xc0) != 0x80 || + /* overlong? */ + (s[0] & 0xfe) == 0xc0) + goto invalid; + ch = ((s[0] & 0x1f) << 6) | (s[1] & 0x3f); + *start += 2; + } else if ((s[0] & 0xf0) == 0xe0) { + /* 1110XXXX 10Xxxxxx 10xxxxxx */ + if ((s[1] & 0xc0) != 0x80 || + (s[2] & 0xc0) != 0x80 || + /* overlong? */ + (s[0] == 0xe0 && (s[1] & 0xe0) == 0x80) || + /* surrogate? */ + (s[0] == 0xed && (s[1] & 0xe0) == 0xa0) || + /* U+FFFE or U+FFFF? */ + (s[0] == 0xef && s[1] == 0xbf && + (s[2] & 0xfe) == 0xbe)) + goto invalid; + ch = ((s[0] & 0x0f) << 12) | + ((s[1] & 0x3f) << 6) | (s[2] & 0x3f); + *start += 3; + } else if ((s[0] & 0xf8) == 0xf0) { + /* 11110XXX 10XXxxxx 10xxxxxx 10xxxxxx */ + if ((s[1] & 0xc0) != 0x80 || + (s[2] & 0xc0) != 0x80 || + (s[3] & 0xc0) != 0x80 || + /* overlong? */ + (s[0] == 0xf0 && (s[1] & 0xf0) == 0x80) || + /* > U+10FFFF? */ + (s[0] == 0xf4 && s[1] > 0x8f) || s[0] > 0xf4) + goto invalid; + ch = ((s[0] & 0x07) << 18) | ((s[1] & 0x3f) << 12) | + ((s[2] & 0x3f) << 6) | (s[3] & 0x3f); + *start += 4; + } else { +invalid: + *start = NULL; + return 0; + } + + return wcwidth(ch); +} + +int is_utf8(const char *text) +{ + while (*text) { + if (*text == '\n' || *text == '\t' || *text == '\r') { + text++; + continue; + } + utf8_width(&text); + if (!text) + return 0; + } + return 1; +} + +static void print_spaces(int count) +{ + static const char s[] = " "; + while (count >= sizeof(s)) { + fwrite(s, sizeof(s) - 1, 1, stdout); + count -= sizeof(s) - 1; + } + fwrite(s, count, 1, stdout); +} + +/* + * Wrap the text, if necessary. The variable indent is the indent for the + * first line, indent2 is the indent for all other lines. + */ +void print_wrapped_text(const char *text, int indent, int indent2, int width) +{ + int w = indent, assume_utf8 = is_utf8(text); + const char *bol = text, *space = NULL; + + for (;;) { + char c = *text; + if (!c || isspace(c)) { + if (w < width || !space) { + const char *start = bol; + if (space) + start = space; + else + print_spaces(indent); + fwrite(start, text - start, 1, stdout); + if (!c) { + putchar('\n'); + return; + } else if (c == '\t') + w |= 0x07; + space = text; + w++; + text++; + } + else { + putchar('\n'); + text = bol = space + 1; + space = NULL; + w = indent = indent2; + } + continue; + } + if (assume_utf8) + w += utf8_width(&text); + else { + w++; + text++; + } + } +} + +int is_encoding_utf8(const char *name) +{ + if (!name) + return 1; + if (!strcasecmp(name, "utf-8") || !strcasecmp(name, "utf8")) + return 1; + return 0; +} + +/* + * Given a buffer and its encoding, return it re-encoded + * with iconv. If the conversion fails, returns NULL. + */ +#ifndef NO_ICONV +char *reencode_string(const char *in, const char *out_encoding, const char *in_encoding) +{ + iconv_t conv; + size_t insz, outsz, outalloc; + char *out, *outpos, *cp; + + if (!in_encoding) + return NULL; + conv = iconv_open(out_encoding, in_encoding); + if (conv == (iconv_t) -1) + return NULL; + insz = strlen(in); + outsz = insz; + outalloc = outsz + 1; /* for terminating NUL */ + out = xmalloc(outalloc); + outpos = out; + cp = (char *)in; + + while (1) { + size_t cnt = iconv(conv, &cp, &insz, &outpos, &outsz); + + if (cnt == -1) { + size_t sofar; + if (errno != E2BIG) { + free(out); + iconv_close(conv); + return NULL; + } + /* insz has remaining number of bytes. + * since we started outsz the same as insz, + * it is likely that insz is not enough for + * converting the rest. + */ + sofar = outpos - out; + outalloc = sofar + insz * 2 + 32; + out = xrealloc(out, outalloc); + outpos = out + sofar; + outsz = outalloc - sofar - 1; + } + else { + *outpos = '\0'; + break; + } + } + iconv_close(conv); + return out; +} +#endif diff --git a/utf8.h b/utf8.h new file mode 100644 index 0000000000..a07c5a88af --- /dev/null +++ b/utf8.h @@ -0,0 +1,16 @@ +#ifndef GIT_UTF8_H +#define GIT_UTF8_H + +int utf8_width(const char **start); +int is_utf8(const char *text); +int is_encoding_utf8(const char *name); + +void print_wrapped_text(const char *text, int indent, int indent2, int len); + +#ifndef NO_ICONV +char *reencode_string(const char *in, const char *out_encoding, const char *in_encoding); +#else +#define reencode_string(a,b,c) NULL +#endif + +#endif @@ -0,0 +1,74 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Eric Biederman, 2005 + */ +#include "cache.h" + +static const char var_usage[] = "git-var [-l | <variable>]"; + +struct git_var { + const char *name; + const char *(*read)(int); +}; +static struct git_var git_vars[] = { + { "GIT_COMMITTER_IDENT", git_committer_info }, + { "GIT_AUTHOR_IDENT", git_author_info }, + { "", NULL }, +}; + +static void list_vars(void) +{ + struct git_var *ptr; + for(ptr = git_vars; ptr->read; ptr++) { + printf("%s=%s\n", ptr->name, ptr->read(0)); + } +} + +static const char *read_var(const char *var) +{ + struct git_var *ptr; + const char *val; + val = NULL; + for(ptr = git_vars; ptr->read; ptr++) { + if (strcmp(var, ptr->name) == 0) { + val = ptr->read(1); + break; + } + } + return val; +} + +static int show_config(const char *var, const char *value) +{ + if (value) + printf("%s=%s\n", var, value); + else + printf("%s\n", var); + return git_default_config(var, value); +} + +int main(int argc, char **argv) +{ + const char *val; + if (argc != 2) { + usage(var_usage); + } + + setup_git_directory(); + val = NULL; + + if (strcmp(argv[1], "-l") == 0) { + git_config(show_config); + list_vars(); + return 0; + } + git_config(git_default_config); + val = read_var(argv[1]); + if (!val) + usage(var_usage); + + printf("%s\n", val); + + return 0; +} diff --git a/write_or_die.c b/write_or_die.c new file mode 100644 index 0000000000..5c4bc8515a --- /dev/null +++ b/write_or_die.c @@ -0,0 +1,72 @@ +#include "cache.h" + +int read_in_full(int fd, void *buf, size_t count) +{ + char *p = buf; + ssize_t total = 0; + + while (count > 0) { + ssize_t loaded = xread(fd, p, count); + if (loaded <= 0) + return total ? total : loaded; + count -= loaded; + p += loaded; + total += loaded; + } + + return total; +} + +int write_in_full(int fd, const void *buf, size_t count) +{ + const char *p = buf; + ssize_t total = 0; + + while (count > 0) { + ssize_t written = xwrite(fd, p, count); + if (written < 0) + return -1; + if (!written) { + errno = ENOSPC; + return -1; + } + count -= written; + p += written; + total += written; + } + + return total; +} + +void write_or_die(int fd, const void *buf, size_t count) +{ + if (write_in_full(fd, buf, count) < 0) { + if (errno == EPIPE) + exit(0); + die("write error (%s)", strerror(errno)); + } +} + +int write_or_whine_pipe(int fd, const void *buf, size_t count, const char *msg) +{ + if (write_in_full(fd, buf, count) < 0) { + if (errno == EPIPE) + exit(0); + fprintf(stderr, "%s: write error (%s)\n", + msg, strerror(errno)); + return 0; + } + + return 1; +} + +int write_or_whine(int fd, const void *buf, size_t count, const char *msg) +{ + if (write_in_full(fd, buf, count) < 0) { + fprintf(stderr, "%s: write error (%s)\n", + msg, strerror(errno)); + return 0; + } + + return 1; +} diff --git a/wt-status.c b/wt-status.c new file mode 100644 index 0000000000..2879c3d5ec --- /dev/null +++ b/wt-status.c @@ -0,0 +1,352 @@ +#include "cache.h" +#include "wt-status.h" +#include "color.h" +#include "object.h" +#include "dir.h" +#include "commit.h" +#include "diff.h" +#include "revision.h" +#include "diffcore.h" + +int wt_status_use_color = 0; +static char wt_status_colors[][COLOR_MAXLEN] = { + "", /* WT_STATUS_HEADER: normal */ + "\033[32m", /* WT_STATUS_UPDATED: green */ + "\033[31m", /* WT_STATUS_CHANGED: red */ + "\033[31m", /* WT_STATUS_UNTRACKED: red */ +}; + +static const char use_add_msg[] = +"use \"git add <file>...\" to update what will be committed"; +static const char use_add_rm_msg[] = +"use \"git add/rm <file>...\" to update what will be committed"; +static const char use_add_to_include_msg[] = +"use \"git add <file>...\" to include in what will be committed"; + +static int parse_status_slot(const char *var, int offset) +{ + if (!strcasecmp(var+offset, "header")) + return WT_STATUS_HEADER; + if (!strcasecmp(var+offset, "updated") + || !strcasecmp(var+offset, "added")) + return WT_STATUS_UPDATED; + if (!strcasecmp(var+offset, "changed")) + return WT_STATUS_CHANGED; + if (!strcasecmp(var+offset, "untracked")) + return WT_STATUS_UNTRACKED; + die("bad config variable '%s'", var); +} + +static const char* color(int slot) +{ + return wt_status_use_color ? wt_status_colors[slot] : ""; +} + +void wt_status_prepare(struct wt_status *s) +{ + unsigned char sha1[20]; + const char *head; + + memset(s, 0, sizeof(*s)); + head = resolve_ref("HEAD", sha1, 0, NULL); + s->branch = head ? xstrdup(head) : NULL; + s->reference = "HEAD"; +} + +static void wt_status_print_cached_header(const char *reference) +{ + const char *c = color(WT_STATUS_HEADER); + color_printf_ln(c, "# Changes to be committed:"); + if (reference) { + color_printf_ln(c, "# (use \"git reset %s <file>...\" to unstage)", reference); + } else { + color_printf_ln(c, "# (use \"git rm --cached <file>...\" to unstage)"); + } + color_printf_ln(c, "#"); +} + +static void wt_status_print_header(const char *main, const char *sub) +{ + const char *c = color(WT_STATUS_HEADER); + color_printf_ln(c, "# %s:", main); + color_printf_ln(c, "# (%s)", sub); + color_printf_ln(c, "#"); +} + +static void wt_status_print_trailer(void) +{ + color_printf_ln(color(WT_STATUS_HEADER), "#"); +} + +static const char *quote_crlf(const char *in, char *buf, size_t sz) +{ + const char *scan; + char *out; + const char *ret = in; + + for (scan = in, out = buf; *scan; scan++) { + int ch = *scan; + int quoted; + + switch (ch) { + case '\n': + quoted = 'n'; + break; + case '\r': + quoted = 'r'; + break; + default: + *out++ = ch; + continue; + } + *out++ = '\\'; + *out++ = quoted; + ret = buf; + } + *out = '\0'; + return ret; +} + +static void wt_status_print_filepair(int t, struct diff_filepair *p) +{ + const char *c = color(t); + const char *one, *two; + char onebuf[PATH_MAX], twobuf[PATH_MAX]; + + one = quote_crlf(p->one->path, onebuf, sizeof(onebuf)); + two = quote_crlf(p->two->path, twobuf, sizeof(twobuf)); + + color_printf(color(WT_STATUS_HEADER), "#\t"); + switch (p->status) { + case DIFF_STATUS_ADDED: + color_printf(c, "new file: %s", one); + break; + case DIFF_STATUS_COPIED: + color_printf(c, "copied: %s -> %s", one, two); + break; + case DIFF_STATUS_DELETED: + color_printf(c, "deleted: %s", one); + break; + case DIFF_STATUS_MODIFIED: + color_printf(c, "modified: %s", one); + break; + case DIFF_STATUS_RENAMED: + color_printf(c, "renamed: %s -> %s", one, two); + break; + case DIFF_STATUS_TYPE_CHANGED: + color_printf(c, "typechange: %s", one); + break; + case DIFF_STATUS_UNKNOWN: + color_printf(c, "unknown: %s", one); + break; + case DIFF_STATUS_UNMERGED: + color_printf(c, "unmerged: %s", one); + break; + default: + die("bug: unhandled diff status %c", p->status); + } + printf("\n"); +} + +static void wt_status_print_updated_cb(struct diff_queue_struct *q, + struct diff_options *options, + void *data) +{ + struct wt_status *s = data; + int shown_header = 0; + int i; + for (i = 0; i < q->nr; i++) { + if (q->queue[i]->status == 'U') + continue; + if (!shown_header) { + wt_status_print_cached_header(s->reference); + s->commitable = 1; + shown_header = 1; + } + wt_status_print_filepair(WT_STATUS_UPDATED, q->queue[i]); + } + if (shown_header) + wt_status_print_trailer(); +} + +static void wt_status_print_changed_cb(struct diff_queue_struct *q, + struct diff_options *options, + void *data) +{ + struct wt_status *s = data; + int i; + if (q->nr) { + const char *msg = use_add_msg; + s->workdir_dirty = 1; + for (i = 0; i < q->nr; i++) + if (q->queue[i]->status == DIFF_STATUS_DELETED) { + msg = use_add_rm_msg; + break; + } + wt_status_print_header("Changed but not updated", msg); + } + for (i = 0; i < q->nr; i++) + wt_status_print_filepair(WT_STATUS_CHANGED, q->queue[i]); + if (q->nr) + wt_status_print_trailer(); +} + +void wt_status_print_initial(struct wt_status *s) +{ + int i; + char buf[PATH_MAX]; + + read_cache(); + if (active_nr) { + s->commitable = 1; + wt_status_print_cached_header(NULL); + } + for (i = 0; i < active_nr; i++) { + color_printf(color(WT_STATUS_HEADER), "#\t"); + color_printf_ln(color(WT_STATUS_UPDATED), "new file: %s", + quote_crlf(active_cache[i]->name, + buf, sizeof(buf))); + } + if (active_nr) + wt_status_print_trailer(); +} + +static void wt_status_print_updated(struct wt_status *s) +{ + struct rev_info rev; + init_revisions(&rev, NULL); + setup_revisions(0, NULL, &rev, s->reference); + rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = wt_status_print_updated_cb; + rev.diffopt.format_callback_data = s; + rev.diffopt.detect_rename = 1; + run_diff_index(&rev, 1); +} + +static void wt_status_print_changed(struct wt_status *s) +{ + struct rev_info rev; + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = wt_status_print_changed_cb; + rev.diffopt.format_callback_data = s; + run_diff_files(&rev, 0); +} + +static void wt_status_print_untracked(struct wt_status *s) +{ + struct dir_struct dir; + const char *x; + int i; + int shown_header = 0; + + memset(&dir, 0, sizeof(dir)); + + dir.exclude_per_dir = ".gitignore"; + if (!s->untracked) { + dir.show_other_directories = 1; + dir.hide_empty_directories = 1; + } + x = git_path("info/exclude"); + if (file_exists(x)) + add_excludes_from_file(&dir, x); + + read_directory(&dir, ".", "", 0); + for(i = 0; i < dir.nr; i++) { + /* check for matching entry, which is unmerged; lifted from + * builtin-ls-files:show_other_files */ + struct dir_entry *ent = dir.entries[i]; + int pos = cache_name_pos(ent->name, ent->len); + struct cache_entry *ce; + if (0 <= pos) + die("bug in wt_status_print_untracked"); + pos = -pos - 1; + if (pos < active_nr) { + ce = active_cache[pos]; + if (ce_namelen(ce) == ent->len && + !memcmp(ce->name, ent->name, ent->len)) + continue; + } + if (!shown_header) { + s->workdir_untracked = 1; + wt_status_print_header("Untracked files", + use_add_to_include_msg); + shown_header = 1; + } + color_printf(color(WT_STATUS_HEADER), "#\t"); + color_printf_ln(color(WT_STATUS_UNTRACKED), "%.*s", + ent->len, ent->name); + } +} + +static void wt_status_print_verbose(struct wt_status *s) +{ + struct rev_info rev; + init_revisions(&rev, NULL); + setup_revisions(0, NULL, &rev, s->reference); + rev.diffopt.output_format |= DIFF_FORMAT_PATCH; + rev.diffopt.detect_rename = 1; + run_diff_index(&rev, 1); +} + +void wt_status_print(struct wt_status *s) +{ + unsigned char sha1[20]; + s->is_initial = get_sha1(s->reference, sha1) ? 1 : 0; + + if (s->branch) { + const char *on_what = "On branch "; + const char *branch_name = s->branch; + if (!strncmp(branch_name, "refs/heads/", 11)) + branch_name += 11; + else if (!strcmp(branch_name, "HEAD")) { + branch_name = ""; + on_what = "Not currently on any branch."; + } + color_printf_ln(color(WT_STATUS_HEADER), + "# %s%s", on_what, branch_name); + } + + if (s->is_initial) { + color_printf_ln(color(WT_STATUS_HEADER), "#"); + color_printf_ln(color(WT_STATUS_HEADER), "# Initial commit"); + color_printf_ln(color(WT_STATUS_HEADER), "#"); + wt_status_print_initial(s); + } + else { + wt_status_print_updated(s); + discard_cache(); + } + + wt_status_print_changed(s); + wt_status_print_untracked(s); + + if (s->verbose && !s->is_initial) + wt_status_print_verbose(s); + if (!s->commitable) { + if (s->amend) + printf("# No changes\n"); + else if (s->workdir_dirty) + printf("no changes added to commit (use \"git add\" and/or \"git commit -a\")\n"); + else if (s->workdir_untracked) + printf("nothing added to commit but untracked files present (use \"git add\" to track)\n"); + else if (s->is_initial) + printf("nothing to commit (create/copy files and use \"git add\" to track)\n"); + else + printf("nothing to commit (working directory clean)\n"); + } +} + +int git_status_config(const char *k, const char *v) +{ + if (!strcmp(k, "status.color") || !strcmp(k, "color.status")) { + wt_status_use_color = git_config_colorbool(k, v); + return 0; + } + if (!strncmp(k, "status.color.", 13) || !strncmp(k, "color.status.", 13)) { + int slot = parse_status_slot(k, 13); + color_parse(v, k, wt_status_colors[slot]); + } + return git_default_config(k, v); +} diff --git a/wt-status.h b/wt-status.h new file mode 100644 index 0000000000..cfea4ae688 --- /dev/null +++ b/wt-status.h @@ -0,0 +1,28 @@ +#ifndef STATUS_H +#define STATUS_H + +enum color_wt_status { + WT_STATUS_HEADER, + WT_STATUS_UPDATED, + WT_STATUS_CHANGED, + WT_STATUS_UNTRACKED, +}; + +struct wt_status { + int is_initial; + char *branch; + const char *reference; + int verbose; + int amend; + int untracked; + /* These are computed during processing of the individual sections */ + int commitable; + int workdir_dirty; + int workdir_untracked; +}; + +int git_status_config(const char *var, const char *value); +void wt_status_prepare(struct wt_status *s); +void wt_status_print(struct wt_status *s); + +#endif /* STATUS_H */ diff --git a/xdiff-interface.c b/xdiff-interface.c new file mode 100644 index 0000000000..6c1f99b149 --- /dev/null +++ b/xdiff-interface.c @@ -0,0 +1,123 @@ +#include "cache.h" +#include "xdiff-interface.h" + +static int parse_num(char **cp_p, int *num_p) +{ + char *cp = *cp_p; + int num = 0; + int read_some; + + while ('0' <= *cp && *cp <= '9') + num = num * 10 + *cp++ - '0'; + if (!(read_some = cp - *cp_p)) + return -1; + *cp_p = cp; + *num_p = num; + return 0; +} + +int parse_hunk_header(char *line, int len, + int *ob, int *on, + int *nb, int *nn) +{ + char *cp; + cp = line + 4; + if (parse_num(&cp, ob)) { + bad_line: + return error("malformed diff output: %s", line); + } + if (*cp == ',') { + cp++; + if (parse_num(&cp, on)) + goto bad_line; + } + else + *on = 1; + if (*cp++ != ' ' || *cp++ != '+') + goto bad_line; + if (parse_num(&cp, nb)) + goto bad_line; + if (*cp == ',') { + cp++; + if (parse_num(&cp, nn)) + goto bad_line; + } + else + *nn = 1; + return -!!memcmp(cp, " @@", 3); +} + +static void consume_one(void *priv_, char *s, unsigned long size) +{ + struct xdiff_emit_state *priv = priv_; + char *ep; + while (size) { + unsigned long this_size; + ep = memchr(s, '\n', size); + this_size = (ep == NULL) ? size : (ep - s + 1); + priv->consume(priv, s, this_size); + size -= this_size; + s += this_size; + } +} + +int xdiff_outf(void *priv_, mmbuffer_t *mb, int nbuf) +{ + struct xdiff_emit_state *priv = priv_; + int i; + + for (i = 0; i < nbuf; i++) { + if (mb[i].ptr[mb[i].size-1] != '\n') { + /* Incomplete line */ + priv->remainder = xrealloc(priv->remainder, + priv->remainder_size + + mb[i].size); + memcpy(priv->remainder + priv->remainder_size, + mb[i].ptr, mb[i].size); + priv->remainder_size += mb[i].size; + continue; + } + + /* we have a complete line */ + if (!priv->remainder) { + consume_one(priv, mb[i].ptr, mb[i].size); + continue; + } + priv->remainder = xrealloc(priv->remainder, + priv->remainder_size + + mb[i].size); + memcpy(priv->remainder + priv->remainder_size, + mb[i].ptr, mb[i].size); + consume_one(priv, priv->remainder, + priv->remainder_size + mb[i].size); + free(priv->remainder); + priv->remainder = NULL; + priv->remainder_size = 0; + } + if (priv->remainder) { + consume_one(priv, priv->remainder, priv->remainder_size); + free(priv->remainder); + priv->remainder = NULL; + priv->remainder_size = 0; + } + return 0; +} + +int read_mmfile(mmfile_t *ptr, const char *filename) +{ + struct stat st; + FILE *f; + + if (stat(filename, &st)) + return error("Could not stat %s", filename); + if ((f = fopen(filename, "rb")) == NULL) + return error("Could not open %s", filename); + ptr->ptr = xmalloc(st.st_size); + if (fread(ptr->ptr, st.st_size, 1, f) != 1) + return error("Could not read %s", filename); + fclose(f); + ptr->size = st.st_size; + return 0; +} + + diff --git a/xdiff-interface.h b/xdiff-interface.h new file mode 100644 index 0000000000..1918808081 --- /dev/null +++ b/xdiff-interface.h @@ -0,0 +1,22 @@ +#ifndef XDIFF_INTERFACE_H +#define XDIFF_INTERFACE_H + +#include "xdiff/xdiff.h" + +struct xdiff_emit_state; + +typedef void (*xdiff_emit_consume_fn)(void *, char *, unsigned long); + +struct xdiff_emit_state { + xdiff_emit_consume_fn consume; + char *remainder; + unsigned long remainder_size; +}; + +int xdiff_outf(void *priv_, mmbuffer_t *mb, int nbuf); +int parse_hunk_header(char *line, int len, + int *ob, int *on, + int *nb, int *nn); +int read_mmfile(mmfile_t *ptr, const char *filename); + +#endif diff --git a/xdiff/xdiff.h b/xdiff/xdiff.h new file mode 100644 index 0000000000..fa409d5234 --- /dev/null +++ b/xdiff/xdiff.h @@ -0,0 +1,105 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#if !defined(XDIFF_H) +#define XDIFF_H + +#ifdef __cplusplus +extern "C" { +#endif /* #ifdef __cplusplus */ + + +#define XDF_NEED_MINIMAL (1 << 1) +#define XDF_IGNORE_WHITESPACE (1 << 2) +#define XDF_IGNORE_WHITESPACE_CHANGE (1 << 3) +#define XDF_WHITESPACE_FLAGS (XDF_IGNORE_WHITESPACE | XDF_IGNORE_WHITESPACE_CHANGE) + +#define XDL_PATCH_NORMAL '-' +#define XDL_PATCH_REVERSE '+' +#define XDL_PATCH_MODEMASK ((1 << 8) - 1) +#define XDL_PATCH_IGNOREBSPACE (1 << 8) + +#define XDL_EMIT_FUNCNAMES (1 << 0) +#define XDL_EMIT_COMMON (1 << 1) + +#define XDL_MMB_READONLY (1 << 0) + +#define XDL_MMF_ATOMIC (1 << 0) + +#define XDL_BDOP_INS 1 +#define XDL_BDOP_CPY 2 +#define XDL_BDOP_INSB 3 + +#define XDL_MERGE_MINIMAL 0 +#define XDL_MERGE_EAGER 1 +#define XDL_MERGE_ZEALOUS 2 + +typedef struct s_mmfile { + char *ptr; + long size; +} mmfile_t; + +typedef struct s_mmbuffer { + char *ptr; + long size; +} mmbuffer_t; + +typedef struct s_xpparam { + unsigned long flags; +} xpparam_t; + +typedef struct s_xdemitcb { + void *priv; + int (*outf)(void *, mmbuffer_t *, int); +} xdemitcb_t; + +typedef struct s_xdemitconf { + long ctxlen; + unsigned long flags; +} xdemitconf_t; + +typedef struct s_bdiffparam { + long bsize; +} bdiffparam_t; + + +#define xdl_malloc(x) malloc(x) +#define xdl_free(ptr) free(ptr) +#define xdl_realloc(ptr,x) realloc(ptr,x) + +void *xdl_mmfile_first(mmfile_t *mmf, long *size); +void *xdl_mmfile_next(mmfile_t *mmf, long *size); +long xdl_mmfile_size(mmfile_t *mmf); + +int xdl_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp, + xdemitconf_t const *xecfg, xdemitcb_t *ecb); + +int xdl_merge(mmfile_t *orig, mmfile_t *mf1, const char *name1, + mmfile_t *mf2, const char *name2, + xpparam_t const *xpp, int level, mmbuffer_t *result); + +#ifdef __cplusplus +} +#endif /* #ifdef __cplusplus */ + +#endif /* #if !defined(XDIFF_H) */ + diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c new file mode 100644 index 0000000000..9aeebc473b --- /dev/null +++ b/xdiff/xdiffi.c @@ -0,0 +1,568 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#include "xinclude.h" + + + +#define XDL_MAX_COST_MIN 256 +#define XDL_HEUR_MIN_COST 256 +#define XDL_LINE_MAX (long)((1UL << (8 * sizeof(long) - 1)) - 1) +#define XDL_SNAKE_CNT 20 +#define XDL_K_HEUR 4 + + + +typedef struct s_xdpsplit { + long i1, i2; + int min_lo, min_hi; +} xdpsplit_t; + + + + +static long xdl_split(unsigned long const *ha1, long off1, long lim1, + unsigned long const *ha2, long off2, long lim2, + long *kvdf, long *kvdb, int need_min, xdpsplit_t *spl, + xdalgoenv_t *xenv); +static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1, long chg2); + + + + + +/* + * See "An O(ND) Difference Algorithm and its Variations", by Eugene Myers. + * Basically considers a "box" (off1, off2, lim1, lim2) and scan from both + * the forward diagonal starting from (off1, off2) and the backward diagonal + * starting from (lim1, lim2). If the K values on the same diagonal crosses + * returns the furthest point of reach. We might end up having to expensive + * cases using this algorithm is full, so a little bit of heuristic is needed + * to cut the search and to return a suboptimal point. + */ +static long xdl_split(unsigned long const *ha1, long off1, long lim1, + unsigned long const *ha2, long off2, long lim2, + long *kvdf, long *kvdb, int need_min, xdpsplit_t *spl, + xdalgoenv_t *xenv) { + long dmin = off1 - lim2, dmax = lim1 - off2; + long fmid = off1 - off2, bmid = lim1 - lim2; + long odd = (fmid - bmid) & 1; + long fmin = fmid, fmax = fmid; + long bmin = bmid, bmax = bmid; + long ec, d, i1, i2, prev1, best, dd, v, k; + + /* + * Set initial diagonal values for both forward and backward path. + */ + kvdf[fmid] = off1; + kvdb[bmid] = lim1; + + for (ec = 1;; ec++) { + int got_snake = 0; + + /* + * We need to extent the diagonal "domain" by one. If the next + * values exits the box boundaries we need to change it in the + * opposite direction because (max - min) must be a power of two. + * Also we initialize the external K value to -1 so that we can + * avoid extra conditions check inside the core loop. + */ + if (fmin > dmin) + kvdf[--fmin - 1] = -1; + else + ++fmin; + if (fmax < dmax) + kvdf[++fmax + 1] = -1; + else + --fmax; + + for (d = fmax; d >= fmin; d -= 2) { + if (kvdf[d - 1] >= kvdf[d + 1]) + i1 = kvdf[d - 1] + 1; + else + i1 = kvdf[d + 1]; + prev1 = i1; + i2 = i1 - d; + for (; i1 < lim1 && i2 < lim2 && ha1[i1] == ha2[i2]; i1++, i2++); + if (i1 - prev1 > xenv->snake_cnt) + got_snake = 1; + kvdf[d] = i1; + if (odd && bmin <= d && d <= bmax && kvdb[d] <= i1) { + spl->i1 = i1; + spl->i2 = i2; + spl->min_lo = spl->min_hi = 1; + return ec; + } + } + + /* + * We need to extent the diagonal "domain" by one. If the next + * values exits the box boundaries we need to change it in the + * opposite direction because (max - min) must be a power of two. + * Also we initialize the external K value to -1 so that we can + * avoid extra conditions check inside the core loop. + */ + if (bmin > dmin) + kvdb[--bmin - 1] = XDL_LINE_MAX; + else + ++bmin; + if (bmax < dmax) + kvdb[++bmax + 1] = XDL_LINE_MAX; + else + --bmax; + + for (d = bmax; d >= bmin; d -= 2) { + if (kvdb[d - 1] < kvdb[d + 1]) + i1 = kvdb[d - 1]; + else + i1 = kvdb[d + 1] - 1; + prev1 = i1; + i2 = i1 - d; + for (; i1 > off1 && i2 > off2 && ha1[i1 - 1] == ha2[i2 - 1]; i1--, i2--); + if (prev1 - i1 > xenv->snake_cnt) + got_snake = 1; + kvdb[d] = i1; + if (!odd && fmin <= d && d <= fmax && i1 <= kvdf[d]) { + spl->i1 = i1; + spl->i2 = i2; + spl->min_lo = spl->min_hi = 1; + return ec; + } + } + + if (need_min) + continue; + + /* + * If the edit cost is above the heuristic trigger and if + * we got a good snake, we sample current diagonals to see + * if some of the, have reached an "interesting" path. Our + * measure is a function of the distance from the diagonal + * corner (i1 + i2) penalized with the distance from the + * mid diagonal itself. If this value is above the current + * edit cost times a magic factor (XDL_K_HEUR) we consider + * it interesting. + */ + if (got_snake && ec > xenv->heur_min) { + for (best = 0, d = fmax; d >= fmin; d -= 2) { + dd = d > fmid ? d - fmid: fmid - d; + i1 = kvdf[d]; + i2 = i1 - d; + v = (i1 - off1) + (i2 - off2) - dd; + + if (v > XDL_K_HEUR * ec && v > best && + off1 + xenv->snake_cnt <= i1 && i1 < lim1 && + off2 + xenv->snake_cnt <= i2 && i2 < lim2) { + for (k = 1; ha1[i1 - k] == ha2[i2 - k]; k++) + if (k == xenv->snake_cnt) { + best = v; + spl->i1 = i1; + spl->i2 = i2; + break; + } + } + } + if (best > 0) { + spl->min_lo = 1; + spl->min_hi = 0; + return ec; + } + + for (best = 0, d = bmax; d >= bmin; d -= 2) { + dd = d > bmid ? d - bmid: bmid - d; + i1 = kvdb[d]; + i2 = i1 - d; + v = (lim1 - i1) + (lim2 - i2) - dd; + + if (v > XDL_K_HEUR * ec && v > best && + off1 < i1 && i1 <= lim1 - xenv->snake_cnt && + off2 < i2 && i2 <= lim2 - xenv->snake_cnt) { + for (k = 0; ha1[i1 + k] == ha2[i2 + k]; k++) + if (k == xenv->snake_cnt - 1) { + best = v; + spl->i1 = i1; + spl->i2 = i2; + break; + } + } + } + if (best > 0) { + spl->min_lo = 0; + spl->min_hi = 1; + return ec; + } + } + + /* + * Enough is enough. We spent too much time here and now we collect + * the furthest reaching path using the (i1 + i2) measure. + */ + if (ec >= xenv->mxcost) { + long fbest, fbest1, bbest, bbest1; + + fbest = fbest1 = -1; + for (d = fmax; d >= fmin; d -= 2) { + i1 = XDL_MIN(kvdf[d], lim1); + i2 = i1 - d; + if (lim2 < i2) + i1 = lim2 + d, i2 = lim2; + if (fbest < i1 + i2) { + fbest = i1 + i2; + fbest1 = i1; + } + } + + bbest = bbest1 = XDL_LINE_MAX; + for (d = bmax; d >= bmin; d -= 2) { + i1 = XDL_MAX(off1, kvdb[d]); + i2 = i1 - d; + if (i2 < off2) + i1 = off2 + d, i2 = off2; + if (i1 + i2 < bbest) { + bbest = i1 + i2; + bbest1 = i1; + } + } + + if ((lim1 + lim2) - bbest < fbest - (off1 + off2)) { + spl->i1 = fbest1; + spl->i2 = fbest - fbest1; + spl->min_lo = 1; + spl->min_hi = 0; + } else { + spl->i1 = bbest1; + spl->i2 = bbest - bbest1; + spl->min_lo = 0; + spl->min_hi = 1; + } + return ec; + } + } + + return -1; +} + + +/* + * Rule: "Divide et Impera". Recursively split the box in sub-boxes by calling + * the box splitting function. Note that the real job (marking changed lines) + * is done in the two boundary reaching checks. + */ +int xdl_recs_cmp(diffdata_t *dd1, long off1, long lim1, + diffdata_t *dd2, long off2, long lim2, + long *kvdf, long *kvdb, int need_min, xdalgoenv_t *xenv) { + unsigned long const *ha1 = dd1->ha, *ha2 = dd2->ha; + + /* + * Shrink the box by walking through each diagonal snake (SW and NE). + */ + for (; off1 < lim1 && off2 < lim2 && ha1[off1] == ha2[off2]; off1++, off2++); + for (; off1 < lim1 && off2 < lim2 && ha1[lim1 - 1] == ha2[lim2 - 1]; lim1--, lim2--); + + /* + * If one dimension is empty, then all records on the other one must + * be obviously changed. + */ + if (off1 == lim1) { + char *rchg2 = dd2->rchg; + long *rindex2 = dd2->rindex; + + for (; off2 < lim2; off2++) + rchg2[rindex2[off2]] = 1; + } else if (off2 == lim2) { + char *rchg1 = dd1->rchg; + long *rindex1 = dd1->rindex; + + for (; off1 < lim1; off1++) + rchg1[rindex1[off1]] = 1; + } else { + long ec; + xdpsplit_t spl; + spl.i1 = spl.i2 = 0; + + /* + * Divide ... + */ + if ((ec = xdl_split(ha1, off1, lim1, ha2, off2, lim2, kvdf, kvdb, + need_min, &spl, xenv)) < 0) { + + return -1; + } + + /* + * ... et Impera. + */ + if (xdl_recs_cmp(dd1, off1, spl.i1, dd2, off2, spl.i2, + kvdf, kvdb, spl.min_lo, xenv) < 0 || + xdl_recs_cmp(dd1, spl.i1, lim1, dd2, spl.i2, lim2, + kvdf, kvdb, spl.min_hi, xenv) < 0) { + + return -1; + } + } + + return 0; +} + + +int xdl_do_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp, + xdfenv_t *xe) { + long ndiags; + long *kvd, *kvdf, *kvdb; + xdalgoenv_t xenv; + diffdata_t dd1, dd2; + + if (xdl_prepare_env(mf1, mf2, xpp, xe) < 0) { + + return -1; + } + + /* + * Allocate and setup K vectors to be used by the differential algorithm. + * One is to store the forward path and one to store the backward path. + */ + ndiags = xe->xdf1.nreff + xe->xdf2.nreff + 3; + if (!(kvd = (long *) xdl_malloc((2 * ndiags + 2) * sizeof(long)))) { + + xdl_free_env(xe); + return -1; + } + kvdf = kvd; + kvdb = kvdf + ndiags; + kvdf += xe->xdf2.nreff + 1; + kvdb += xe->xdf2.nreff + 1; + + xenv.mxcost = xdl_bogosqrt(ndiags); + if (xenv.mxcost < XDL_MAX_COST_MIN) + xenv.mxcost = XDL_MAX_COST_MIN; + xenv.snake_cnt = XDL_SNAKE_CNT; + xenv.heur_min = XDL_HEUR_MIN_COST; + + dd1.nrec = xe->xdf1.nreff; + dd1.ha = xe->xdf1.ha; + dd1.rchg = xe->xdf1.rchg; + dd1.rindex = xe->xdf1.rindex; + dd2.nrec = xe->xdf2.nreff; + dd2.ha = xe->xdf2.ha; + dd2.rchg = xe->xdf2.rchg; + dd2.rindex = xe->xdf2.rindex; + + if (xdl_recs_cmp(&dd1, 0, dd1.nrec, &dd2, 0, dd2.nrec, + kvdf, kvdb, (xpp->flags & XDF_NEED_MINIMAL) != 0, &xenv) < 0) { + + xdl_free(kvd); + xdl_free_env(xe); + return -1; + } + + xdl_free(kvd); + + return 0; +} + + +static xdchange_t *xdl_add_change(xdchange_t *xscr, long i1, long i2, long chg1, long chg2) { + xdchange_t *xch; + + if (!(xch = (xdchange_t *) xdl_malloc(sizeof(xdchange_t)))) + return NULL; + + xch->next = xscr; + xch->i1 = i1; + xch->i2 = i2; + xch->chg1 = chg1; + xch->chg2 = chg2; + + return xch; +} + + +int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags) { + long ix, ixo, ixs, ixref, grpsiz, nrec = xdf->nrec; + char *rchg = xdf->rchg, *rchgo = xdfo->rchg; + xrecord_t **recs = xdf->recs; + + /* + * This is the same of what GNU diff does. Move back and forward + * change groups for a consistent and pretty diff output. This also + * helps in finding joinable change groups and reduce the diff size. + */ + for (ix = ixo = 0;;) { + /* + * Find the first changed line in the to-be-compacted file. + * We need to keep track of both indexes, so if we find a + * changed lines group on the other file, while scanning the + * to-be-compacted file, we need to skip it properly. Note + * that loops that are testing for changed lines on rchg* do + * not need index bounding since the array is prepared with + * a zero at position -1 and N. + */ + for (; ix < nrec && !rchg[ix]; ix++) + while (rchgo[ixo++]); + if (ix == nrec) + break; + + /* + * Record the start of a changed-group in the to-be-compacted file + * and find the end of it, on both to-be-compacted and other file + * indexes (ix and ixo). + */ + ixs = ix; + for (ix++; rchg[ix]; ix++); + for (; rchgo[ixo]; ixo++); + + do { + grpsiz = ix - ixs; + + /* + * If the line before the current change group, is equal to + * the last line of the current change group, shift backward + * the group. + */ + while (ixs > 0 && recs[ixs - 1]->ha == recs[ix - 1]->ha && + xdl_recmatch(recs[ixs - 1]->ptr, recs[ixs - 1]->size, recs[ix - 1]->ptr, recs[ix - 1]->size, flags)) { + rchg[--ixs] = 1; + rchg[--ix] = 0; + + /* + * This change might have joined two change groups, + * so we try to take this scenario in account by moving + * the start index accordingly (and so the other-file + * end-of-group index). + */ + for (; rchg[ixs - 1]; ixs--); + while (rchgo[--ixo]); + } + + /* + * Record the end-of-group position in case we are matched + * with a group of changes in the other file (that is, the + * change record before the enf-of-group index in the other + * file is set). + */ + ixref = rchgo[ixo - 1] ? ix: nrec; + + /* + * If the first line of the current change group, is equal to + * the line next of the current change group, shift forward + * the group. + */ + while (ix < nrec && recs[ixs]->ha == recs[ix]->ha && + xdl_recmatch(recs[ixs]->ptr, recs[ixs]->size, recs[ix]->ptr, recs[ix]->size, flags)) { + rchg[ixs++] = 0; + rchg[ix++] = 1; + + /* + * This change might have joined two change groups, + * so we try to take this scenario in account by moving + * the start index accordingly (and so the other-file + * end-of-group index). Keep tracking the reference + * index in case we are shifting together with a + * corresponding group of changes in the other file. + */ + for (; rchg[ix]; ix++); + while (rchgo[++ixo]) + ixref = ix; + } + } while (grpsiz != ix - ixs); + + /* + * Try to move back the possibly merged group of changes, to match + * the recorded postion in the other file. + */ + while (ixref < ix) { + rchg[--ixs] = 1; + rchg[--ix] = 0; + while (rchgo[--ixo]); + } + } + + return 0; +} + + +int xdl_build_script(xdfenv_t *xe, xdchange_t **xscr) { + xdchange_t *cscr = NULL, *xch; + char *rchg1 = xe->xdf1.rchg, *rchg2 = xe->xdf2.rchg; + long i1, i2, l1, l2; + + /* + * Trivial. Collects "groups" of changes and creates an edit script. + */ + for (i1 = xe->xdf1.nrec, i2 = xe->xdf2.nrec; i1 >= 0 || i2 >= 0; i1--, i2--) + if (rchg1[i1 - 1] || rchg2[i2 - 1]) { + for (l1 = i1; rchg1[i1 - 1]; i1--); + for (l2 = i2; rchg2[i2 - 1]; i2--); + + if (!(xch = xdl_add_change(cscr, i1, i2, l1 - i1, l2 - i2))) { + xdl_free_script(cscr); + return -1; + } + cscr = xch; + } + + *xscr = cscr; + + return 0; +} + + +void xdl_free_script(xdchange_t *xscr) { + xdchange_t *xch; + + while ((xch = xscr) != NULL) { + xscr = xscr->next; + xdl_free(xch); + } +} + + +int xdl_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp, + xdemitconf_t const *xecfg, xdemitcb_t *ecb) { + xdchange_t *xscr; + xdfenv_t xe; + + if (xdl_do_diff(mf1, mf2, xpp, &xe) < 0) { + + return -1; + } + if (xdl_change_compact(&xe.xdf1, &xe.xdf2, xpp->flags) < 0 || + xdl_change_compact(&xe.xdf2, &xe.xdf1, xpp->flags) < 0 || + xdl_build_script(&xe, &xscr) < 0) { + + xdl_free_env(&xe); + return -1; + } + if (xscr) { + if (xdl_emit_diff(&xe, xscr, ecb, xecfg) < 0) { + + xdl_free_script(xscr); + xdl_free_env(&xe); + return -1; + } + xdl_free_script(xscr); + } + xdl_free_env(&xe); + + return 0; +} + diff --git a/xdiff/xdiffi.h b/xdiff/xdiffi.h new file mode 100644 index 0000000000..472aeaecfa --- /dev/null +++ b/xdiff/xdiffi.h @@ -0,0 +1,60 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#if !defined(XDIFFI_H) +#define XDIFFI_H + + +typedef struct s_diffdata { + long nrec; + unsigned long const *ha; + long *rindex; + char *rchg; +} diffdata_t; + +typedef struct s_xdalgoenv { + long mxcost; + long snake_cnt; + long heur_min; +} xdalgoenv_t; + +typedef struct s_xdchange { + struct s_xdchange *next; + long i1, i2; + long chg1, chg2; +} xdchange_t; + + + +int xdl_recs_cmp(diffdata_t *dd1, long off1, long lim1, + diffdata_t *dd2, long off2, long lim2, + long *kvdf, long *kvdb, int need_min, xdalgoenv_t *xenv); +int xdl_do_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp, + xdfenv_t *xe); +int xdl_change_compact(xdfile_t *xdf, xdfile_t *xdfo, long flags); +int xdl_build_script(xdfenv_t *xe, xdchange_t **xscr); +void xdl_free_script(xdchange_t *xscr); +int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb, + xdemitconf_t const *xecfg); + +#endif /* #if !defined(XDIFFI_H) */ + diff --git a/xdiff/xemit.c b/xdiff/xemit.c new file mode 100644 index 0000000000..e291dc7608 --- /dev/null +++ b/xdiff/xemit.c @@ -0,0 +1,197 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#include "xinclude.h" + + + + +static long xdl_get_rec(xdfile_t *xdf, long ri, char const **rec); +static int xdl_emit_record(xdfile_t *xdf, long ri, char const *pre, xdemitcb_t *ecb); +static xdchange_t *xdl_get_hunk(xdchange_t *xscr, xdemitconf_t const *xecfg); + + + + +static long xdl_get_rec(xdfile_t *xdf, long ri, char const **rec) { + + *rec = xdf->recs[ri]->ptr; + + return xdf->recs[ri]->size; +} + + +static int xdl_emit_record(xdfile_t *xdf, long ri, char const *pre, xdemitcb_t *ecb) { + long size, psize = strlen(pre); + char const *rec; + + size = xdl_get_rec(xdf, ri, &rec); + if (xdl_emit_diffrec(rec, size, pre, psize, ecb) < 0) { + + return -1; + } + + return 0; +} + + +/* + * Starting at the passed change atom, find the latest change atom to be included + * inside the differential hunk according to the specified configuration. + */ +static xdchange_t *xdl_get_hunk(xdchange_t *xscr, xdemitconf_t const *xecfg) { + xdchange_t *xch, *xchp; + + for (xchp = xscr, xch = xscr->next; xch; xchp = xch, xch = xch->next) + if (xch->i1 - (xchp->i1 + xchp->chg1) > 2 * xecfg->ctxlen) + break; + + return xchp; +} + + +static void xdl_find_func(xdfile_t *xf, long i, char *buf, long sz, long *ll) { + + /* + * Be quite stupid about this for now. Find a line in the old file + * before the start of the hunk (and context) which starts with a + * plausible character. + */ + + const char *rec; + long len; + + *ll = 0; + while (i-- > 0) { + len = xdl_get_rec(xf, i, &rec); + if (len > 0 && + (isalpha((unsigned char)*rec) || /* identifier? */ + *rec == '_' || /* also identifier? */ + *rec == '$')) { /* mysterious GNU diff's invention */ + if (len > sz) + len = sz; + while (0 < len && isspace((unsigned char)rec[len - 1])) + len--; + memcpy(buf, rec, len); + *ll = len; + return; + } + } +} + + +int xdl_emit_common(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb, + xdemitconf_t const *xecfg) { + xdfile_t *xdf = &xe->xdf1; + const char *rchg = xdf->rchg; + long ix; + + for (ix = 0; ix < xdf->nrec; ix++) { + if (rchg[ix]) + continue; + if (xdl_emit_record(xdf, ix, "", ecb)) + return -1; + } + return 0; +} + +int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb, + xdemitconf_t const *xecfg) { + long s1, s2, e1, e2, lctx; + xdchange_t *xch, *xche; + char funcbuf[80]; + long funclen = 0; + + if (xecfg->flags & XDL_EMIT_COMMON) + return xdl_emit_common(xe, xscr, ecb, xecfg); + + for (xch = xche = xscr; xch; xch = xche->next) { + xche = xdl_get_hunk(xch, xecfg); + + s1 = XDL_MAX(xch->i1 - xecfg->ctxlen, 0); + s2 = XDL_MAX(xch->i2 - xecfg->ctxlen, 0); + + lctx = xecfg->ctxlen; + lctx = XDL_MIN(lctx, xe->xdf1.nrec - (xche->i1 + xche->chg1)); + lctx = XDL_MIN(lctx, xe->xdf2.nrec - (xche->i2 + xche->chg2)); + + e1 = xche->i1 + xche->chg1 + lctx; + e2 = xche->i2 + xche->chg2 + lctx; + + /* + * Emit current hunk header. + */ + + if (xecfg->flags & XDL_EMIT_FUNCNAMES) { + xdl_find_func(&xe->xdf1, s1, funcbuf, + sizeof(funcbuf), &funclen); + } + if (xdl_emit_hunk_hdr(s1 + 1, e1 - s1, s2 + 1, e2 - s2, + funcbuf, funclen, ecb) < 0) + return -1; + + /* + * Emit pre-context. + */ + for (; s1 < xch->i1; s1++) + if (xdl_emit_record(&xe->xdf1, s1, " ", ecb) < 0) + return -1; + + for (s1 = xch->i1, s2 = xch->i2;; xch = xch->next) { + /* + * Merge previous with current change atom. + */ + for (; s1 < xch->i1 && s2 < xch->i2; s1++, s2++) + if (xdl_emit_record(&xe->xdf1, s1, " ", ecb) < 0) + return -1; + + /* + * Removes lines from the first file. + */ + for (s1 = xch->i1; s1 < xch->i1 + xch->chg1; s1++) + if (xdl_emit_record(&xe->xdf1, s1, "-", ecb) < 0) + return -1; + + /* + * Adds lines from the second file. + */ + for (s2 = xch->i2; s2 < xch->i2 + xch->chg2; s2++) + if (xdl_emit_record(&xe->xdf2, s2, "+", ecb) < 0) + return -1; + + if (xch == xche) + break; + s1 = xch->i1 + xch->chg1; + s2 = xch->i2 + xch->chg2; + } + + /* + * Emit post-context. + */ + for (s1 = xche->i1 + xche->chg1; s1 < e1; s1++) + if (xdl_emit_record(&xe->xdf1, s1, " ", ecb) < 0) + return -1; + } + + return 0; +} + diff --git a/xdiff/xemit.h b/xdiff/xemit.h new file mode 100644 index 0000000000..e629417dd2 --- /dev/null +++ b/xdiff/xemit.h @@ -0,0 +1,34 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#if !defined(XEMIT_H) +#define XEMIT_H + + + +int xdl_emit_diff(xdfenv_t *xe, xdchange_t *xscr, xdemitcb_t *ecb, + xdemitconf_t const *xecfg); + + + +#endif /* #if !defined(XEMIT_H) */ + diff --git a/xdiff/xinclude.h b/xdiff/xinclude.h new file mode 100644 index 0000000000..04a9da82c9 --- /dev/null +++ b/xdiff/xinclude.h @@ -0,0 +1,43 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#if !defined(XINCLUDE_H) +#define XINCLUDE_H + +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <limits.h> + +#include "xmacros.h" +#include "xdiff.h" +#include "xtypes.h" +#include "xutils.h" +#include "xprepare.h" +#include "xdiffi.h" +#include "xemit.h" + + +#endif /* #if !defined(XINCLUDE_H) */ + diff --git a/xdiff/xmacros.h b/xdiff/xmacros.h new file mode 100644 index 0000000000..e2cd2023b3 --- /dev/null +++ b/xdiff/xmacros.h @@ -0,0 +1,54 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#if !defined(XMACROS_H) +#define XMACROS_H + + + + +#define XDL_MIN(a, b) ((a) < (b) ? (a): (b)) +#define XDL_MAX(a, b) ((a) > (b) ? (a): (b)) +#define XDL_ABS(v) ((v) >= 0 ? (v): -(v)) +#define XDL_ISDIGIT(c) ((c) >= '0' && (c) <= '9') +#define XDL_ADDBITS(v,b) ((v) + ((v) >> (b))) +#define XDL_MASKBITS(b) ((1UL << (b)) - 1) +#define XDL_HASHLONG(v,b) (XDL_ADDBITS((unsigned long)(v), b) & XDL_MASKBITS(b)) +#define XDL_PTRFREE(p) do { if (p) { xdl_free(p); (p) = NULL; } } while (0) +#define XDL_LE32_PUT(p, v) \ +do { \ + unsigned char *__p = (unsigned char *) (p); \ + *__p++ = (unsigned char) (v); \ + *__p++ = (unsigned char) ((v) >> 8); \ + *__p++ = (unsigned char) ((v) >> 16); \ + *__p = (unsigned char) ((v) >> 24); \ +} while (0) +#define XDL_LE32_GET(p, v) \ +do { \ + unsigned char const *__p = (unsigned char const *) (p); \ + (v) = (unsigned long) __p[0] | ((unsigned long) __p[1]) << 8 | \ + ((unsigned long) __p[2]) << 16 | ((unsigned long) __p[3]) << 24; \ +} while (0) + + +#endif /* #if !defined(XMACROS_H) */ + diff --git a/xdiff/xmerge.c b/xdiff/xmerge.c new file mode 100644 index 0000000000..b83b3348cc --- /dev/null +++ b/xdiff/xmerge.c @@ -0,0 +1,426 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003-2006 Davide Libenzi, Johannes E. Schindelin + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#include "xinclude.h" + +typedef struct s_xdmerge { + struct s_xdmerge *next; + /* + * 0 = conflict, + * 1 = no conflict, take first, + * 2 = no conflict, take second. + */ + int mode; + long i1, i2; + long chg1, chg2; +} xdmerge_t; + +static int xdl_append_merge(xdmerge_t **merge, int mode, + long i1, long chg1, long i2, long chg2) +{ + xdmerge_t *m = *merge; + if (m && (i1 <= m->i1 + m->chg1 || i2 <= m->i2 + m->chg2)) { + if (mode != m->mode) + m->mode = 0; + m->chg1 = i1 + chg1 - m->i1; + m->chg2 = i2 + chg2 - m->i2; + } else { + m = xdl_malloc(sizeof(xdmerge_t)); + if (!m) + return -1; + m->next = NULL; + m->mode = mode; + m->i1 = i1; + m->chg1 = chg1; + m->i2 = i2; + m->chg2 = chg2; + if (*merge) + (*merge)->next = m; + *merge = m; + } + return 0; +} + +static int xdl_cleanup_merge(xdmerge_t *c) +{ + int count = 0; + xdmerge_t *next_c; + + /* were there conflicts? */ + for (; c; c = next_c) { + if (c->mode == 0) + count++; + next_c = c->next; + free(c); + } + return count; +} + +static int xdl_merge_cmp_lines(xdfenv_t *xe1, int i1, xdfenv_t *xe2, int i2, + int line_count, long flags) +{ + int i; + xrecord_t **rec1 = xe1->xdf2.recs + i1; + xrecord_t **rec2 = xe2->xdf2.recs + i2; + + for (i = 0; i < line_count; i++) { + int result = xdl_recmatch(rec1[i]->ptr, rec1[i]->size, + rec2[i]->ptr, rec2[i]->size, flags); + if (!result) + return -1; + } + return 0; +} + +static int xdl_recs_copy(xdfenv_t *xe, int i, int count, int add_nl, char *dest) +{ + xrecord_t **recs = xe->xdf2.recs + i; + int size = 0; + + if (count < 1) + return 0; + + for (i = 0; i < count; size += recs[i++]->size) + if (dest) + memcpy(dest + size, recs[i]->ptr, recs[i]->size); + if (add_nl) { + i = recs[count - 1]->size; + if (i == 0 || recs[count - 1]->ptr[i - 1] != '\n') { + if (dest) + dest[size] = '\n'; + size++; + } + } + return size; +} + +static int xdl_fill_merge_buffer(xdfenv_t *xe1, const char *name1, + xdfenv_t *xe2, const char *name2, xdmerge_t *m, char *dest) +{ + const int marker_size = 7; + int marker1_size = (name1 ? strlen(name1) + 1 : 0); + int marker2_size = (name2 ? strlen(name2) + 1 : 0); + int conflict_marker_size = 3 * (marker_size + 1) + + marker1_size + marker2_size; + int size, i1, j; + + for (size = i1 = 0; m; m = m->next) { + if (m->mode == 0) { + size += xdl_recs_copy(xe1, i1, m->i1 - i1, 0, + dest ? dest + size : NULL); + if (dest) { + for (j = 0; j < marker_size; j++) + dest[size++] = '<'; + if (marker1_size) { + dest[size] = ' '; + memcpy(dest + size + 1, name1, + marker1_size - 1); + size += marker1_size; + } + dest[size++] = '\n'; + } else + size += conflict_marker_size; + size += xdl_recs_copy(xe1, m->i1, m->chg1, 1, + dest ? dest + size : NULL); + if (dest) { + for (j = 0; j < marker_size; j++) + dest[size++] = '='; + dest[size++] = '\n'; + } + size += xdl_recs_copy(xe2, m->i2, m->chg2, 1, + dest ? dest + size : NULL); + if (dest) { + for (j = 0; j < marker_size; j++) + dest[size++] = '>'; + if (marker2_size) { + dest[size] = ' '; + memcpy(dest + size + 1, name2, + marker2_size - 1); + size += marker2_size; + } + dest[size++] = '\n'; + } + } else if (m->mode == 1) + size += xdl_recs_copy(xe1, i1, m->i1 + m->chg1 - i1, 0, + dest ? dest + size : NULL); + else if (m->mode == 2) + size += xdl_recs_copy(xe2, m->i2 - m->i1 + i1, + m->i1 + m->chg2 - i1, 0, + dest ? dest + size : NULL); + else + continue; + i1 = m->i1 + m->chg1; + } + size += xdl_recs_copy(xe1, i1, xe1->xdf2.nrec - i1, 0, + dest ? dest + size : NULL); + return size; +} + +/* + * Sometimes, changes are not quite identical, but differ in only a few + * lines. Try hard to show only these few lines as conflicting. + */ +static int xdl_refine_conflicts(xdfenv_t *xe1, xdfenv_t *xe2, xdmerge_t *m, + xpparam_t const *xpp) +{ + for (; m; m = m->next) { + mmfile_t t1, t2; + xdfenv_t xe; + xdchange_t *xscr, *x; + int i1 = m->i1, i2 = m->i2; + + /* let's handle just the conflicts */ + if (m->mode) + continue; + + /* no sense refining a conflict when one side is empty */ + if (m->chg1 == 0 || m->chg2 == 0) + continue; + + /* + * This probably does not work outside git, since + * we have a very simple mmfile structure. + */ + t1.ptr = (char *)xe1->xdf2.recs[m->i1]->ptr; + t1.size = xe1->xdf2.recs[m->i1 + m->chg1 - 1]->ptr + + xe1->xdf2.recs[m->i1 + m->chg1 - 1]->size - t1.ptr; + t2.ptr = (char *)xe2->xdf2.recs[m->i2]->ptr; + t2.size = xe2->xdf2.recs[m->i2 + m->chg2 - 1]->ptr + + xe2->xdf2.recs[m->i2 + m->chg2 - 1]->size - t2.ptr; + if (xdl_do_diff(&t1, &t2, xpp, &xe) < 0) + return -1; + if (xdl_change_compact(&xe.xdf1, &xe.xdf2, xpp->flags) < 0 || + xdl_change_compact(&xe.xdf2, &xe.xdf1, xpp->flags) < 0 || + xdl_build_script(&xe, &xscr) < 0) { + xdl_free_env(&xe); + return -1; + } + if (!xscr) { + /* If this happens, the changes are identical. */ + xdl_free_env(&xe); + m->mode = 4; + continue; + } + x = xscr; + m->i1 = xscr->i1 + i1; + m->chg1 = xscr->chg1; + m->i2 = xscr->i2 + i2; + m->chg2 = xscr->chg2; + while (xscr->next) { + xdmerge_t *m2 = xdl_malloc(sizeof(xdmerge_t)); + if (!m2) { + xdl_free_env(&xe); + xdl_free_script(x); + return -1; + } + xscr = xscr->next; + m2->next = m->next; + m->next = m2; + m = m2; + m->mode = 0; + m->i1 = xscr->i1 + i1; + m->chg1 = xscr->chg1; + m->i2 = xscr->i2 + i2; + m->chg2 = xscr->chg2; + } + xdl_free_env(&xe); + xdl_free_script(x); + } + return 0; +} + +/* + * level == 0: mark all overlapping changes as conflict + * level == 1: mark overlapping changes as conflict only if not identical + * level == 2: analyze non-identical changes for minimal conflict set + * + * returns < 0 on error, == 0 for no conflicts, else number of conflicts + */ +static int xdl_do_merge(xdfenv_t *xe1, xdchange_t *xscr1, const char *name1, + xdfenv_t *xe2, xdchange_t *xscr2, const char *name2, + int level, xpparam_t const *xpp, mmbuffer_t *result) { + xdmerge_t *changes, *c; + int i1, i2, chg1, chg2; + + c = changes = NULL; + + while (xscr1 && xscr2) { + if (!changes) + changes = c; + if (xscr1->i1 + xscr1->chg1 < xscr2->i1) { + i1 = xscr1->i2; + i2 = xscr2->i2 - xscr2->i1 + xscr1->i1; + chg1 = xscr1->chg2; + chg2 = xscr1->chg1; + if (xdl_append_merge(&c, 1, i1, chg1, i2, chg2)) { + xdl_cleanup_merge(changes); + return -1; + } + xscr1 = xscr1->next; + continue; + } + if (xscr2->i1 + xscr2->chg1 < xscr1->i1) { + i1 = xscr1->i2 - xscr1->i1 + xscr2->i1; + i2 = xscr2->i2; + chg1 = xscr2->chg1; + chg2 = xscr2->chg2; + if (xdl_append_merge(&c, 2, i1, chg1, i2, chg2)) { + xdl_cleanup_merge(changes); + return -1; + } + xscr2 = xscr2->next; + continue; + } + if (level < 1 || xscr1->i1 != xscr2->i1 || + xscr1->chg1 != xscr2->chg1 || + xscr1->chg2 != xscr2->chg2 || + xdl_merge_cmp_lines(xe1, xscr1->i2, + xe2, xscr2->i2, + xscr1->chg2, xpp->flags)) { + /* conflict */ + int off = xscr1->i1 - xscr2->i1; + int ffo = off + xscr1->chg1 - xscr2->chg1; + + i1 = xscr1->i2; + i2 = xscr2->i2; + if (off > 0) + i1 -= off; + else + i2 += off; + chg1 = xscr1->i2 + xscr1->chg2 - i1; + chg2 = xscr2->i2 + xscr2->chg2 - i2; + if (ffo > 0) + chg2 += ffo; + else + chg1 -= ffo; + if (xdl_append_merge(&c, 0, i1, chg1, i2, chg2)) { + xdl_cleanup_merge(changes); + return -1; + } + } + + i1 = xscr1->i1 + xscr1->chg1; + i2 = xscr2->i1 + xscr2->chg1; + + if (i1 >= i2) + xscr2 = xscr2->next; + if (i2 >= i1) + xscr1 = xscr1->next; + } + while (xscr1) { + if (!changes) + changes = c; + i1 = xscr1->i2; + i2 = xscr1->i1 + xe2->xdf2.nrec - xe2->xdf1.nrec; + chg1 = xscr1->chg2; + chg2 = xscr1->chg1; + if (xdl_append_merge(&c, 1, i1, chg1, i2, chg2)) { + xdl_cleanup_merge(changes); + return -1; + } + xscr1 = xscr1->next; + } + while (xscr2) { + if (!changes) + changes = c; + i1 = xscr2->i1 + xe1->xdf2.nrec - xe1->xdf1.nrec; + i2 = xscr2->i2; + chg1 = xscr2->chg1; + chg2 = xscr2->chg2; + if (xdl_append_merge(&c, 2, i1, chg1, i2, chg2)) { + xdl_cleanup_merge(changes); + return -1; + } + xscr2 = xscr2->next; + } + if (!changes) + changes = c; + /* refine conflicts */ + if (level > 1 && xdl_refine_conflicts(xe1, xe2, changes, xpp) < 0) { + xdl_cleanup_merge(changes); + return -1; + } + /* output */ + if (result) { + int size = xdl_fill_merge_buffer(xe1, name1, xe2, name2, + changes, NULL); + result->ptr = xdl_malloc(size); + if (!result->ptr) { + xdl_cleanup_merge(changes); + return -1; + } + result->size = size; + xdl_fill_merge_buffer(xe1, name1, xe2, name2, changes, + result->ptr); + } + return xdl_cleanup_merge(changes); +} + +int xdl_merge(mmfile_t *orig, mmfile_t *mf1, const char *name1, + mmfile_t *mf2, const char *name2, + xpparam_t const *xpp, int level, mmbuffer_t *result) { + xdchange_t *xscr1, *xscr2; + xdfenv_t xe1, xe2; + int status; + + result->ptr = NULL; + result->size = 0; + + if (xdl_do_diff(orig, mf1, xpp, &xe1) < 0 || + xdl_do_diff(orig, mf2, xpp, &xe2) < 0) { + return -1; + } + if (xdl_change_compact(&xe1.xdf1, &xe1.xdf2, xpp->flags) < 0 || + xdl_change_compact(&xe1.xdf2, &xe1.xdf1, xpp->flags) < 0 || + xdl_build_script(&xe1, &xscr1) < 0) { + xdl_free_env(&xe1); + return -1; + } + if (xdl_change_compact(&xe2.xdf1, &xe2.xdf2, xpp->flags) < 0 || + xdl_change_compact(&xe2.xdf2, &xe2.xdf1, xpp->flags) < 0 || + xdl_build_script(&xe2, &xscr2) < 0) { + xdl_free_env(&xe2); + return -1; + } + status = 0; + if (xscr1 || xscr2) { + if (!xscr1) { + result->ptr = xdl_malloc(mf2->size); + memcpy(result->ptr, mf2->ptr, mf2->size); + result->size = mf2->size; + } else if (!xscr2) { + result->ptr = xdl_malloc(mf1->size); + memcpy(result->ptr, mf1->ptr, mf1->size); + result->size = mf1->size; + } else { + status = xdl_do_merge(&xe1, xscr1, name1, + &xe2, xscr2, name2, + level, xpp, result); + } + xdl_free_script(xscr1); + xdl_free_script(xscr2); + } + xdl_free_env(&xe1); + xdl_free_env(&xe2); + + return status; +} diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c new file mode 100644 index 0000000000..1be7b31950 --- /dev/null +++ b/xdiff/xprepare.c @@ -0,0 +1,469 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#include "xinclude.h" + + + +#define XDL_KPDIS_RUN 4 +#define XDL_MAX_EQLIMIT 1024 + + + +typedef struct s_xdlclass { + struct s_xdlclass *next; + unsigned long ha; + char const *line; + long size; + long idx; +} xdlclass_t; + +typedef struct s_xdlclassifier { + unsigned int hbits; + long hsize; + xdlclass_t **rchash; + chastore_t ncha; + long count; + long flags; +} xdlclassifier_t; + + + + +static int xdl_init_classifier(xdlclassifier_t *cf, long size, long flags); +static void xdl_free_classifier(xdlclassifier_t *cf); +static int xdl_classify_record(xdlclassifier_t *cf, xrecord_t **rhash, unsigned int hbits, + xrecord_t *rec); +static int xdl_prepare_ctx(mmfile_t *mf, long narec, xpparam_t const *xpp, + xdlclassifier_t *cf, xdfile_t *xdf); +static void xdl_free_ctx(xdfile_t *xdf); +static int xdl_clean_mmatch(char const *dis, long i, long s, long e); +static int xdl_cleanup_records(xdfile_t *xdf1, xdfile_t *xdf2); +static int xdl_trim_ends(xdfile_t *xdf1, xdfile_t *xdf2); +static int xdl_optimize_ctxs(xdfile_t *xdf1, xdfile_t *xdf2); + + + + +static int xdl_init_classifier(xdlclassifier_t *cf, long size, long flags) { + long i; + + cf->flags = flags; + + cf->hbits = xdl_hashbits((unsigned int) size); + cf->hsize = 1 << cf->hbits; + + if (xdl_cha_init(&cf->ncha, sizeof(xdlclass_t), size / 4 + 1) < 0) { + + return -1; + } + if (!(cf->rchash = (xdlclass_t **) xdl_malloc(cf->hsize * sizeof(xdlclass_t *)))) { + + xdl_cha_free(&cf->ncha); + return -1; + } + for (i = 0; i < cf->hsize; i++) + cf->rchash[i] = NULL; + + cf->count = 0; + + return 0; +} + + +static void xdl_free_classifier(xdlclassifier_t *cf) { + + xdl_free(cf->rchash); + xdl_cha_free(&cf->ncha); +} + + +static int xdl_classify_record(xdlclassifier_t *cf, xrecord_t **rhash, unsigned int hbits, + xrecord_t *rec) { + long hi; + char const *line; + xdlclass_t *rcrec; + + line = rec->ptr; + hi = (long) XDL_HASHLONG(rec->ha, cf->hbits); + for (rcrec = cf->rchash[hi]; rcrec; rcrec = rcrec->next) + if (rcrec->ha == rec->ha && + xdl_recmatch(rcrec->line, rcrec->size, + rec->ptr, rec->size, cf->flags)) + break; + + if (!rcrec) { + if (!(rcrec = xdl_cha_alloc(&cf->ncha))) { + + return -1; + } + rcrec->idx = cf->count++; + rcrec->line = line; + rcrec->size = rec->size; + rcrec->ha = rec->ha; + rcrec->next = cf->rchash[hi]; + cf->rchash[hi] = rcrec; + } + + rec->ha = (unsigned long) rcrec->idx; + + hi = (long) XDL_HASHLONG(rec->ha, hbits); + rec->next = rhash[hi]; + rhash[hi] = rec; + + return 0; +} + + +static int xdl_prepare_ctx(mmfile_t *mf, long narec, xpparam_t const *xpp, + xdlclassifier_t *cf, xdfile_t *xdf) { + unsigned int hbits; + long i, nrec, hsize, bsize; + unsigned long hav; + char const *blk, *cur, *top, *prev; + xrecord_t *crec; + xrecord_t **recs, **rrecs; + xrecord_t **rhash; + unsigned long *ha; + char *rchg; + long *rindex; + + if (xdl_cha_init(&xdf->rcha, sizeof(xrecord_t), narec / 4 + 1) < 0) { + + return -1; + } + if (!(recs = (xrecord_t **) xdl_malloc(narec * sizeof(xrecord_t *)))) { + + xdl_cha_free(&xdf->rcha); + return -1; + } + + hbits = xdl_hashbits((unsigned int) narec); + hsize = 1 << hbits; + if (!(rhash = (xrecord_t **) xdl_malloc(hsize * sizeof(xrecord_t *)))) { + + xdl_free(recs); + xdl_cha_free(&xdf->rcha); + return -1; + } + for (i = 0; i < hsize; i++) + rhash[i] = NULL; + + nrec = 0; + if ((cur = blk = xdl_mmfile_first(mf, &bsize)) != NULL) { + for (top = blk + bsize;;) { + if (cur >= top) { + if (!(cur = blk = xdl_mmfile_next(mf, &bsize))) + break; + top = blk + bsize; + } + prev = cur; + hav = xdl_hash_record(&cur, top, xpp->flags); + if (nrec >= narec) { + narec *= 2; + if (!(rrecs = (xrecord_t **) xdl_realloc(recs, narec * sizeof(xrecord_t *)))) { + + xdl_free(rhash); + xdl_free(recs); + xdl_cha_free(&xdf->rcha); + return -1; + } + recs = rrecs; + } + if (!(crec = xdl_cha_alloc(&xdf->rcha))) { + + xdl_free(rhash); + xdl_free(recs); + xdl_cha_free(&xdf->rcha); + return -1; + } + crec->ptr = prev; + crec->size = (long) (cur - prev); + crec->ha = hav; + recs[nrec++] = crec; + + if (xdl_classify_record(cf, rhash, hbits, crec) < 0) { + + xdl_free(rhash); + xdl_free(recs); + xdl_cha_free(&xdf->rcha); + return -1; + } + } + } + + if (!(rchg = (char *) xdl_malloc((nrec + 2) * sizeof(char)))) { + + xdl_free(rhash); + xdl_free(recs); + xdl_cha_free(&xdf->rcha); + return -1; + } + memset(rchg, 0, (nrec + 2) * sizeof(char)); + + if (!(rindex = (long *) xdl_malloc((nrec + 1) * sizeof(long)))) { + + xdl_free(rchg); + xdl_free(rhash); + xdl_free(recs); + xdl_cha_free(&xdf->rcha); + return -1; + } + if (!(ha = (unsigned long *) xdl_malloc((nrec + 1) * sizeof(unsigned long)))) { + + xdl_free(rindex); + xdl_free(rchg); + xdl_free(rhash); + xdl_free(recs); + xdl_cha_free(&xdf->rcha); + return -1; + } + + xdf->nrec = nrec; + xdf->recs = recs; + xdf->hbits = hbits; + xdf->rhash = rhash; + xdf->rchg = rchg + 1; + xdf->rindex = rindex; + xdf->nreff = 0; + xdf->ha = ha; + xdf->dstart = 0; + xdf->dend = nrec - 1; + + return 0; +} + + +static void xdl_free_ctx(xdfile_t *xdf) { + + xdl_free(xdf->rhash); + xdl_free(xdf->rindex); + xdl_free(xdf->rchg - 1); + xdl_free(xdf->ha); + xdl_free(xdf->recs); + xdl_cha_free(&xdf->rcha); +} + + +int xdl_prepare_env(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp, + xdfenv_t *xe) { + long enl1, enl2; + xdlclassifier_t cf; + + enl1 = xdl_guess_lines(mf1) + 1; + enl2 = xdl_guess_lines(mf2) + 1; + + if (xdl_init_classifier(&cf, enl1 + enl2 + 1, xpp->flags) < 0) { + + return -1; + } + + if (xdl_prepare_ctx(mf1, enl1, xpp, &cf, &xe->xdf1) < 0) { + + xdl_free_classifier(&cf); + return -1; + } + if (xdl_prepare_ctx(mf2, enl2, xpp, &cf, &xe->xdf2) < 0) { + + xdl_free_ctx(&xe->xdf1); + xdl_free_classifier(&cf); + return -1; + } + + xdl_free_classifier(&cf); + + if (xdl_optimize_ctxs(&xe->xdf1, &xe->xdf2) < 0) { + + xdl_free_ctx(&xe->xdf2); + xdl_free_ctx(&xe->xdf1); + return -1; + } + + return 0; +} + + +void xdl_free_env(xdfenv_t *xe) { + + xdl_free_ctx(&xe->xdf2); + xdl_free_ctx(&xe->xdf1); +} + + +static int xdl_clean_mmatch(char const *dis, long i, long s, long e) { + long r, rdis0, rpdis0, rdis1, rpdis1; + + /* + * Scans the lines before 'i' to find a run of lines that either + * have no match (dis[j] == 0) or have multiple matches (dis[j] > 1). + * Note that we always call this function with dis[i] > 1, so the + * current line (i) is already a multimatch line. + */ + for (r = 1, rdis0 = 0, rpdis0 = 1; (i - r) >= s; r++) { + if (!dis[i - r]) + rdis0++; + else if (dis[i - r] == 2) + rpdis0++; + else + break; + } + /* + * If the run before the line 'i' found only multimatch lines, we + * return 0 and hence we don't make the current line (i) discarded. + * We want to discard multimatch lines only when they appear in the + * middle of runs with nomatch lines (dis[j] == 0). + */ + if (rdis0 == 0) + return 0; + for (r = 1, rdis1 = 0, rpdis1 = 1; (i + r) <= e; r++) { + if (!dis[i + r]) + rdis1++; + else if (dis[i + r] == 2) + rpdis1++; + else + break; + } + /* + * If the run after the line 'i' found only multimatch lines, we + * return 0 and hence we don't make the current line (i) discarded. + */ + if (rdis1 == 0) + return 0; + rdis1 += rdis0; + rpdis1 += rpdis0; + + return rpdis1 * XDL_KPDIS_RUN < (rpdis1 + rdis1); +} + + +/* + * Try to reduce the problem complexity, discard records that have no + * matches on the other file. Also, lines that have multiple matches + * might be potentially discarded if they happear in a run of discardable. + */ +static int xdl_cleanup_records(xdfile_t *xdf1, xdfile_t *xdf2) { + long i, nm, rhi, nreff, mlim; + unsigned long hav; + xrecord_t **recs; + xrecord_t *rec; + char *dis, *dis1, *dis2; + + if (!(dis = (char *) xdl_malloc(xdf1->nrec + xdf2->nrec + 2))) { + + return -1; + } + memset(dis, 0, xdf1->nrec + xdf2->nrec + 2); + dis1 = dis; + dis2 = dis1 + xdf1->nrec + 1; + + if ((mlim = xdl_bogosqrt(xdf1->nrec)) > XDL_MAX_EQLIMIT) + mlim = XDL_MAX_EQLIMIT; + for (i = xdf1->dstart, recs = &xdf1->recs[xdf1->dstart]; i <= xdf1->dend; i++, recs++) { + hav = (*recs)->ha; + rhi = (long) XDL_HASHLONG(hav, xdf2->hbits); + for (nm = 0, rec = xdf2->rhash[rhi]; rec; rec = rec->next) + if (rec->ha == hav && ++nm == mlim) + break; + dis1[i] = (nm == 0) ? 0: (nm >= mlim) ? 2: 1; + } + + if ((mlim = xdl_bogosqrt(xdf2->nrec)) > XDL_MAX_EQLIMIT) + mlim = XDL_MAX_EQLIMIT; + for (i = xdf2->dstart, recs = &xdf2->recs[xdf2->dstart]; i <= xdf2->dend; i++, recs++) { + hav = (*recs)->ha; + rhi = (long) XDL_HASHLONG(hav, xdf1->hbits); + for (nm = 0, rec = xdf1->rhash[rhi]; rec; rec = rec->next) + if (rec->ha == hav && ++nm == mlim) + break; + dis2[i] = (nm == 0) ? 0: (nm >= mlim) ? 2: 1; + } + + for (nreff = 0, i = xdf1->dstart, recs = &xdf1->recs[xdf1->dstart]; + i <= xdf1->dend; i++, recs++) { + if (dis1[i] == 1 || + (dis1[i] == 2 && !xdl_clean_mmatch(dis1, i, xdf1->dstart, xdf1->dend))) { + xdf1->rindex[nreff] = i; + xdf1->ha[nreff] = (*recs)->ha; + nreff++; + } else + xdf1->rchg[i] = 1; + } + xdf1->nreff = nreff; + + for (nreff = 0, i = xdf2->dstart, recs = &xdf2->recs[xdf2->dstart]; + i <= xdf2->dend; i++, recs++) { + if (dis2[i] == 1 || + (dis2[i] == 2 && !xdl_clean_mmatch(dis2, i, xdf2->dstart, xdf2->dend))) { + xdf2->rindex[nreff] = i; + xdf2->ha[nreff] = (*recs)->ha; + nreff++; + } else + xdf2->rchg[i] = 1; + } + xdf2->nreff = nreff; + + xdl_free(dis); + + return 0; +} + + +/* + * Early trim initial and terminal matching records. + */ +static int xdl_trim_ends(xdfile_t *xdf1, xdfile_t *xdf2) { + long i, lim; + xrecord_t **recs1, **recs2; + + recs1 = xdf1->recs; + recs2 = xdf2->recs; + for (i = 0, lim = XDL_MIN(xdf1->nrec, xdf2->nrec); i < lim; + i++, recs1++, recs2++) + if ((*recs1)->ha != (*recs2)->ha) + break; + + xdf1->dstart = xdf2->dstart = i; + + recs1 = xdf1->recs + xdf1->nrec - 1; + recs2 = xdf2->recs + xdf2->nrec - 1; + for (lim -= i, i = 0; i < lim; i++, recs1--, recs2--) + if ((*recs1)->ha != (*recs2)->ha) + break; + + xdf1->dend = xdf1->nrec - i - 1; + xdf2->dend = xdf2->nrec - i - 1; + + return 0; +} + + +static int xdl_optimize_ctxs(xdfile_t *xdf1, xdfile_t *xdf2) { + + if (xdl_trim_ends(xdf1, xdf2) < 0 || + xdl_cleanup_records(xdf1, xdf2) < 0) { + + return -1; + } + + return 0; +} + diff --git a/xdiff/xprepare.h b/xdiff/xprepare.h new file mode 100644 index 0000000000..344c569e8b --- /dev/null +++ b/xdiff/xprepare.h @@ -0,0 +1,35 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#if !defined(XPREPARE_H) +#define XPREPARE_H + + + +int xdl_prepare_env(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp, + xdfenv_t *xe); +void xdl_free_env(xdfenv_t *xe); + + + +#endif /* #if !defined(XPREPARE_H) */ + diff --git a/xdiff/xtypes.h b/xdiff/xtypes.h new file mode 100644 index 0000000000..3593a664fc --- /dev/null +++ b/xdiff/xtypes.h @@ -0,0 +1,68 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#if !defined(XTYPES_H) +#define XTYPES_H + + + +typedef struct s_chanode { + struct s_chanode *next; + long icurr; +} chanode_t; + +typedef struct s_chastore { + chanode_t *head, *tail; + long isize, nsize; + chanode_t *ancur; + chanode_t *sncur; + long scurr; +} chastore_t; + +typedef struct s_xrecord { + struct s_xrecord *next; + char const *ptr; + long size; + unsigned long ha; +} xrecord_t; + +typedef struct s_xdfile { + chastore_t rcha; + long nrec; + unsigned int hbits; + xrecord_t **rhash; + long dstart, dend; + xrecord_t **recs; + char *rchg; + long *rindex; + long nreff; + unsigned long *ha; +} xdfile_t; + +typedef struct s_xdfenv { + xdfile_t xdf1, xdf2; +} xdfenv_t; + + + +#endif /* #if !defined(XTYPES_H) */ + diff --git a/xdiff/xutils.c b/xdiff/xutils.c new file mode 100644 index 0000000000..1b899f32c4 --- /dev/null +++ b/xdiff/xutils.c @@ -0,0 +1,341 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#include "xinclude.h" + + + +#define XDL_GUESS_NLINES 256 + + + + +long xdl_bogosqrt(long n) { + long i; + + /* + * Classical integer square root approximation using shifts. + */ + for (i = 1; n > 0; n >>= 2) + i <<= 1; + + return i; +} + + +int xdl_emit_diffrec(char const *rec, long size, char const *pre, long psize, + xdemitcb_t *ecb) { + int i = 2; + mmbuffer_t mb[3]; + + mb[0].ptr = (char *) pre; + mb[0].size = psize; + mb[1].ptr = (char *) rec; + mb[1].size = size; + if (size > 0 && rec[size - 1] != '\n') { + mb[2].ptr = (char *) "\n\\ No newline at end of file\n"; + mb[2].size = strlen(mb[2].ptr); + i++; + } + if (ecb->outf(ecb->priv, mb, i) < 0) { + + return -1; + } + + return 0; +} + +void *xdl_mmfile_first(mmfile_t *mmf, long *size) +{ + *size = mmf->size; + return mmf->ptr; +} + + +void *xdl_mmfile_next(mmfile_t *mmf, long *size) +{ + return NULL; +} + + +long xdl_mmfile_size(mmfile_t *mmf) +{ + return mmf->size; +} + + +int xdl_cha_init(chastore_t *cha, long isize, long icount) { + + cha->head = cha->tail = NULL; + cha->isize = isize; + cha->nsize = icount * isize; + cha->ancur = cha->sncur = NULL; + cha->scurr = 0; + + return 0; +} + + +void xdl_cha_free(chastore_t *cha) { + chanode_t *cur, *tmp; + + for (cur = cha->head; (tmp = cur) != NULL;) { + cur = cur->next; + xdl_free(tmp); + } +} + + +void *xdl_cha_alloc(chastore_t *cha) { + chanode_t *ancur; + void *data; + + if (!(ancur = cha->ancur) || ancur->icurr == cha->nsize) { + if (!(ancur = (chanode_t *) xdl_malloc(sizeof(chanode_t) + cha->nsize))) { + + return NULL; + } + ancur->icurr = 0; + ancur->next = NULL; + if (cha->tail) + cha->tail->next = ancur; + if (!cha->head) + cha->head = ancur; + cha->tail = ancur; + cha->ancur = ancur; + } + + data = (char *) ancur + sizeof(chanode_t) + ancur->icurr; + ancur->icurr += cha->isize; + + return data; +} + + +void *xdl_cha_first(chastore_t *cha) { + chanode_t *sncur; + + if (!(cha->sncur = sncur = cha->head)) + return NULL; + + cha->scurr = 0; + + return (char *) sncur + sizeof(chanode_t) + cha->scurr; +} + + +void *xdl_cha_next(chastore_t *cha) { + chanode_t *sncur; + + if (!(sncur = cha->sncur)) + return NULL; + cha->scurr += cha->isize; + if (cha->scurr == sncur->icurr) { + if (!(sncur = cha->sncur = sncur->next)) + return NULL; + cha->scurr = 0; + } + + return (char *) sncur + sizeof(chanode_t) + cha->scurr; +} + + +long xdl_guess_lines(mmfile_t *mf) { + long nl = 0, size, tsize = 0; + char const *data, *cur, *top; + + if ((cur = data = xdl_mmfile_first(mf, &size)) != NULL) { + for (top = data + size; nl < XDL_GUESS_NLINES;) { + if (cur >= top) { + tsize += (long) (cur - data); + if (!(cur = data = xdl_mmfile_next(mf, &size))) + break; + top = data + size; + } + nl++; + if (!(cur = memchr(cur, '\n', top - cur))) + cur = top; + else + cur++; + } + tsize += (long) (cur - data); + } + + if (nl && tsize) + nl = xdl_mmfile_size(mf) / (tsize / nl); + + return nl + 1; +} + +int xdl_recmatch(const char *l1, long s1, const char *l2, long s2, long flags) +{ + int i1, i2; + + if (flags & XDF_IGNORE_WHITESPACE) { + for (i1 = i2 = 0; i1 < s1 && i2 < s2; ) { + if (isspace(l1[i1])) + while (isspace(l1[i1]) && i1 < s1) + i1++; + if (isspace(l2[i2])) + while (isspace(l2[i2]) && i2 < s2) + i2++; + if (i1 < s1 && i2 < s2 && l1[i1++] != l2[i2++]) + return 0; + } + return (i1 >= s1 && i2 >= s2); + } else if (flags & XDF_IGNORE_WHITESPACE_CHANGE) { + for (i1 = i2 = 0; i1 < s1 && i2 < s2; ) { + if (isspace(l1[i1])) { + if (!isspace(l2[i2])) + return 0; + while (isspace(l1[i1]) && i1 < s1) + i1++; + while (isspace(l2[i2]) && i2 < s2) + i2++; + } else if (l1[i1++] != l2[i2++]) + return 0; + } + return (i1 >= s1 && i2 >= s2); + } else + return s1 == s2 && !memcmp(l1, l2, s1); + + return 0; +} + +unsigned long xdl_hash_record(char const **data, char const *top, long flags) { + unsigned long ha = 5381; + char const *ptr = *data; + + for (; ptr < top && *ptr != '\n'; ptr++) { + if (isspace(*ptr) && (flags & XDF_WHITESPACE_FLAGS)) { + while (ptr + 1 < top && isspace(ptr[1]) + && ptr[1] != '\n') + ptr++; + if (flags & XDF_IGNORE_WHITESPACE_CHANGE + && ptr[1] != '\n') { + ha += (ha << 5); + ha ^= (unsigned long) ' '; + } + continue; + } + ha += (ha << 5); + ha ^= (unsigned long) *ptr; + } + *data = ptr < top ? ptr + 1: ptr; + + return ha; +} + + +unsigned int xdl_hashbits(unsigned int size) { + unsigned int val = 1, bits = 0; + + for (; val < size && bits < CHAR_BIT * sizeof(unsigned int); val <<= 1, bits++); + return bits ? bits: 1; +} + + +int xdl_num_out(char *out, long val) { + char *ptr, *str = out; + char buf[32]; + + ptr = buf + sizeof(buf) - 1; + *ptr = '\0'; + if (val < 0) { + *--ptr = '-'; + val = -val; + } + for (; val && ptr > buf; val /= 10) + *--ptr = "0123456789"[val % 10]; + if (*ptr) + for (; *ptr; ptr++, str++) + *str = *ptr; + else + *str++ = '0'; + *str = '\0'; + + return str - out; +} + + +long xdl_atol(char const *str, char const **next) { + long val, base; + char const *top; + + for (top = str; XDL_ISDIGIT(*top); top++); + if (next) + *next = top; + for (val = 0, base = 1, top--; top >= str; top--, base *= 10) + val += base * (long)(*top - '0'); + return val; +} + + +int xdl_emit_hunk_hdr(long s1, long c1, long s2, long c2, + const char *func, long funclen, xdemitcb_t *ecb) { + int nb = 0; + mmbuffer_t mb; + char buf[128]; + + memcpy(buf, "@@ -", 4); + nb += 4; + + nb += xdl_num_out(buf + nb, c1 ? s1: s1 - 1); + + if (c1 != 1) { + memcpy(buf + nb, ",", 1); + nb += 1; + + nb += xdl_num_out(buf + nb, c1); + } + + memcpy(buf + nb, " +", 2); + nb += 2; + + nb += xdl_num_out(buf + nb, c2 ? s2: s2 - 1); + + if (c2 != 1) { + memcpy(buf + nb, ",", 1); + nb += 1; + + nb += xdl_num_out(buf + nb, c2); + } + + memcpy(buf + nb, " @@", 3); + nb += 3; + if (func && funclen) { + buf[nb++] = ' '; + if (funclen > sizeof(buf) - nb - 1) + funclen = sizeof(buf) - nb - 1; + memcpy(buf + nb, func, funclen); + nb += funclen; + } + buf[nb++] = '\n'; + + mb.ptr = buf; + mb.size = nb; + if (ecb->outf(ecb->priv, &mb, 1) < 0) + return -1; + + return 0; +} + diff --git a/xdiff/xutils.h b/xdiff/xutils.h new file mode 100644 index 0000000000..70d8b9838a --- /dev/null +++ b/xdiff/xutils.h @@ -0,0 +1,48 @@ +/* + * LibXDiff by Davide Libenzi ( File Differential Library ) + * Copyright (C) 2003 Davide Libenzi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Davide Libenzi <davidel@xmailserver.org> + * + */ + +#if !defined(XUTILS_H) +#define XUTILS_H + + + +long xdl_bogosqrt(long n); +int xdl_emit_diffrec(char const *rec, long size, char const *pre, long psize, + xdemitcb_t *ecb); +int xdl_cha_init(chastore_t *cha, long isize, long icount); +void xdl_cha_free(chastore_t *cha); +void *xdl_cha_alloc(chastore_t *cha); +void *xdl_cha_first(chastore_t *cha); +void *xdl_cha_next(chastore_t *cha); +long xdl_guess_lines(mmfile_t *mf); +int xdl_recmatch(const char *l1, long s1, const char *l2, long s2, long flags); +unsigned long xdl_hash_record(char const **data, char const *top, long flags); +unsigned int xdl_hashbits(unsigned int size); +int xdl_num_out(char *out, long val); +long xdl_atol(char const *str, char const **next); +int xdl_emit_hunk_hdr(long s1, long c1, long s2, long c2, + const char *func, long funclen, xdemitcb_t *ecb); + + + +#endif /* #if !defined(XUTILS_H) */ + |