diff options
265 files changed, 38249 insertions, 1 deletions
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/Makefile b/Documentation/Makefile new file mode 100644 index 0000000000..4798933a49 --- /dev/null +++ b/Documentation/Makefile @@ -0,0 +1,54 @@ +MAN1_TXT=$(wildcard git-*.txt) +MAN7_TXT=git.txt + +DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT) $(MAN7_TXT)) + +DOC_MAN1=$(patsubst %.txt,%.1,$(MAN1_TXT)) +DOC_MAN7=$(patsubst %.txt,%.7,$(MAN7_TXT)) + +prefix=$(HOME) +bin=$(prefix)/bin +mandir=$(prefix)/man +man1=$(mandir)/man1 +man7=$(mandir)/man7 + +INSTALL=install + +# +# 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) + + +man: man1 man7 +man1: $(DOC_MAN1) +man7: $(DOC_MAN7) + +install: + $(INSTALL) -m755 -d $(dest)/$(man1) $(dest)/$(man7) + $(INSTALL) $(DOC_MAN1) $(dest)/$(man1) + $(INSTALL) $(DOC_MAN7) $(dest)/$(man7) + +# 'include' dependencies +git-diff-%.txt: diff-format.txt diff-options.txt + touch $@ + +clean: + rm -f *.xml *.html *.1 *.7 + +%.html : %.txt + asciidoc -b xhtml11 -d manpage $< + +%.1 %.7 : %.xml + xmlto man $< + +%.xml : %.txt + asciidoc -b docbook -d manpage $< diff --git a/Documentation/cvs-migration.txt b/Documentation/cvs-migration.txt new file mode 100644 index 0000000000..a4d4b7e878 --- /dev/null +++ b/Documentation/cvs-migration.txt @@ -0,0 +1,232 @@ +Git for CVS users +================= + +Ok, so you're a CVS user. That's ok, it's a treatable condition, and the +first step to recovery is admitting you have a problem. The fact that +you are reading this file means that you may be well on that path +already. + +The thing about CVS is that it absolutely sucks as a source control +manager, and you'll thus be happy with almost anything else. Git, +however, may be a bit _too_ different (read: "good") for your taste, and +does a lot of things differently. + +One particular suckage of CVS is very hard to work around: CVS is +basically a tool for tracking _file_ history, while git is a tool for +tracking _project_ history. This sometimes causes problems if you are +used to doing very strange things in CVS, in particular if you're doing +things like making branches of just a subset of the project. Git can't +track that, since git never tracks things on the level of an individual +file, only on the whole project level. + +The good news is that most people don't do that, and in fact most sane +people think it's a bug in CVS that makes it tag (and check in changes) +one file at a time. So most projects you'll ever see will use CVS +_as_if_ it was sane. In which case you'll find it very easy indeed to +move over to Git. + +First off: this is not a git tutorial. See Documentation/tutorial.txt +for how git actually works. This is more of a random collection of +gotcha's and notes on converting from CVS to git. + +Second: CVS has the notion of a "repository" as opposed to the thing +that you're actually working in (your working directory, or your +"checked out tree"). Git does not have that notion at all, and all git +working directories _are_ the repositories. However, you can easily +emulate the CVS model by having one special "global repository", which +people can synchronize with. See details later, but in the meantime +just keep in mind that with git, every checked out working tree will +have a full revision control history of its own. + + +Importing a CVS archive +----------------------- + +Ok, you have an old project, and you want to at least give git a chance +to see how it performs. The first thing you want to do (after you've +gone through the git tutorial, and generally familiarized yourself with +how to commit stuff etc in git) is to create a git'ified version of your +CVS archive. + +Happily, that's very easy indeed. Git will do it for you, although git +will need the help of a program called "cvsps": + + http://www.cobite.com/cvsps/ + +which is not actually related to git at all, but which makes CVS usage +look almost sane (ie you almost certainly want to have it even if you +decide to stay with CVS). However, git will want at _least_ version 2.1 +of cvsps (available at the address above), and in fact will currently +refuse to work with anything else. + +Once you've gotten (and installed) cvsps, you may or may not want to get +any more familiar with it, but make sure it is in your path. After that, +the magic command line is + + git cvsimport -v -d <cvsroot> -C <destination> <module> + +which will do exactly what you'd think it does: it will create a git +archive of the named CVS module. The new archive will be created in the +subdirectory named <destination>; it'll be created if it doesn't exist. +Default is the local directory. + +It can take some time to actually do the conversion for a large archive +since it involves checking out from CVS every revision of every file, +and the conversion script is reasonably chatty unless you omit the '-v' +option, but on some not very scientific tests it averaged about twenty +revisions per second, so a medium-sized project should not take more +than a couple of minutes. For larger projects or remote repositories, +the process may take longer. + +After the (initial) import is done, the CVS archive's current head +revision will be checked out -- thus, you can start adding your own +changes right away. + +The import is incremental, i.e. if you call it again next month it'll +fetch any CVS updates that have been happening in the meantime. The +cut-off is date-based, so don't change the branches that were imported +from CVS. + +You can merge those updates (or, in fact, a different CVS branch) into +your main branch: + + cg-merge <branch> + +The HEAD revision from CVS is named "origin", not "HEAD", because git +already uses "HEAD". (If you don't like 'origin', use cvsimport's +'-o' option to change it.) + + +Emulating CVS behaviour +----------------------- + + +FIXME! Talk about setting up several repositories, and pulling and +pushing between them. Talk about merging, and branches. Some of this +needs to be in the tutorial too. + + + +CVS annotate +------------ + +So, something has gone wrong, and you don't know whom to blame, and +you're an ex-CVS user and used to do "cvs annotate" to see who caused +the breakage. You're looking for the "git annotate", and it's just +claiming not to find such a script. You're annoyed. + +Yes, that's right. Core git doesn't do "annotate", although it's +technically possible, and there are at least two specialized scripts out +there that can be used to get equivalent information (see the git +mailing list archives for details). + +Git has a couple of alternatives, though, that you may find sufficient +or even superior depending on your use. One is called "git-whatchanged" +(for obvious reasons) and the other one is called "pickaxe" ("a tool for +the software archeologist"). + +The "git-whatchanged" script is a truly trivial script that can give you +a good overview of what has changed in a file or a directory (or an +arbitrary list of files or directories). The "pickaxe" support is an +additional layer that can be used to further specify exactly what you're +looking for, if you already know the specific area that changed. + +Let's step back a bit and think about the reason why you would +want to do "cvs annotate a-file.c" to begin with. + +You would use "cvs annotate" on a file when you have trouble +with a function (or even a single "if" statement in a function) +that happens to be defined in the file, which does not do what +you want it to do. And you would want to find out why it was +written that way, because you are about to modify it to suit +your needs, and at the same time you do not want to break its +current callers. For that, you are trying to find out why the +original author did things that way in the original context. + +Many times, it may be enough to see the commit log messages of +commits that touch the file in question, possibly along with the +patches themselves, like this: + + $ git-whatchanged -p a-file.c + +This will show log messages and patches for each commit that +touches a-file. + +This, however, may not be very useful when this file has many +modifications that are not related to the piece of code you are +interested in. You would see many log messages and patches that +do not have anything to do with the piece of code you are +interested in. As an example, assuming that you have this piece +code that you are interested in in the HEAD version: + + if (frotz) { + nitfol(); + } + +you would use git-rev-list and git-diff-tree like this: + + $ git-rev-list HEAD | + git-diff-tree --stdin -v -p -S'if (frotz) { + nitfol(); + }' + +We have already talked about the "--stdin" form of git-diff-tree +command that reads the list of commits and compares each commit +with its parents. The git-whatchanged command internally runs +the equivalent of the above command, and can be used like this: + + $ git-whatchanged -p -S'if (frotz) { + nitfol(); + }' + +When the -S option is used, git-diff-tree command outputs +differences between two commits only if one tree has the +specified string in a file and the corresponding file in the +other tree does not. The above example looks for a commit that +has the "if" statement in it in a file, but its parent commit +does not have it in the same shape in the corresponding file (or +the other way around, where the parent has it and the commit +does not), and the differences between them are shown, along +with the commit message (thanks to the -v flag). It does not +show anything for commits that do not touch this "if" statement. + +Also, in the original context, the same statement might have +appeared at first in a different file and later the file was +renamed to "a-file.c". CVS annotate would not help you to go +back across such a rename, but GIT would still help you in such +a situation. For that, you can give the -C flag to +git-diff-tree, like this: + + $ git-whatchanged -p -C -S'if (frotz) { + nitfol(); + }' + +When the -C flag is used, file renames and copies are followed. +So if the "if" statement in question happens to be in "a-file.c" +in the current HEAD commit, even if the file was originally +called "o-file.c" and then renamed in an earlier commit, or if +the file was created by copying an existing "o-file.c" in an +earlier commit, you will not lose track. If the "if" statement +did not change across such rename or copy, then the commit that +does rename or copy would not show in the output, and if the +"if" statement was modified while the file was still called +"o-file.c", it would find the commit that changed the statement +when it was in "o-file.c". + +[ BTW, the current versions of "git-diff-tree -C" is not eager + enough to find copies, and it will miss the fact that a-file.c + was created by copying o-file.c unless o-file.c was somehow + changed in the same commit.] + +You can use the --pickaxe-all flag in addition to the -S flag. +This causes the differences from all the files contained in +those two commits, not just the differences between the files +that contain this changed "if" statement: + + $ git-whatchanged -p -C -S'if (frotz) { + nitfol(); + }' --pickaxe-all + +[ Side note. This option is called "--pickaxe-all" because -S + option is internally called "pickaxe", a tool for software + archaeologists.] diff --git a/Documentation/diff-format.txt b/Documentation/diff-format.txt new file mode 100644 index 0000000000..d6ce035419 --- /dev/null +++ b/Documentation/diff-format.txt @@ -0,0 +1,135 @@ +The output format from "git-diff-cache", "git-diff-tree" and +"git-diff-files" is very similar. + +These commands all compare two sets of things; what are +compared are different: + +git-diff-cache <tree-ish>:: + compares the <tree-ish> and the files on the filesystem. + +git-diff-cache --cached <tree-ish>:: + compares the <tree-ish> and the cache. + +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 cache 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... N file4 +delete :100644 000000 1234567... 0000000... D file5 +unmerged :000000 000000 0000000... 0000000... U file6 + +That is, from the left to the right: + + (1) a colon. + (2) mode for "src"; 000000 if creation or unmerged. + (3) a space. + (4) mode for "dst"; 000000 if deletion or unmerged. + (5) a space. + (6) sha1 for "src"; 0{40} if creation or unmerged. + (7) a space. + (8) sha1 for "dst"; 0{40} if creation, unmerged or "look at work tree". + (9) a space. + (10) status, followed by optional "score" number. + (11) a tab or a NUL when '-z' option is used. + (12) path for "src" + (13) a tab or a NUL when '-z' option is used; only exists for C or R. + (14) path for "dst"; only exists for C or R. + (15) an LF or a NUL when '-z' option is used, to terminate the record. + +<sha1> is shown as all 0's if new is a file on the filesystem +and it is out of sync with the cache. Example: + + :100644 100644 5be4a4...... 000000...... M file.c + +Generating patches with -p +-------------------------- + +When "git-diff-cache", "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. + +The patch generation can be customized at two levels. This +customization also applies to "git-diff-helper". + +1. When the environment variable 'GIT_EXTERNAL_DIFF' is not set, + these commands internally invoke "diff" like this: + + diff -L a/<path> -L a/<path> -pu <old> <new> ++ +For added files, `/dev/null` is used for <old>. For removed +files, `/dev/null` is used for <new> ++ +The "diff" formatting options can be customized via the +environment variable 'GIT_DIFF_OPTS'. For example, if you +prefer context diff: + + GIT_DIFF_OPTS=-c git-diff-cache -p $(cat .git/HEAD) + + +2. 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|ne>, + <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 +cache). '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>. + + +Git specific extention to diff format +------------------------------------- + +What -p option produces is slightly different from the +traditional diff format. + + (1) It is preceeded 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/ filename. + + When rename/copy is involved, file1 and file2 shows 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 extended header lines that are one or + more of: + + 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> diff --git a/Documentation/diff-options.txt b/Documentation/diff-options.txt new file mode 100644 index 0000000000..58b9c46c20 --- /dev/null +++ b/Documentation/diff-options.txt @@ -0,0 +1,53 @@ +-p:: + Generate patch (see section on generating patches) + +-u:: + Synonym for "-p". + +-r:: + Look recursivelly in subdirectories; this flag does not + mean anything to commands other than "git-diff-tree"; + other commands always looks at all the subdirectories. + +-z:: + \0 line termination on output + +--name-only:: + Show only names of changed files. + +--name-only-z:: + Same as --name-only, but terminate lines with NUL. + +-B:: + Break complete rewrite changes into pairs of delete and create. + +-M:: + Detect renames. + +-C:: + Detect copies as well as renames. + +--find-copies-harder:: + By default, -C option finds copies only if the original + file of the copy was modified in the same changeset for + performance reasons. 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. + +-S<string>:: + Look for differences that contains the change in <string>. + +--pickaxe-all:: + When -S finds a change, show all the changes in that + changeset, not just the files that contains the change + in <string>. + +-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 cache or + on-disk file to tree contents. + diff --git a/Documentation/diffcore.txt b/Documentation/diffcore.txt new file mode 100644 index 0000000000..6c474d1c0c --- /dev/null +++ b/Documentation/diffcore.txt @@ -0,0 +1,248 @@ +Tweaking diff output +==================== +June 2005 + + +Introduction +------------ + +The diff commands git-diff-cache, git-diff-files, and +git-diff-tree 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-cache 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. + +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... N 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 +----------------- + +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. + +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 +-------------- + +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... N 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 +--------------- + +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... N 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 contents of unchanged files are considered as +candidates of the source files in rename/copy operation, in +addition to the deleted files. 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... 0123456... N 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... 0123456... 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 +similarity score different from the default 50% by giving a +number after "-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 +--------------------- + +This transformation is used to merge filepairs broken by +diffcore-break, and were 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 defautls to + 50%). + +Note that earlier implementation left a broken pair as a separate +creation and deletion patches. This was unnecessary hack and +the latest implementation always merges all the broken pairs +back into modifications, but the resulting patch output is +formatted differently to still let the reviewing easier for 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 +---------------- + +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 touches 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 +-------------- + +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 line 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, typical orderfile for the core GIT probably +should look like this: + + README + Makefile + Documentation + *.h + *.c + t + diff --git a/Documentation/git-apply-patch-script.txt b/Documentation/git-apply-patch-script.txt new file mode 100644 index 0000000000..a6f860d424 --- /dev/null +++ b/Documentation/git-apply-patch-script.txt @@ -0,0 +1,32 @@ +git-apply-patch-script(1) +========================= +v0.1, May 2005 + +NAME +---- +git-apply-patch-script - Sample script to apply the diffs from git-diff-* + + +SYNOPSIS +-------- +'git-apply-patch-script' + +DESCRIPTION +----------- +This is a sample script to be used via the 'GIT_EXTERNAL_DIFF' +environment variable to apply the differences that the "git-diff-*" +family of commands report to the current work tree. + + +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 --git a/Documentation/git-apply.txt b/Documentation/git-apply.txt new file mode 100644 index 0000000000..391d6f5c2f --- /dev/null +++ b/Documentation/git-apply.txt @@ -0,0 +1,76 @@ +git-apply(1) +============ +v0.1, June 2005 + +NAME +---- +git-apply - Apply patch on a GIT index file and a work tree + + +SYNOPSIS +-------- +'git-apply' [--no-merge] [--stat] [--summary] [--check] [--index] [--show-files] [--apply] [<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. + +--no-merge:: + The default mode of operation is the merge behaviour + which is not implemented yet. This flag explicitly + tells the program not to use the merge behaviour. + +--stat:: + Instead of applying the patch, output diffstat for the + input. 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. + +--show-files:: + Show summary of files that are affected by the patch. + +--apply:: + If you use any of the options marked ``Turns off + "apply"'' above, git-apply reads and outputs the + information you asked without actually applying the + patch. Give this flag after those flags to also apply + the patch. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano + +GIT +--- +Part of the link:git.html[git] suite + diff --git a/Documentation/git-cat-file.txt b/Documentation/git-cat-file.txt new file mode 100644 index 0000000000..2131a29993 --- /dev/null +++ b/Documentation/git-cat-file.txt @@ -0,0 +1,61 @@ +git-cat-file(1) +=============== +v0.1, May 2005 + +NAME +---- +git-cat-file - Provide content or type information for repository objects + + +SYNOPSIS +-------- +'git-cat-file' (-t | -s | <type>) <object> + +DESCRIPTION +----------- +Provides content or type of objects in the repository. The type +is required unless '-t' is used to find the object type, +or '-s' is used to find the object size. + +OPTIONS +------- +<object>:: + The sha1 identifier of the object. + +-t:: + Instead of the content, show the object type identified by + <object>. + +-s:: + Instead of the content, show the object size identified by + <object>. + +<type>:: + Typically this matches the real type of <object> but asking + for a type that can trivially 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. + +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 link:git.html[git] suite + diff --git a/Documentation/git-check-files.txt b/Documentation/git-check-files.txt new file mode 100644 index 0000000000..6146098022 --- /dev/null +++ b/Documentation/git-check-files.txt @@ -0,0 +1,50 @@ +git-check-files(1) +================== +v0.1, May 2005 + +NAME +---- +git-check-files - Verify a list of files are up-to-date + + + +SYNOPSIS +-------- +'git-check-files' <file>... + +DESCRIPTION +----------- +Check that a list of files are up-to-date between the filesystem and +the cache. Used to verify a patch target before doing a patch. + +Files that do not exist on the filesystem are considered up-to-date +(whether or not they are in the cache). + +Emits an error message on failure: + +preparing to update existing file <file> not in cache:: + <file> exists but is not in the cache + +preparing to update file <file> not uptodate in cache:: + <file> on disk is not up-to-date with the cache + +Exits with a status code indicating success if all files are +up-to-date. + +See Also +-------- +link:git-update-cache.html[git-update-cache] + + +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 --git a/Documentation/git-checkout-cache.txt b/Documentation/git-checkout-cache.txt new file mode 100644 index 0000000000..321a00c251 --- /dev/null +++ b/Documentation/git-checkout-cache.txt @@ -0,0 +1,106 @@ +git-checkout-cache(1) +===================== +v0.1, May 2005 + +NAME +---- +git-checkout-cache - Copy files from the cache to the working directory + + +SYNOPSIS +-------- +'git-checkout-cache' [-u] [-q] [-a] [-f] [-n] [--prefix=<string>] + [--] <file>... + +DESCRIPTION +----------- +Will copy all files listed from the cache to the working directory +(not overwriting existing files). + +OPTIONS +------- +-u:: + update stat information for the checked out entries in + the cache file. + +-q:: + be quiet if files exist or are not in the cache + +-f:: + forces overwrite of existing files + +-a:: + checks out all files in the cache (will then continue to + process listed files). + +-n:: + Don't checkout new files, only refresh files already checked + out. + +--prefix=<string>:: + When creating files, prepend <string> (usually a directory + including a trailing /) + +--:: + Do not interpret any more arguments as options. + +Note that the order of the flags matters: + + git-checkout-cache -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-cache" does nothing. You probably meant +"git-checkout-cache -a". And if you want to force it, you want +"git-checkout-cache -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-cache -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 cache, which was not the point. + +To update and refresh only the files already checked out: + + git-checkout-cache -n -f -a && git-update-cache --ignore-missing --refresh + +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!). + +The prefix ability basically makes it trivial to use +git-checkout-cache as an "export as tree" function. Just read the +desired tree into the index, and do a + + git-checkout-cache --prefix=git-export-dir/ -a + +and git-checkout-cache will "export" the cache into the specified +directory. + +NOTE The final "/" is important. The exported name is literally just +prefixed with the specified string, so you can also do something like + + git-checkout-cache --prefix=.merged- Makefile + +to 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 link:git.html[git] suite + diff --git a/Documentation/git-clone-pack.txt b/Documentation/git-clone-pack.txt new file mode 100644 index 0000000000..0dc89a9072 --- /dev/null +++ b/Documentation/git-clone-pack.txt @@ -0,0 +1,65 @@ +git-clone-pack(1) +================= +v0.1, July 2005 + +NAME +---- +git-clone-pack - Clones a repository by receiving packed objects. + + +SYNOPSIS +-------- +'git-clone-pack' [-q] [--exec=<git-upload-pack>] [<host>:]<directory> [<head>...] + +DESCRIPTION +----------- +Clones a repository into the current repository by invoking +'git-upload-pack', possibly on the remote host via ssh, in +the named repository, and invoking 'git-unpack-objects' locally +to receive the pack. + +OPTIONS +------- +-q:: + Pass '-q' flag to 'git-unpack-objects'; this makes the + cloning process less verbose. + +--exec=<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). + +<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. + +<head>...:: + The heads to update. This is relative to $GIT_DIR + (e.g. "HEAD", "refs/heads/master"). When unspecified, + all heads are updated to match the remote repository. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano. + + +GIT +--- +Part of the link:git.html[git] suite + diff --git a/Documentation/git-clone-script.txt b/Documentation/git-clone-script.txt new file mode 100644 index 0000000000..afb25bd83c --- /dev/null +++ b/Documentation/git-clone-script.txt @@ -0,0 +1,60 @@ +git-clone-script(1) +=================== +v0.1, July 2005 + +NAME +---- +git-clone-script - Clones a repository. + + +SYNOPSIS +-------- +'git clone' [-l] [-u <upload-pack>] [-q] <repository> <directory> + +DESCRIPTION +----------- +Clones a repository into a newly created directory. + +OPTIONS +------- +-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. + +-q:: + Operate quietly. This flag is passed to "rsync" and + "git-clone-pack" commands when given. + +-u <upload-pack>:: + When given, and the repository to clone from is handled + by 'git-clone-pack', '--exec=<upload-pack>' is passed to + the command to specify non-default path for the command + run on the other end. + +<repository>:: + The (possibly remote) repository to clone from. It can + be an "rsync://host/dir" URL, an "http://host/dir" URL, + or [<host>:]/dir notation that is used by 'git-clone-pack'. + Currently http transport is not supported. + +<directory>:: + The name of a new directory to be cloned into. It is an + error to specify an existing directory. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano. + + +GIT +--- +Part of the link:git.html[git] suite + diff --git a/Documentation/git-commit-tree.txt b/Documentation/git-commit-tree.txt new file mode 100644 index 0000000000..5536668813 --- /dev/null +++ b/Documentation/git-commit-tree.txt @@ -0,0 +1,92 @@ +git-commit-tree(1) +================== +v0.1, May 2005 + +NAME +---- +git-commit-tree - Creates a new commit object + + +SYNOPSIS +-------- +'git-commit-tree' <tree> [-p <parent commit>]\ < changelog + +DESCRIPTION +----------- +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 `.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 a 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 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) + +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. + +See Also +-------- +link:git-write-tree.html[git-write-tree] + + +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 --git a/Documentation/git-convert-cache.txt b/Documentation/git-convert-cache.txt new file mode 100644 index 0000000000..66d7fe7855 --- /dev/null +++ b/Documentation/git-convert-cache.txt @@ -0,0 +1,30 @@ +git-convert-cache(1) +==================== +v0.1, May 2005 + +NAME +---- +git-convert-cache - Converts old-style GIT repository + + +SYNOPSIS +-------- +'git-convert-cache' + +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 link:git.html[git] suite + diff --git a/Documentation/git-cvsimport-script.txt b/Documentation/git-cvsimport-script.txt new file mode 100644 index 0000000000..05145e9383 --- /dev/null +++ b/Documentation/git-cvsimport-script.txt @@ -0,0 +1,80 @@ +git-cvsimport-script(1) +======================= +v0.1, July 2005 + +NAME +---- +git-cvsimport-script - Import a CVS repository into git + + +SYNOPSIS +-------- +'git-cvsimport-script' [ -o <branch-for-HEAD> ] [ -h ] [ -v ] + [ -d <CVSROOT> ] [ -p <options-for-cvsps> ] + [ -C <GIT_repository> ] [ -i ] [ <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. + +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. + +-i:: + Import-only: don't perform a checkout after importing. This option + ensures the working directory and cache remain untouched and will + not create them if they do not exist. + +-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 '-x' and '-A' are implicit and should not be used here. + + If you need to pass multiple options, separate them with a comma. + +-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. + +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 link:git.html[git] suite + diff --git a/Documentation/git-diff-cache.txt b/Documentation/git-diff-cache.txt new file mode 100644 index 0000000000..b463593408 --- /dev/null +++ b/Documentation/git-diff-cache.txt @@ -0,0 +1,134 @@ +git-diff-cache(1) +================= +v0.1, May 2005 + +NAME +---- +git-diff-cache - Compares content and mode of blobs between the cache and repository + + +SYNOPSIS +-------- +'git-diff-cache' [-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 cache 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 cache 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-cache" 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 index file, and are +ready to commit. You want to see eactly *what* you are going to commit is +without having to write a new tree object and compare it that way, and to +do that, you just do + + git-diff-cache --cached $(cat .git/HEAD) + +Example: let's say I had renamed `commit.c` to `git-commit.c`, and I had +done an "git-update-cache" 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-cache" does: + + torvalds@ppc970:~/git> git-diff-cache --cached $(cat .git/HEAD) + -100644 blob 4161aecc6700a2eb579e842af0b7f22b98443f74 commit.c + +100644 blob 4161aecc6700a2eb579e842af0b7f22b98443f74 git-commit.c + +You can trivially see that the above is a rename. + +In fact, "git-diff-cache --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-cache --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 cache, 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-cache" on it yet - there is no +"object" associated with the new state, and you get: + + torvalds@ppc970:~/v2.6/linux> git-diff-cache $(cat .git/HEAD ) + *100644->100664 blob 7476bb......->000000...... kernel/sched.c + +ie 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-cache" 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-upate-cache" it to make the cache be in sync. + +NOTE 2! 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 link:git.html[git] suite + diff --git a/Documentation/git-diff-files.txt b/Documentation/git-diff-files.txt new file mode 100644 index 0000000000..3e1244f8d3 --- /dev/null +++ b/Documentation/git-diff-files.txt @@ -0,0 +1,44 @@ +git-diff-files(1) +================= +v0.1, May 2005 + +NAME +---- +git-diff-files - Compares files in the working tree and the cache + + +SYNOPSIS +-------- +'git-diff-files' [-q] [<common diff options>] [<path>...] + +DESCRIPTION +----------- +Compares the files in the working tree and the cache. When paths +are specified, compares only those named paths. Otherwise all +entries in the cache are compared. The output format is the +same as "git-diff-cache" and "git-diff-tree". + +OPTIONS +------- +include::diff-options.txt[] + +-q:: + Remain silent even on nonexisting 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 link:git.html[git] suite + diff --git a/Documentation/git-diff-helper.txt b/Documentation/git-diff-helper.txt new file mode 100644 index 0000000000..d826deb42e --- /dev/null +++ b/Documentation/git-diff-helper.txt @@ -0,0 +1,53 @@ +git-diff-helper(1) +================== +v0.1, May 2005 + +NAME +---- +git-diff-helper - Generates patch format output for git-diff-* + + +SYNOPSIS +-------- +'git-diff-helper' [-z] [-S<string>] [-O<orderfile>] + +DESCRIPTION +----------- +Reads output from "git-diff-cache", "git-diff-tree" and "git-diff-files" and +generates patch format output. + +OPTIONS +------- +-z:: + \0 line termination on input + +-S<string>:: + Look for differences that contains the change in <string>. + +--pickaxe-all:: + When -S finds a change, show all the changes in that + changeset, not just the files that contains the change + in <string>. + +-O<orderfile>:: + Output the patch in the order specified in the + <orderfile>, which has one shell glob pattern per line. + +See Also +-------- +The section on generating patches in link:git-diff-cache.html[git-diff-cache] + + +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 link:git.html[git] suite + diff --git a/Documentation/git-diff-stages.txt b/Documentation/git-diff-stages.txt new file mode 100644 index 0000000000..2084c7041f --- /dev/null +++ b/Documentation/git-diff-stages.txt @@ -0,0 +1,41 @@ +git-diff-stages(1) +================== +v0.1, June 2005 + +NAME +---- +git-diff-stages - Compares content and mode of blobs between stages in an unmerged index file. + + +SYNOPSIS +-------- +'git-diff-stages' [<common diff options>] <stage1> <stage2> [<path>...] + +DESCRIPTION +----------- +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 link:git.html[git] suite diff --git a/Documentation/git-diff-tree.txt b/Documentation/git-diff-tree.txt new file mode 100644 index 0000000000..b2095cb049 --- /dev/null +++ b/Documentation/git-diff-tree.txt @@ -0,0 +1,130 @@ +git-diff-tree(1) +================ +v0.1, May 2005 + +NAME +---- +git-diff-tree - Compares the content and mode of blobs found via two tree objects + + +SYNOPSIS +-------- +'git-diff-tree' [--stdin] [-m] [-s] [-v] [--pretty] [-t] [<common diff options>] <tree-ish> <tree-ish> [<path>...] + +DESCRIPTION +----------- +Compares the content and mode of the blobs found via two tree objects. + +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. + ie file matches `/^<pattern1>|<pattern2>|.../` + Note that this parameter does not provide any wildcard or regexp + features. + +-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 +behaviour. 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. + +-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 supressed. It is + only useful with '-v' flag. + +-v:: + This flag causes "git-diff-tree --stdin" to also show + the commit message before the differences. + +--pretty[=(raw|medium|short)]:: + This is used to control "pretty printing" format of the + commit message. Without "=<style>", it defaults to + medium. + + +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 complete path comonent. +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-cache.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-cache" 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 link:git.html[git] suite + diff --git a/Documentation/git-export.txt b/Documentation/git-export.txt new file mode 100644 index 0000000000..d2d0dc498e --- /dev/null +++ b/Documentation/git-export.txt @@ -0,0 +1,31 @@ +git-export(1) +============= +v0.1, May 2005 + +NAME +---- +git-export - Exports each commit and a diff against each of its parents + + +SYNOPSIS +-------- +'git-export' top [base] + +DESCRIPTION +----------- +Exports each commit and diff against each of its parents, between +top and base. If base is not specified it exports everything. + + +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 --git a/Documentation/git-fetch-pack.txt b/Documentation/git-fetch-pack.txt new file mode 100644 index 0000000000..884a26bbf3 --- /dev/null +++ b/Documentation/git-fetch-pack.txt @@ -0,0 +1,73 @@ +git-fetch-pack(1) +================= +v0.1, July 2005 + +NAME +---- +git-fetch-pack - Receive missing objects from another repository. + + +SYNOPSIS +-------- +git-fetch-pack [-q] [--exec=<git-upload-pack>] [<host>:]<directory> [<head>...] < <commit-list> + +DESCRIPTION +----------- +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 fed from the standard input, to be sent to 'git-upload-pack' +running on the other end. + +This command can be used only when the local side has a common +(ancestor) commit with the remote head that is being pulled +from. Use 'git-clone-pack' for that. + + +OPTIONS +------- +-q:: + Pass '-q' flag to 'git-unpack-objects'; this makes the + cloning process less verbose. + +--exec=<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). + +<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. + +<head>...:: + 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. + + However the program refuses to work if more than one + remote head matches the specified heads. I am not sure + what this means... Help!!!!! + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the link:git.html[git] suite diff --git a/Documentation/git-fetch-script.txt b/Documentation/git-fetch-script.txt new file mode 100644 index 0000000000..937df05dbc --- /dev/null +++ b/Documentation/git-fetch-script.txt @@ -0,0 +1,47 @@ +git-fetch-script(1) +=================== +v0.1, July 2005 + +NAME +---- +git-fetch-script - Download objects and a head from another repository. + + +SYNOPSIS +-------- +'git-fetch-script' <repository> [ <head> | tag <tag> ] + + +DESCRIPTION +----------- +Fetches a named head or a tag from another repository, along +with the objects necessary to complete that head or tag. The +head to pull defaults to HEAD if unspecified. The head or tag +fetched from the remote repository is stored in +$GIT_DIR/FETCH_HEAD. + +When a <tag> is specified, the <tag> fetched from the remote is +also copied to the local $GIT_DIR/tags/<tag> file. When no +<head> nor <tag> is specified, and <repository> was specified +with the short-hand notation (i.e. naming a file under the +$GIT_DIR/branches directory), the head fetched from the remote +repository is also copied to the local $GIT_DIR/heads/<repository> +file. + + +OPTIONS +------- +include::pull-fetch-param.txt[] + + +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 link:git.html[git] suite diff --git a/Documentation/git-fsck-cache.txt b/Documentation/git-fsck-cache.txt new file mode 100644 index 0000000000..f1c18c10d8 --- /dev/null +++ b/Documentation/git-fsck-cache.txt @@ -0,0 +1,141 @@ +git-fsck-cache(1) +================= +v0.1, May 2005 + +NAME +---- +git-fsck-cache - Verifies the connectivity and validity of the objects in the database + + +SYNOPSIS +-------- +'git-fsck-cache' [--tags] [--root] [--unreachable] [--cache] [--standalone | --full] [<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-cache 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 cache also as a head node for + an unreachability trace. + +--standalone:: + Limit checks to the contents of GIT_OBJECT_DIRECTORY + (.git/objects), making sure that it is consistent and + complete without referring to objects found in alternate + object pools listed in GIT_ALTERNATE_OBJECT_DIRECTORIES, + nor packed GIT archives found in .git/objects/pack; + cannot be used with --full. + +--full:: + Check not just objects in GIT_OBJECT_DIRECTORY + (.git/objects), but also the ones found in alternate + object pools listed in GIT_ALTERNATE_OBJECT_DIRECTORIES, + and in packed GIT archives found in .git/objects/pack + and corresponding pack subdirectories in alternate + object pools; cannot be used with --standalone. + +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-cache --unreachable $(cat .git/HEAD) + +or, for Cogito users: + + git-fsck-cache --unreachable $(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-cache" is happy, you +do have a valid tree. + +Any corrupt objects you will have to find in backups or other archives +(ie you can just remove them and do an "rsync" with some other site in +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-cache: 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. + (note: this error occured during early git development when + the database format changed.) + +Environment Variables +--------------------- + +GIT_OBJECT_DIRECTORY:: + used to specify the object database root (usually .git/objects) + +GIT_INDEX_FILE:: + used to specify the cache + + +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 --git a/Documentation/git-hash-object.txt b/Documentation/git-hash-object.txt new file mode 100644 index 0000000000..8790d19b2f --- /dev/null +++ b/Documentation/git-hash-object.txt @@ -0,0 +1,36 @@ +git-hash-object(1) +================== +v0.1, May 2005 + +NAME +---- +git-hash-object - Computes object ID and optionally creates a blob from a file. + + +SYNOPSIS +-------- +'git-hash-object' [-t <type>] [-w] <any-file-on-the-filesystem> + +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-script" to update the cache +without modifying files in the work tree. When <type> is not +specified, it defaults to "blob". + + +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 link:git.html[git] suite + diff --git a/Documentation/git-http-pull.txt b/Documentation/git-http-pull.txt new file mode 100644 index 0000000000..431ef7cab3 --- /dev/null +++ b/Documentation/git-http-pull.txt @@ -0,0 +1,41 @@ +git-http-pull(1) +================ +v0.1, May 2005 + +NAME +---- +git-http-pull - Downloads a remote GIT repository via HTTP + + +SYNOPSIS +-------- +'git-http-pull' [-c] [-t] [-a] [-d] [-v] [-w filename] [--recover] commit-id url + +DESCRIPTION +----------- +Downloads a remote GIT repository via HTTP. + +-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 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 --git a/Documentation/git-init-db.txt b/Documentation/git-init-db.txt new file mode 100644 index 0000000000..99f96f7d4f --- /dev/null +++ b/Documentation/git-init-db.txt @@ -0,0 +1,40 @@ +git-init-db(1) +============== +v0.1, May 2005 + +NAME +---- +git-init-db - Creates an empty git object database + + +SYNOPSIS +-------- +'git-init-db' + +DESCRIPTION +----------- +This simply creates an empty git object database - basically a `.git` +directory and `.git/object/??/` directories. + +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/objects` directory is used. + +"git-init-db" won't hurt an existing repository. + + +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 --git a/Documentation/git-local-pull.txt b/Documentation/git-local-pull.txt new file mode 100644 index 0000000000..cb95334018 --- /dev/null +++ b/Documentation/git-local-pull.txt @@ -0,0 +1,43 @@ +git-local-pull(1) +================= +v0.1, May 2005 + +NAME +---- +git-local-pull - Duplicates another GIT repository on a local system + + +SYNOPSIS +-------- +'git-local-pull' [-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:: + Writes the commit-id into the filename under $GIT_DIR/refs/ on + the local end after the transfer is complete. + +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 link:git.html[git] suite + diff --git a/Documentation/git-ls-files.txt b/Documentation/git-ls-files.txt new file mode 100644 index 0000000000..be83ab12fe --- /dev/null +++ b/Documentation/git-ls-files.txt @@ -0,0 +1,108 @@ +git-ls-files(1) +=============== +v0.1, May 2005 + +NAME +---- +git-ls-files - Information about files in the cache/working directory + + +SYNOPSIS +-------- +'git-ls-files' [-z] [-t] + (--[cached|deleted|others|ignored|stage|unmerged|killed])\* + (-[c|d|o|i|s|u|k])\* + [-x <pattern>|--exclude=<pattern>] + [-X <file>|--exclude-from=<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 + +-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 + +-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-cache 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. + Allows the use of the famous dontdiff file as follows to find + out about uncommitted files just as dontdiff is used with + the diff command: + git-ls-files --others --exclude-from=dontdiff + +-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 + K to be killed + ? other + +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 Cogito) to see what should eventually be recorded at the +path. (see read-cache for more information on state) + +See Also +-------- +link:read-cache.html[read-cache] + + +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 --git a/Documentation/git-ls-tree.txt b/Documentation/git-ls-tree.txt new file mode 100644 index 0000000000..958b56d8f4 --- /dev/null +++ b/Documentation/git-ls-tree.txt @@ -0,0 +1,55 @@ +git-ls-tree(1) +============== +v0.1, May 2005 + +NAME +---- +git-ls-tree - Lists the contents of a tree object. + + +SYNOPSIS +-------- +'git-ls-tree' [-d] [-r] [-z] <tree-ish> [paths...] + +DESCRIPTION +----------- +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 + +-z:: + \0 line termination on output + +paths:: + When paths are given, shows them. Otherwise implicitly + uses the root level of the tree as the sole path argument. + + +Output Format +------------- + <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 +-------------- +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 --git a/Documentation/git-merge-base.txt b/Documentation/git-merge-base.txt new file mode 100644 index 0000000000..1e27bf2301 --- /dev/null +++ b/Documentation/git-merge-base.txt @@ -0,0 +1,34 @@ +git-merge-base(1) +================= +v0.1, May 2005 + +NAME +---- +git-merge-base - Finds as good a common ancestor as possible for a merge + + +SYNOPSIS +-------- +'git-merge-base' <commit> <commit> + +DESCRIPTION +----------- +"git-merge-base" finds as good a common ancestor as possible. 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... + + +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 --git a/Documentation/git-merge-cache.txt b/Documentation/git-merge-cache.txt new file mode 100644 index 0000000000..3fb4d492b1 --- /dev/null +++ b/Documentation/git-merge-cache.txt @@ -0,0 +1,84 @@ +git-merge-cache(1) +================== +v0.1, May 2005 + +NAME +---- +git-merge-cache - Runs a merge for files needing merging + + +SYNOPSIS +-------- +'git-merge-cache' [-o] <merge-program> (-a | -- | <file>\*) + +DESCRIPTION +----------- +This looks up the <file>(s) in the cache 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 +------- +--:: + Interpret all future arguments as filenames. + +-a:: + Run merge against all files in the cache 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. + +If "git-merge-cache" 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 the merge command from +the RCS package. + +A sample script called "git-merge-one-file-script" is included in the +ditribution. + +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-cache 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-cache 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-cache" will stop trying to +merge once anything has returned an error (ie "cat" returned an error +for the AA file, because it didn't exist in the original, and thus +"git-merge-cache" 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 link:git.html[git] suite + diff --git a/Documentation/git-merge-one-file-script.txt b/Documentation/git-merge-one-file-script.txt new file mode 100644 index 0000000000..387601d7e4 --- /dev/null +++ b/Documentation/git-merge-one-file-script.txt @@ -0,0 +1,30 @@ +git-merge-one-file-script(1) +============================ +v0.1, May 2005 + +NAME +---- +git-merge-one-file-script - The standard helper program to use with "git-merge-cache" + + +SYNOPSIS +-------- +'git-merge-one-file-script' + +DESCRIPTION +----------- +This is the standard helper program to use with "git-merge-cache" +to resolve a merge after the trivial merge done with "git-read-tree -m". + +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 --git a/Documentation/git-mktag.txt b/Documentation/git-mktag.txt new file mode 100644 index 0000000000..708f4ef8da --- /dev/null +++ b/Documentation/git-mktag.txt @@ -0,0 +1,48 @@ +git-mktag(1) +============ +v0.1, May 2005 + +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 link:git.html[git] suite + diff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt new file mode 100644 index 0000000000..ae6e88d433 --- /dev/null +++ b/Documentation/git-pack-objects.txt @@ -0,0 +1,80 @@ +git-pack-objects(1) +=================== +v0.1, July 2005 + +NAME +---- +git-pack-objects - Create a packed archive of objects. + + +SYNOPSIS +-------- +'git-pack-objects' [--incremental] [--window=N] [--depth=N] {--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 pack subdirectory of $GIT_OBJECT_DIRECTORY (or +any of the directories on $GIT_ALTERNATE_OBJECT_DIRECTORIES) +enables GIT to read from such an archive. + + +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 object names (currently in random order so it does + not have any useful meaning) to make the resulting + filename reasonably unique, and written to the standard + output of the command. + +--stdout:: + Write the pack contents (what would have been writtin to + .pack file) out to the standard output. + +--window and --depth:: + These two options affects 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. + +--incremental:: + This flag causes an object already in a pack ignored + even if it appears in the standard input. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +------------- +Documentation by Junio C Hamano + +GIT +--- +Part of the link:git.html[git] suite + diff --git a/Documentation/git-prune-script.txt b/Documentation/git-prune-script.txt new file mode 100644 index 0000000000..537b7905b6 --- /dev/null +++ b/Documentation/git-prune-script.txt @@ -0,0 +1,32 @@ +git-prune-script(1) +=================== +v0.1, May 2005 + +NAME +---- +git-prune-script - Prunes all unreachable objects from the object database + + +SYNOPSIS +-------- +'git-prune-script' + +DESCRIPTION +----------- +This runs "git-fsck-cache --unreachable" program using the heads specified +on the command line (or `.git/refs/heads/\*` and `.git/refs/tags/\*` if none is +specified), and prunes all unreachable objects from the object database. + + +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 --git a/Documentation/git-pull-script.txt b/Documentation/git-pull-script.txt new file mode 100644 index 0000000000..ec1e7a2a90 --- /dev/null +++ b/Documentation/git-pull-script.txt @@ -0,0 +1,37 @@ +git-pull-script(1) +================== +v0.1, May 2005 + +NAME +---- +git-pull-script - Pull and merge from another repository. + + +SYNOPSIS +-------- +'git-pull-script' <repository> [ <head> | tag <tag> ] + + +DESCRIPTION +----------- +Runs 'git-fetch-script' with the given parameters, then +'git-resolve-script' to merge the local HEAD and FETCH_HEAD. + + +OPTIONS +------- +include::pull-fetch-param.txt[] + + +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 link:git.html[git] suite + diff --git a/Documentation/git-read-tree.txt b/Documentation/git-read-tree.txt new file mode 100644 index 0000000000..7665946f80 --- /dev/null +++ b/Documentation/git-read-tree.txt @@ -0,0 +1,268 @@ +git-read-tree(1) +================ +v0.1, May 2005 + +NAME +---- +git-read-tree - Reads tree information into the directory cache + + +SYNOPSIS +-------- +'git-read-tree' (<tree-ish> | [-m [-u]] <tree-ish1> [<tree-ish2> [<tree-ish3>]]) + + +DESCRIPTION +----------- +Reads the tree information given by <tree-ish> into the directory cache, +but does not actually *update* any of the files it "caches". (see: +git-checkout-cache) + +Optionally, it can merge a tree into the cache, 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. + +-u:: + After a successful merge, update the files in the work + tree with the result of the merge. + +<tree-ish#>:: + The id of the tree object(s) to be read/merged. + + +Merging +------- +If '-m' is specified, "git-read-tree" can performs 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 cache has an entry for a +given pathname; and the contents of the path matches with the tree +being read, the stat info from the cache is used. (In other words, the +cache'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-cache -f -u -a", the "git-checkout-cache" 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 cache + 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 cache + 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 cache 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-cache --cached $M". Note that this does not +necessarily match "git-diff-cache --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-cache +--cached $H" would have told you about the change before this +merge, but it would not show in "git-diff-cache --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". + +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 stage 2 and 3) + + - stage 1 and stage 2 are the same and stage 3 is different; take + stage 3 (some work has been done on stage 3) + + - stage 1 and stage 3 are the same and stage 2 is different take + stage 2 (some work has been done on stage 2) + +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 "script + 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 (ie "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-cache" with supplied +"git-merge-one-file-script" to do this last step. The script +does not touch the files in the work tree, and the entire merge +happens in the index file. In other words, there is no need to +worry about what is in the working directory, since it is never +shown and never used. + +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. To illustrate, suppose you start from what has been +commited last to your repository: + + $ JC=`cat .git/HEAD` + $ git-checkout-cache -f -u -a $JC + +You do random edits, without running git-update-cache. And then +you notice that the tip of your "upstream" tree has advanced +since you pulled from him: + + $ git-fetch-script rsync://.... linus + $ LT=`cat .git/MERGE_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 cache 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-cache git-merge-one-file-script -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. + + +See Also +-------- +link:git-write-tree.html[git-write-tree]; link:git-ls-files.html[git-ls-files] + + +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 --git a/Documentation/git-receive-pack.txt b/Documentation/git-receive-pack.txt new file mode 100644 index 0000000000..d3a835d4ba --- /dev/null +++ b/Documentation/git-receive-pack.txt @@ -0,0 +1,41 @@ +git-receive-pack(1) +=================== +v0.1, July 2005 + +NAME +---- +git-receive-pack - Receive what is pushed into it + + +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' and +'git-clone-pack'. + + +OPTIONS +------- +<directory>:: + The repository to sync into. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the link:git.html[git] suite diff --git a/Documentation/git-resolve-script.txt b/Documentation/git-resolve-script.txt new file mode 100644 index 0000000000..8dd84a381a --- /dev/null +++ b/Documentation/git-resolve-script.txt @@ -0,0 +1,30 @@ +git-resolve-script(1) +===================== +v0.1, May 2005 + +NAME +---- +git-resolve-script - Script used to merge two trees + + +SYNOPSIS +-------- +'git-resolve-script' + +DESCRIPTION +----------- +This script is used by Linus to merge two trees. + + +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 --git a/Documentation/git-rev-list.txt b/Documentation/git-rev-list.txt new file mode 100644 index 0000000000..fe86c9c26d --- /dev/null +++ b/Documentation/git-rev-list.txt @@ -0,0 +1,75 @@ +git-rev-list(1) +=============== +v0.1, May 2005 + +NAME +---- +git-rev-list - Lists commit objects in reverse chronological order + + +SYNOPSIS +-------- +'git-rev-list' [ *--max-count*=number ] [ *--max-age*=timestamp ] [ *--min-age*=timestamp ] [ *--merge-order* [ *--show-breaks* ] ] <commit> + +DESCRIPTION +----------- +Lists commit objects in reverse chronological order starting at the +given commit, taking ancestry relationship into account. This is +useful to produce human-readable log output. + +If *--merge-order* is specified, the commit history is decomposed into a +unique sequence of minimal, non-linear epochs and maximal, linear epochs. +Non-linear epochs are then linearised by sorting them into merge order, which +is described below. + +Maximal, linear epochs correspond to periods of sequential development. +Minimal, non-linear epochs correspond to periods of divergent development +followed by a converging merge. The theory of epochs is described in more +detail at +link:http://blackcubes.dyndns.org/epoch/[http://blackcubes.dyndns.org/epoch/]. + +The merge order for a non-linear epoch is defined as a linearisation for which +the following invariants are true: + + 1. if a commit P is reachable from commit N, commit P sorts after commit N + in the linearised list. + 2. if Pi and Pj are any two parents of a merge M (with i < j), then any + commit N, such that N is reachable from Pj but not reachable from Pi, + sorts before all commits reachable from Pi. + +Invariant 1 states that later commits appear before earlier commits they are +derived from. + +Invariant 2 states that commits unique to "later" parents in a merge, appear +before all commits from "earlier" parents of a merge. + +If *--show-breaks* is specified, each item of the list is output with a +2-character prefix consisting of one of: (|), (^), (=) followed by a space. + +Commits marked with (=) represent the boundaries of minimal, non-linear epochs +and correspond either to the start of a period of divergent development or to +the end of such a period. + +Commits marked with (|) are direct parents of commits immediately preceding +the marked commit in the list. + +Commits marked with (^) are not parents of the immediately preceding commit. +These "breaks" represent necessary discontinuities implied by trying to +represent an arbtirary DAG in a linear form. + +*--show-breaks* is only valid if *--merge-order* is also specified. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Original *--merge-order* logic by Jon Seymour <jon.seymour@gmail.com> + +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 --git a/Documentation/git-rev-tree.txt b/Documentation/git-rev-tree.txt new file mode 100644 index 0000000000..2ec7ed073b --- /dev/null +++ b/Documentation/git-rev-tree.txt @@ -0,0 +1,88 @@ +git-rev-tree(1) +=============== +v0.1, May 2005 + +NAME +---- +git-rev-tree - Provides the revision tree for one or more commits + + +SYNOPSIS +-------- +'git-rev-tree' [--edges] [--cache <cache-file>] [^]<commit> [[^]<commit>] + +DESCRIPTION +----------- +Provides the revision tree for one or more commits. + +OPTIONS +------- +--edges:: + Show edges (ie places where the marking changes between parent + and child) + +--cache <cache-file>:: + Use the specified file as a cache from a previous git-rev-list run + to speed things up. Note that this "cache" is totally different + concept from the directory index. Also this option is not + implemented yet. + +[^]<commit>:: + The commit id to trace (a leading caret means to ignore this + commit-id and below) + +Output +------ + + <date> <commit>:<flags> [<parent-commit>:<flags> ]\* + +<date>:: + Date in 'seconds since epoch' + +<commit>:: + id of commit object + +<parent-commit>:: + id of each parent commit object (>1 indicates a merge) + +<flags>:: + + The flags are read as a bitmask representing each commit + provided on the commandline. eg: given the command: + + $ git-rev-tree <com1> <com2> <com3> + + The output: + + <date> <commit>:5 + + means that <commit> is reachable from <com1>(1) and <com3>(4) + +A revtree can get quite large. "git-rev-tree" will eventually allow +you to cache previous state so that you don't have to follow the whole +thing down. + +So the change difference between two commits is literally + + git-rev-tree [commit-id1] > commit1-revtree + git-rev-tree [commit-id2] > commit2-revtree + join -t : commit1-revtree commit2-revtree > common-revisions + +(this is also how to find the most common parent - you'd look at just +the head revisions - the ones that aren't referred to by other +revisions - in "common-revision", and figure out the best one. I +think.) + + +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 --git a/Documentation/git-send-pack.txt b/Documentation/git-send-pack.txt new file mode 100644 index 0000000000..245a7d0bde --- /dev/null +++ b/Documentation/git-send-pack.txt @@ -0,0 +1,46 @@ +git-send-pack(1) +================ +v0.1, July 2005 + +NAME +---- +git-send-pack - Push missing objects packed. + + +SYNOPSIS +-------- +'git-send-pack' [--exec=<git-receive-pack>] [<host>:]<directory> [<head>...] + +DESCRIPTION +----------- +Invokes 'git-receive-pack' on a possibly remote repository, and +updates it from the current repository, sending named heads. + + +OPTIONS +------- +--exec=<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. + +<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. + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +-------------- +Documentation by Junio C Hamano. + +GIT +--- +Part of the link:git.html[git] suite diff --git a/Documentation/git-show-index.txt b/Documentation/git-show-index.txt new file mode 100644 index 0000000000..beefe947af --- /dev/null +++ b/Documentation/git-show-index.txt @@ -0,0 +1,36 @@ +git-show-index(1) +================= +v0.1, July 2005 + +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 link:git.html[git] suite + diff --git a/Documentation/git-ssh-pull.txt b/Documentation/git-ssh-pull.txt new file mode 100644 index 0000000000..ac3fb342f0 --- /dev/null +++ b/Documentation/git-ssh-pull.txt @@ -0,0 +1,52 @@ +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] [-w filename] [--recover] commit-id url + +DESCRIPTION +----------- +Pulls from a remote repository over ssh connection, invoking +git-ssh-push on the other end. It functions identically to +git-ssh-push, 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 link:git.html[git] suite + diff --git a/Documentation/git-ssh-push.txt b/Documentation/git-ssh-push.txt new file mode 100644 index 0000000000..e38679deb9 --- /dev/null +++ b/Documentation/git-ssh-push.txt @@ -0,0 +1,48 @@ +git-ssh-push(1) +=============== +v0.1, Jun 2005 + +NAME +---- +git-ssh-push - Pushes to a remote repository over ssh connection + + +SYNOPSIS +-------- +'git-ssh-push' [-c] [-t] [-a] [-d] [-v] [-w filename] [--recover] commit-id url + +DESCRIPTION +----------- +Pushes from a remote repository over ssh connection, invoking +git-ssh-pull on the other end. It functions identically to +git-ssh-pull, 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 link:git.html[git] suite + diff --git a/Documentation/git-tag-script.txt b/Documentation/git-tag-script.txt new file mode 100644 index 0000000000..daf350b5bf --- /dev/null +++ b/Documentation/git-tag-script.txt @@ -0,0 +1,32 @@ +git-tag-script(1) +================= +v0.1, May 2005 + +NAME +---- +git-tag-script - An example script to create a tag object signed with GPG + + + +SYNOPSIS +-------- +'git-tag-script' + +DESCRIPTION +----------- +This is an example script that uses "git-mktag" to create a tag object +signed with GPG. + + +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 --git a/Documentation/git-tar-tree.txt b/Documentation/git-tar-tree.txt new file mode 100644 index 0000000000..dc8d0fa92f --- /dev/null +++ b/Documentation/git-tar-tree.txt @@ -0,0 +1,39 @@ +git-tar-tree(1) +=============== +v0.1, May 2005 + +NAME +---- +git-tar-tree - Creates a tar archive of the files in the named tree + + +SYNOPSIS +-------- +'git-tar-tree' <tree-ish> [ <base> ] + +DESCRIPTION +----------- +Creates a tar archive containing the tree structure for the named tree. +When <base> is specified it is added as a leading path as 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. + + +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 link:git.html[git] suite + diff --git a/Documentation/git-unpack-file.txt b/Documentation/git-unpack-file.txt new file mode 100644 index 0000000000..2f2130d511 --- /dev/null +++ b/Documentation/git-unpack-file.txt @@ -0,0 +1,37 @@ +git-unpack-file(1) +================== +v0.1, May 2005 + +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 link:git.html[git] suite + diff --git a/Documentation/git-unpack-objects.txt b/Documentation/git-unpack-objects.txt new file mode 100644 index 0000000000..20dcbaa593 --- /dev/null +++ b/Documentation/git-unpack-objects.txt @@ -0,0 +1,33 @@ +git-unpack-objects(1) +===================== +v0.1, July 2005 + +NAME +---- +git-unpack-objects - Create a packed archive of objects. + + +SYNOPSIS +-------- +'git-unpack-objects' < pack-file + + +DESCRIPTION +----------- +Reads a packed archive (.pack) from the standard input, and +expands the objects contained in the pack into "one-file +one-object" format in $GIT_OBJECT_DIRECTORY. + + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> + +Documentation +------------- +Documentation by Junio C Hamano + +GIT +--- +Part of the link:git.html[git] suite + diff --git a/Documentation/git-update-cache.txt b/Documentation/git-update-cache.txt new file mode 100644 index 0000000000..7340e89c1f --- /dev/null +++ b/Documentation/git-update-cache.txt @@ -0,0 +1,126 @@ +git-update-cache(1) +=================== +v0.1, May 2005 + +NAME +---- +git-update-cache - Modifies the index or directory cache + + +SYNOPSIS +-------- +'git-update-cache' + [--add] [--remove] [--refresh] [--replace] + [--ignore-missing] + [--force-remove] + [--cacheinfo <mode> <object> <file>]\* + [--info-only] + [--] [<file>]\* + +DESCRIPTION +----------- +Modifies the index or directory cache. Each file mentioned is updated +into the cache and any 'unmerged' or 'needs updating' state is +cleared. + +The way "git-update-cache" handles files it is told about can be modified +using the various options: + +OPTIONS +------- +--add:: + If a specified file isn't in the cache already then it's + added. + Default behaviour is to ignore new files. + +--remove:: + If a specified file is in the cache but is missing then it's + removed. + Default behaviour is to ignore removed file. + +--refresh:: + Looks at the current cache and checks to see if merges or + updates are needed by checking stat() information. + +--ignore-missing:: + Ignores missing files during a --refresh + +--cacheinfo <mode> <object> <path>:: + Directly insert the specified info into the cache. + +--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 cache. + +--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-cache 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. + +--:: + Do not interpret any more arguments as options. + +<file>:: + Files to act on. + Note that files begining 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 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. + +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-cache --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. + +Examples +-------- +To update and refresh only the files already checked out: + + git-checkout-cache -n -f -a && git-update-cache --ignore-missing --refresh + + +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 --git a/Documentation/git-upload-pack.txt b/Documentation/git-upload-pack.txt new file mode 100644 index 0000000000..be597a1787 --- /dev/null +++ b/Documentation/git-upload-pack.txt @@ -0,0 +1,40 @@ +git-upload-pack(1) +================== +v0.1, July 2005 + +NAME +---- +git-upload-pack - Send missing objects packed. + + +SYNOPSIS +-------- +'git-upload-pack' <directory> + +DESCRIPTION +----------- +Invoked by 'git-clone-pack' and/or '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 link:git.html[git] suite diff --git a/Documentation/git-var.txt b/Documentation/git-var.txt new file mode 100644 index 0000000000..45b100694a --- /dev/null +++ b/Documentation/git-var.txt @@ -0,0 +1,60 @@ +git-var(1) +========== +v0.1, July 2005 + +NAME +---- +git-var - Print the git users identity + + +SYNOPSIS +-------- +git-var [ -l | <variable> ] + +DESCRIPTION +----------- +Prints a git logical variable. + +-l causes the logical variables to be listed. + +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 +-------- +link:git-commit-tree.html[git-commit-tree] +link:git-tag-script.html[git-tag-script] + +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 link:git.html[git] suite + diff --git a/Documentation/git-verify-pack.txt b/Documentation/git-verify-pack.txt new file mode 100644 index 0000000000..d1043eb8df --- /dev/null +++ b/Documentation/git-verify-pack.txt @@ -0,0 +1,49 @@ +git-verify-pack(1) +================== +v0.1, June 2005 + +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. 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 link:git.html[git] suite + diff --git a/Documentation/git-write-tree.txt b/Documentation/git-write-tree.txt new file mode 100644 index 0000000000..7f076b4198 --- /dev/null +++ b/Documentation/git-write-tree.txt @@ -0,0 +1,44 @@ +git-write-tree(1) +================= +v0.1, May 2005 + +NAME +---- +git-write-tree - Creates a tree from the current cache + + +SYNOPSIS +-------- +'git-write-tree' + [--missing-ok] + +DESCRIPTION +----------- +Creates a tree object using the current cache. + +The cache must be merged. + +Conceptually, "git-write-tree" sync()s the current directory cache 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-cache" 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. + +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 --git a/Documentation/git.txt b/Documentation/git.txt new file mode 100644 index 0000000000..ffc27d5d7a --- /dev/null +++ b/Documentation/git.txt @@ -0,0 +1,344 @@ +git(7) +====== +May 2005 + +NAME +---- +git - the stupid content tracker + + +SYNOPSIS +-------- +'git-<command>' <args> + +DESCRIPTION +----------- + +This is reference information for the core git commands. + +The Discussion section below contains much useful definition and +clarification info - read that first. And of the commands, I suggest +reading link:git-update-cache.html[git-update-cache] and +link:git-read-tree.html[git-read-tree] first - I wish I had! + +David Greaves <david@dgreaves.com> +08/05/05 + +Updated by Junio C Hamano <junkio@cox.net> on 2005-05-05 to +reflect recent changes. + +Commands Overview +----------------- +The git commands can helpfully be split into those that manipulate +the repository, the cache and the working fileset, those that +interrogate and compare them, and those that moves objects and +references between repositories. + +There are also some ancilliary programs that can be viewed as useful +aids for using the core commands but which are unlikely to be used by +SCMs layered over git. + +Manipulation commands +~~~~~~~~~~~~~~~~~~~~~ +link:git-checkout-cache.html[git-checkout-cache]:: + Copy files from the cache to the working directory + +link:git-commit-tree.html[git-commit-tree]:: + Creates a new commit object + +link:git-init-db.html[git-init-db]:: + Creates an empty git object database + +link:git-merge-base.html[git-merge-base]:: + Finds as good a common ancestor as possible for a merge + +link:git-mktag.html[git-mktag]:: + Creates a tag object + +link:git-read-tree.html[git-read-tree]:: + Reads tree information into the directory cache + +link:git-update-cache.html[git-update-cache]:: + Modifies the index or directory cache + +link:git-hash-object.html[git-hash-object]:: + Computes the object ID from a file. + +link:git-write-tree.html[git-write-tree]:: + Creates a tree from the current cache + +Interrogation commands +~~~~~~~~~~~~~~~~~~~~~~ +link:git-cat-file.html[git-cat-file]:: + Provide content or type information for repository objects + +link:git-check-files.html[git-check-files]:: + Verify a list of files are up-to-date + +link:git-diff-cache.html[git-diff-cache]:: + Compares content and mode of blobs between the cache and repository + +link:git-diff-files.html[git-diff-files]:: + Compares files in the working tree and the cache + +link:git-diff-tree.html[git-diff-tree]:: + Compares the content and mode of blobs found via two tree objects + +link:git-export.html[git-export]:: + Exports each commit and a diff against each of its parents + +link:git-fsck-cache.html[git-fsck-cache]:: + Verifies the connectivity and validity of the objects in the database + +link:git-ls-files.html[git-ls-files]:: + Information about files in the cache/working directory + +link:git-ls-tree.html[git-ls-tree]:: + Displays a tree object in human readable form + +link:git-merge-cache.html[git-merge-cache]:: + Runs a merge for files needing merging + +link:git-rev-list.html[git-rev-list]:: + Lists commit objects in reverse chronological order + +link:git-rev-tree.html[git-rev-tree]:: + Provides the revision tree for one or more commits + +link:git-tar-tree.html[git-tar-tree]:: + Creates a tar archive of the files in the named tree + +link:git-unpack-file.html[git-unpack-file]:: + Creates a temporary file with a blob's contents + +link:git-var.html[git-var]:: + Displays a git logical variable + +link:git-verify-pack.html[git-verify-pack]:: + Validates packed GIT archive files + +The interrogate commands may create files - and you can force them to +touch the working file set - but in general they don't + + +Synching repositories +~~~~~~~~~~~~~~~~~~~~~ + +link:git-clone-script.html[git-clone-script]:: + Clones a repository into the current repository (user interface) + +link:git-clone-pack.html[git-clone-pack]:: + Clones a repository into the current repository (engine + for ssh and local transport) + +link:git-fetch-script.html[git-pull-script]:: + Pull from a repote repository via various protocols + (user interface). + +link:git-pull-script.html[git-pull-script]:: + Fetch from and merge with a repote repository via + various protocols (user interface). + +link:git-http-pull.html[git-http-pull]:: + Downloads a remote GIT repository via HTTP + +link:git-local-pull.html[git-local-pull]:: + Duplicates another GIT repository on a local system + +link:git-ssh-pull.html[git-ssh-pull]:: + Pulls from a remote repository over ssh connection + +link:git-send-pack.html[git-send-pack]:: + Pushes to a remote repository, intelligently. + +link:git-receive-pack.html[git-receive-pack]:: + Invoked by 'git-send-pack' to receive what is pushed to it. + +link:git-clone-pack.html[git-clone-pack]:: + Clones from a remote repository. + +link:git-fetch-pack.html[git-fetch-pack]:: + Updates from a remote repository. + +link:git-upload-pack.html[git-upload-pack]:: + Invoked by 'git-clone-pack' and 'git-fetch-pack' to push + what are asked for. + + +Ancilliary Commands +------------------- +Manipulators: + +link:git-apply-patch-script.html[git-apply-patch-script]:: + Sample script to apply the diffs from git-diff-* + +link:git-convert-cache.html[git-convert-cache]:: + Converts old-style GIT repository + +link:git-merge-one-file-script.html[git-merge-one-file-script]:: + The standard helper program to use with "git-merge-cache" + +link:git-prune-script.html[git-prune-script]:: + Prunes all unreachable objects from the object database + +link:git-resolve-script.html[git-resolve-script]:: + Script used to merge two trees + +link:git-tag-script.html[git-tag-script]:: + An example script to create a tag object signed with GPG + + +Interogators: + +link:git-diff-helper.html[git-diff-helper]:: + Generates patch format output for git-diff-* + +link:git-ssh-push.html[git-ssh-push]:: + Helper "server-side" program used by git-ssh-pull + + + +Identifier Terminology +---------------------- +<object>:: + Indicates the sha1 identifier for any type of object + +<blob>:: + Indicates a blob object sha1 identifier + +<tree>:: + Indicates a tree object sha1 identifier + +<commit>:: + Indicates a commit object sha1 identifier + +<tree-ish>:: + Indicates a tree, commit or tag object sha1 identifier. 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/tag + +<file>:: + Indicates a filename - always relative to the root of + the tree structure GIT_INDEX_FILE describes. + +Symbolic Identifiers +-------------------- +Any git comand accepting any <object> can also use the following +symbolic notation: + +HEAD:: + indicates the head of the repository (ie the contents of + `$GIT_DIR/HEAD`) +<tag>:: + a valid tag 'name'+ + (ie the contents of `$GIT_DIR/refs/tags/<tag>`) +<head>:: + a valid head 'name'+ + (ie the contents of `$GIT_DIR/refs/heads/<head>`) +<snap>:: + a valid snapshot 'name'+ + (ie the contents of `$GIT_DIR/refs/snap/<snap>`) + + +File/Directory Structure +------------------------ +The git-core manipulates the following areas in the directory: + + .git/ The base (overridden with $GIT_DIR) + objects/ The object base (overridden with $GIT_OBJECT_DIRECTORY) + ??/ 'First 2 chars of object' directories. + pack/ Packed archives. + + refs/ Directories containing symbolic names for objects + (each file contains the hex SHA1 + newline) + heads/ Commits which are heads of various sorts + tags/ Tags, by the tag name (or some local renaming of it) + */ Any other subdirectory of refs/ can be used to store + files similar to what are under refs/heads/. + HEAD Symlink to refs/heads/<current-branch-name> + +Higher level SCMs may provide and manage additional information in the +GIT_DIR. + +Terminology +----------- +Each line contains terms which you may see used interchangeably + + object database, .git directory + directory cache, index + id, sha1, sha1-id, sha1 hash + type, tag + + +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 + cache/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 ":" seperated 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 `./.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 link:git-commit-tree.html[git-commit-tree] + +git Diffs +~~~~~~~~~ +'GIT_DIFF_OPTS':: +'GIT_EXTERNAL_DIFF':: + see the "generating patches" section in : + link:git-diff-cache.html[git-diff-cache]; + link:git-diff-files.html[git-diff-files]; + link:git-diff-tree.html[git-diff-tree] + +Discussion +---------- +include::../README[] + +Author +------ +Written by Linus Torvalds <torvalds@osdl.org> and the git-list <git@vger.kernel.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 --git a/Documentation/pack-protocol.txt b/Documentation/pack-protocol.txt new file mode 100644 index 0000000000..7d6aec409d --- /dev/null +++ b/Documentation/pack-protocol.txt @@ -0,0 +1,38 @@ +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/pull-fetch-param.txt b/Documentation/pull-fetch-param.txt new file mode 100644 index 0000000000..7ae4ba0bd8 --- /dev/null +++ b/Documentation/pull-fetch-param.txt @@ -0,0 +1,36 @@ +<repository>:: + The "remote" repository to pull from. One of the + following notations can be used to name the repository + to pull from: + + Rsync URL + rsync://remote.machine/path/to/repo.git/ + + HTTP(s) URL + http://remote.machine/path/to/repo.git/ + + GIT URL + git://remote.machine/path/to/repo.git/ + remote.machine:/path/to/repo.git/ + + Local directory + /path/to/repo.git/ + + In addition to that, as a short-hand, the name of a file + in $GIT_DIR/branches directory can be specified; 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. + +<head>:: + The remote head name to fetch from. That is, make the + objects reachable from the commit recorded in + $GIT_DIR/refs/heads/<head> in the remote repository + available locally. + +tag <tag>:: + The remote head tag to fetch from. That is, make the + objects reachable from the commit recorded in + $GIT_DIR/refs/tags/<tag> in the remote repository + available locally. + diff --git a/Documentation/tutorial.txt b/Documentation/tutorial.txt new file mode 100644 index 0000000000..925ef2c401 --- /dev/null +++ b/Documentation/tutorial.txt @@ -0,0 +1,1105 @@ +A short git tutorial +==================== +May 2005 + + +Introduction +------------ + +This is trying to be a short tutorial on setting up and using a git +archive, 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... + + +Creating a git archive +---------------------- + +Creating a new git archive couldn't be easier: all git archives 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 archive 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-db": + + mkdir git-tutorial + cd git-tutorial + git-init-db + +to which git will reply + + defaulting to local storage area + +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, ls should show you +three entries: + + - a symlink called HEAD, pointing to "refs/heads/master" + + 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 git SHA1 + 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 of your + repository. + + One note: the special "master" head is the default branch, which is + why the .git/HEAD file was created as a symlink 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. + + [ Implementation note: an "object" is identified by its 160-bit SHA1 + hash, aka "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 this refs + subdirectories when you actually start populating your tree ] + +You have now created your first git archive. Of course, since it's +empty, that's not very useful, so let's start populating it with data. + + + Populating a git archive + ------------------------ + +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 archive. We'll start off with a few bad examples, just to +get a feel for how this works: + + echo "Hello World" > a + echo "Silly example" > b + +you have now created two files in your working directory, but to +actually check in your hard work, you will have to go through two steps: + + - fill in the "cache" aka "index" file with the information about your + working directory 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 directory, you use the "git-update-cache" 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 cache +(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-cache --add a b + +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 +store. 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 object with SHA1 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 (ie just a +regular file), and you can see the contents with + + git-cat-file "blob" 557db03de997c86a4a028e1ebd3a1ceb225be238 + +which will print out "Hello World". The object 557db... is nothing +more than the contents of your file "a". + +[ Digression: don't confuse that object with the file "a" itself. The + object is literally just those specific _contents_ of the file, and + however much you later change the contents in file "a", the object we + just looked at will never change. Objects are immutable. ] + +Anyway, as we mentioned previously, you normally never actually take a +look at the objects themselves, and typing long 40-character hex SHA1 +names is not something you'd normally want to do. The above digression +was just to show that "git-update-cache" did something magical, and +actually saved away the contents of your files into the git content +store. + +Updating the cache 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 "a" first: + + echo "It's a new day for git" >> a + +and you can now, since you told git about the previous state of "a", 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 "a" 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 + +which will spit out + + diff --git a/a b/a + --- a/a + +++ b/a + @@ -1 +1,2 @@ + Hello World + +It's a new day for git + +ie the diff of the change we caused by adding another line to "a". + +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. + + + 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 does exactly as I've described) it should be + + 3ede4ed7e895432c0a247f09d71a76db53bd0fa4 + +which is another incomprehensible object name. Again, if you want to, +you can use "git-cat-file -t 3ede4.." 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 archive, and it has no parents, we only need to pass in +the tree ID. However, git-commit-tree also wants to get a commit message +on its standard input, and it will write out the resulting ID for the +commit to its standard output. + +And this is where we start using the .git/HEAD file. The HEAD file is +supposed to contain the reference to the top-of-tree, and since that's +exactly what git-commit-tree spits out, we can do this all with a simple +shell pipeline: + + echo "Initial commit" | git-commit-tree $(git-write-tree) > .git/HEAD + +which will say: + + Committing initial tree 3ede4ed7e895432c0a247f09d71a76db53bd0fa4 + +just to warn you about the fact that it created 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, and you'll never see this "Committing initial tree" +message ever again. + +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 writtten + + git commit + +instead, and it would have done the above magic scripting for you. + + + Making a change + --------------- + +Remember how we did the "git-update-cache" on file "a" and then we +changed "a" afterward, and could compare the new state of "a" 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 "a", not the new ones. We did +that on purpose, to show the difference between the index state, and the +state in the working directory, 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-cache". + +Unlike "git-diff-files", which showed the difference between the index +file and the working directory, "git-diff-cache" shows the differences +between a committed _tree_ and either the the index file or the working +directory. In other words, git-diff-cache 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-cache -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 directory 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-cache" normally compares a tree against the +working directory, but when given the "--cached" flag, it is told to +instead compare against just the index cache contents, and ignore the +current working directory state entirely. Since we just wrote the index +file to HEAD, doing "git-diff-cache --cached -p HEAD" should thus return +an empty set of differences, and that's exactly what it does. + +[ Digression: "git-diff-cache" really always uses the index for its + comparisons, and saying that it compares a tree against the working + directory 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 directory 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 +directory contents", "index file" and "committed tree". We have changes +in the working directory 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-cache a + +(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-xxx versions here. After +we've updated "a" in the index, "git-diff-files -p" now shows no +differences, but "git-diff-cache -p HEAD" still _does_ show that the +current state is different from the state we committed. In fact, now +"git-diff-cache" shows the same difference whether we use the "--cached" +flag or not, since now the index is coherent with the working directory. + +Now, since we've updated "a" 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're doing. + +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 cache), you +can just leave an empty message. Otherwise git-commit-script 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-script 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. + + + Checking it out + --------------- + +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. + +More interestingly, you can also give git-diff-tree the "-v" 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 +activity. + +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. + +[ Side 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. + + +[ Side 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-cache" 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-cache" on them for you. ] + + + Tagging a version + ----------------- + +In git, there's two kinds of tags, a "light" one, and a "signed 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 + + cat .git/HEAD > .git/refs/tags/my-first-tag + +after which point you can 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 a "anchor-point" to see what has changed +since you tagged it. + +A "signed 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 a PGP signature that says that yes, you really did +that tag. You create these signed tags with + + git tag <tagname> + +which will sign the current HEAD (but you can also give it another +argument that specifies the thing to tag, ie you could have tagged the +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 archives + ----------------- + +Git archives are normally totally self-sufficient, and it's worth noting +that 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. + +[ Side 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 directory 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 archive 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 of the project you created. + + - if you want to move or duplicate a git archive, you can do so. There + is "git clone" command, but if all you want to do is just to + create a copy of your archive (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 archive, 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-cache --refresh + + to make sure that the index file is up-to-date in the new one. + +Note that the second point is true even across machines. You can +duplicate a remote git archive 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-cache" with a + + git-read-tree --reset HEAD + git-update-cache --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-cache +makes sure to match up all index entries with the checked-out files). + +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, and you can learn things by just looking +at what the git-*-script scripts do ("git reset" is the above two lines +implemented in "git-reset-script", but some things like "git status" and +"git commit" are slightly more complex scripts around the basic git +commands). + +NOTE! 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/ my-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_directory_ files to work on. To get +those, you'd check them out with + + git-checkout-cache -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-cache 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 space 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 archive we +used earlier, and create a branch in it. You literally do that by just +creating a new SHA1 reference file, and switch to it by just making the +HEAD pointer point to it: + + cat .git/HEAD > .git/refs/heads/mybranch + ln -sf refs/heads/mybranch .git/HEAD + +and you're done. + +Now, if you make the decision to start your new branch at some other +point in the history than the current HEAD, you usually also want to +actually switch the contents of your working directory to that point +when you switch the head, and "git checkout" will do that for you: +instead of switching the branch by hand with "ln -sf", you can just do + + git checkout mybranch + +which will basically "jump" to the branch specified, update your working +directory to that state, and also make it become the new default HEAD. + +You can always just jump back to your original "master" branch by doing + + git checkout master + +and if you forget which branch you happen to be on, a simple + + ls -l .git/HEAD + +will tell you where it's pointing. + + + 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" >> a + git commit a + +Here, we just added another line to "a", and we used a shorthand for +both going a "git-update-cache a" and "git commit" by just giving the +filename directly to "git commit". + +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 "a", 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" >> a + echo "Lots of fun" >> b + git commit a b + +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 resolve", which wants to know which branches you want +to resolve and what the merge is all about: + + git resolve HEAD mybranch "Merge work in mybranch" + +where the third 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 "b" +file, which had no differences in the "mybranch" branch), and say: + + Simple merge failed, trying Automatic merge + Auto-merging a. + merge: warning: conflicts during merge + ERROR: Merge conflict in a. + fatal: merge program failed + Automatic merge failed, fix up by hand + +which is way too verbose, but it basically tells you that it failed the +really trivial merge ("Simple merge") and did an "Automatic merge" +instead, but that too failed due to conflicts in "a". + +Not to worry. It left the (trivial) conflict in "a" in the same form you +should already be well used to if you've ever used CVS, so let's just +open "a" in our editor (whatever that may be), and fix it up somehow. +I'd suggest just making it so that "a" 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 a + +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. Notive 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. + + + 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 resolve". 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 resolve". + +It's such a common thing to do 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. + +The "remote" repository can even be on the same machine. One of +the following notations can be used to name the repository to +pull from: + + Rsync URL + rsync://remote.machine/path/to/repo.git/ + + HTTP(s) URL + http://remote.machine/path/to/repo.git/ + + GIT URL + git://remote.machine/path/to/repo.git/ + remote.machine:/path/to/repo.git/ + + Local directory + /path/to/repo.git/ + +[ Side Note: currently, HTTP transport is slightly broken in + that when the remote repository is "packed" they do not always + work. But we have not talked about packing repository yet, so + let's not worry too much about it for now. ] + +[ Digression: 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 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 a file under .git/branches/ +directory, like this: + + mkdir -p .git/branches + echo rsync://kernel.org/pub/scm/git/git.git/ \ + >.git/branches/linus + +and use the filenae to "git pull" instead of the full URL. +The contents of a file under .git/branches can even be a prefix +of a full URL, like this: + + echo rsync://kernel.org/pub/.../jgarzik/ + >.git/branches/jgarzik + +Examples. + + (1) git pull linus + (2) git pull linus tag v0.99.1 + (3) git pull jgarzik/netdev-2.6.git/ e100 + +the above are equivalent to: + + (1) git pull rsync://kernel.org/pub/scm/git/git.git/ HEAD + (2) git pull rsync://kernel.org/pub/scm/git/git.git/ tag v0.99.1 + (3) git pull rsync://kernel.org/pub/.../jgarzik/netdev-2.6.git e100 + + + 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 directory 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 directory are in good +shape, update the public repository from it. This is often +called "pushing". + +[ Side note: this public repository could further be mirrored, + and that is how kernel.org git repositories are done. ] + +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. + +[ Digression: "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-db, but this time, since it's name is not the usual +".git", we do things slightly differently: + + GIT_DIR=my-git.git git-init-db + +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. + +[ Side 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. ] + +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/ + + +[ Digression: your GIT "public" repository people can pull from + is different from a public CVS repository that lets read-write + access to multiple developers. It is a copy of _your_ primary + repository published for others to use, and you should not + push into it from more than one repository (this means, not + just disallowing other developers to push into it, but also + you should push into it from a single repository of yours). + Sharing the result of work done by multiple people are always + done by pulling (i.e. fetching and merging) from public + repositories of those people. Typically this is done by the + "project lead" person, and the resulting repository is + published as the public repository of the "project lead" for + everybody to base further changes on. ] + + + Packing your repository + ----------------------- + +Earlier, we saw that one file under .git/objects/??/ directory +is stored for each git object you create. This representation +is convenient and efficient to create atomically and safely, but +not so 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. + +[ Side 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. + +[ Side Note: as we already mentioned, "git pull" is broken for + some transports dealing with packed repositories right now, so + do not run "git prune-packed" if you plan to give "git pull" + access via HTTP transport for now. ] + +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 +archive 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 is usually stored +unpacked in the destination, unless rsync transport is used. + + + 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 Randy +Dunlap's presentation (http://tinyurl.com/a2jdg). + +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. + + (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-packed" 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" that works +on that project and has own "public repository" goes like this: + + (1) Prepare your work repository, by "git clone" the public + repository of the "project lead". + + (2) Prepare a public repository accessible to others. + + (3) Copy over the packed files from "project lead" public + repository to your public repository by hand; this part is + currently not automated. + + (4) Push into the public repository from your primary + repository. Run "git repack" (and possibly "git + prune-packed" 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 repositories, by "git clone" the public + repository of the "project lead" (or "subsystem + maintainer", if you work on a subsystem). + + (2) Copy .git/refs/master to .git/refs/upstream. + + (3) Do your work there. Make commits. + + (4) Run "git fetch" 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/FETCH_HEAD. Copy it in + .git/refs/heads/upstream. + + (5) Use "git cherry" to see which ones of your patches were + accepted, and/or use "git rebase" to port your unmerged + changes forward to the updated upstream. + + (6) Use "git format-patch upstream" to prepare patches for + e-mail submission to your upstream and send it out. + Go back to step (3) and continue. + +[Side Note: I think Cogito calls this upstream "origin". + Somebody care to confirm or deny? ] + + +[ to be continued.. cvsimports ] diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000000..101485d403 --- /dev/null +++ b/INSTALL @@ -0,0 +1,64 @@ + + 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 install + +(or prefix=/usr/local, of course). Some day somebody may send me a RPM +spec file or something, and you can do "make rpm" or whatever. + +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-script" + 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. + + - 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-optimized one too - see the Makefile), and you + can avoid the bignum support by excising git-rev-list support + for "--merge-order" (by hand). + + - "libcurl". git-http-pull uses this. You can disable building of + that program if you just want to get started. + + - "GNU diff" to generate patches. Of course, you don't _have_ to + generate patches if you don't want to, but let's face it, you'll + be wanting to. Or why did you get git in the first place? + + Non-GNU versions of the diff/patch programs don't generally support + the unified patch format (which is the one git uses), so you + really do want to get the GNU one. Trust me, you will want to + do that even if it wasn't for git. There's no point in living + in the dark ages any more. + + - "merge", the standard UNIX three-way merge program. It usually + comes with the "rcs" package on most Linux distributions, so if + you have a developer install you probably have it already, but a + "graphical user desktop" install might have left it out. + + You'll only need the merge program if you do development using + git, and if you only use git to track other peoples work you'll + never notice the lack of it. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..bf3cd8d730 --- /dev/null +++ b/Makefile @@ -0,0 +1,207 @@ +# -DCOLLISION_CHECK if you believe that SHA1's +# 1461501637330902918203684832716283019655932542976 hashes do not give you +# enough guarantees about no collisions between objects ever hapenning. +# +# -DUSE_NSEC if you want git to care about sub-second file mtimes and ctimes. +# -DUSE_STDEV if you want git to care about st_dev changing +# +# Note that you need some new 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). +GIT_VERSION=0.99.1 + +COPTS=-O2 +CFLAGS=-g $(COPTS) -Wall + +prefix=$(HOME) +bin=$(prefix)/bin +# dest= + +CC=gcc +AR=ar +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__ + +SCRIPTS=git git-apply-patch-script git-merge-one-file-script git-prune-script \ + git-pull-script git-tag-script git-resolve-script git-whatchanged \ + git-fetch-script git-status-script git-commit-script \ + git-log-script git-shortlog git-cvsimport-script git-diff-script \ + git-reset-script git-add-script git-checkout-script git-clone-script \ + gitk git-cherry git-rebase-script git-relink-script git-repack-script \ + git-format-patch-script git-sh-setup-script git-push-script \ + git-branch-script git-parse-remote + +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 \ + 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-ssh-push git-ssh-pull git-rev-list git-mktag \ + git-diff-helper git-tar-tree git-local-pull git-hash-object \ + git-get-tar-commit-id git-apply git-stripspace \ + git-diff-stages git-rev-parse git-patch-id git-pack-objects \ + git-unpack-objects git-verify-pack git-receive-pack git-send-pack \ + git-prune-packed git-fetch-pack git-upload-pack git-clone-pack \ + git-show-index git-daemon git-var + +all: $(PROG) + +install: $(PROG) $(SCRIPTS) + $(INSTALL) -m755 -d $(dest)$(bin) + $(INSTALL) $(PROG) $(SCRIPTS) $(dest)$(bin) + +LIB_OBJS=read-cache.o sha1_file.o usage.o object.o commit.o tree.o blob.o \ + tag.o date.o index.o diff-delta.o patch-delta.o entry.o path.o \ + epoch.o refs.o csum-file.o pack-check.o pkt-line.o connect.o ident.o +LIB_FILE=libgit.a +LIB_H=cache.h object.h blob.h tree.h commit.h tag.h delta.h epoch.h csum-file.h \ + pack.h pkt-line.h refs.h + +LIB_H += strbuf.h +LIB_OBJS += strbuf.o + +LIB_H += quote.h +LIB_OBJS += quote.o + +LIB_H += diff.h count-delta.h +LIB_OBJS += diff.o diffcore-rename.o diffcore-pickaxe.o diffcore-pathspec.o \ + count-delta.o diffcore-break.o diffcore-order.o + +LIB_OBJS += gitenv.o + +LIBS = $(LIB_FILE) +LIBS += -lz + +ifdef MOZILLA_SHA1 + SHA1_HEADER="mozilla-sha1/sha1.h" + LIB_OBJS += mozilla-sha1/sha1.o +else +ifdef PPC_SHA1 + SHA1_HEADER="ppc/sha1.h" + LIB_OBJS += ppc/sha1.o ppc/sha1ppc.o +else + SHA1_HEADER=<openssl/sha.h> + LIBS += -lcrypto +endif +endif + +CFLAGS += '-DSHA1_HEADER=$(SHA1_HEADER)' + +$(LIB_FILE): $(LIB_OBJS) + $(AR) rcs $@ $(LIB_OBJS) + +check: + for i in *.c; do sparse $(CFLAGS) $(SPARSE_FLAGS) $$i; done + +test-date: test-date.c date.o + $(CC) $(CFLAGS) -o $@ test-date.c date.o + +test-delta: test-delta.c diff-delta.o patch-delta.o + $(CC) $(CFLAGS) -o $@ $^ + +git-%: %.c $(LIB_FILE) + $(CC) $(CFLAGS) -o $@ $(filter %.c,$^) $(LIBS) + +git-update-cache: update-cache.c +git-diff-files: diff-files.c +git-init-db: init-db.c +git-write-tree: write-tree.c +git-read-tree: read-tree.c +git-commit-tree: commit-tree.c +git-cat-file: cat-file.c +git-fsck-cache: fsck-cache.c +git-checkout-cache: checkout-cache.c +git-diff-tree: diff-tree.c +git-rev-tree: rev-tree.c +git-ls-files: ls-files.c +git-check-files: check-files.c +git-ls-tree: ls-tree.c +git-merge-base: merge-base.c +git-merge-cache: merge-cache.c +git-unpack-file: unpack-file.c +git-export: export.c +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-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 +git-tar-tree: tar-tree.c +git-hash-object: hash-object.c +git-stripspace: stripspace.c +git-diff-stages: diff-stages.c +git-rev-parse: rev-parse.c +git-patch-id: patch-id.c +git-pack-objects: pack-objects.c +git-unpack-objects: unpack-objects.c +git-verify-pack: verify-pack.c +git-receive-pack: receive-pack.c +git-send-pack: send-pack.c +git-prune-packed: prune-packed.c +git-fetch-pack: fetch-pack.c +git-var: var.c + +git-http-pull: LIBS += -lcurl +git-rev-list: LIBS += -lssl + +# Library objects.. +blob.o: $(LIB_H) +tree.o: $(LIB_H) +commit.o: $(LIB_H) +tag.o: $(LIB_H) +object.o: $(LIB_H) +read-cache.o: $(LIB_H) +sha1_file.o: $(LIB_H) +usage.o: $(LIB_H) +strbuf.o: $(LIB_H) +gitenv.o: $(LIB_H) +entry.o: $(LIB_H) +diff.o: $(LIB_H) diffcore.h +diffcore-rename.o : $(LIB_H) diffcore.h +diffcore-pathspec.o : $(LIB_H) diffcore.h +diffcore-pickaxe.o : $(LIB_H) diffcore.h +diffcore-break.o : $(LIB_H) diffcore.h +diffcore-order.o : $(LIB_H) diffcore.h +epoch.o: $(LIB_H) + +git-core.spec: git-core.spec.in Makefile + sed -e 's/@@VERSION@@/$(GIT_VERSION)/g' < $< > $@ + +GIT_TARNAME=git-core-$(GIT_VERSION) +dist: git-core.spec git-tar-tree + ./git-tar-tree HEAD $(GIT_TARNAME) > $(GIT_TARNAME).tar + @mkdir -p $(GIT_TARNAME) + @cp git-core.spec $(GIT_TARNAME) + tar rf $(GIT_TARNAME).tar $(GIT_TARNAME)/git-core.spec + @rm -rf $(GIT_TARNAME) + gzip -f -9 $(GIT_TARNAME).tar + +rpm: dist + $(RPMBUILD) -ta git-core-$(GIT_VERSION).tar.gz + +test: all + $(MAKE) -C t/ all + +doc: + $(MAKE) -C Documentation all + +install-doc: + $(MAKE) -C Documentation install + +clean: + rm -f *.o mozilla-sha1/*.o ppc/*.o $(PROG) $(LIB_FILE) + rm -f git-core-*.tar.gz git-core.spec + $(MAKE) -C Documentation/ clean + +backup: clean + cd .. ; tar czvf dircache.tar.gz dir-cache diff --git a/README b/README new file mode 100644 index 0000000000..80cc27913e --- /dev/null +++ b/README @@ -0,0 +1,482 @@ +//////////////////////////////////////////////////////////////// + + 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 stupid (but extremely fast) directory content manager. It +doesn't do a whole lot, 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 tag +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 tag, 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. +(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 tag 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-cache" 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 it's 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 link:git-update-cache.html[git-update-cache] +is run, and it's data can be accessed by link:git-cat-file.html[git-cat-file]. + +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 link:git-write-tree.html[git-write-tree] and +it's data can be accessed by link:git-ls-tree.html[git-ls-tree] + +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 chane 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 link:git-commit-tree.html[git-commit-tree] and +it's data can be accessed by link:git-cat-file.html[git-cat-file] + +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 link:git-mktag.html[git-mktag] and +it's data can be accessed by link:git-cat-file.html[git-cat-file] + + +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 directory 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 link:git-update-cache.html[git-update-cache] command. You +generally update the index information by just specifying the filename +you want to update, like so: + + git-update-cache 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 in the archive, 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-cache --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-cache"). + +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-cache filename + +or, if you want to check out all of the index, use "-a". + +NOTE! git-checkout-cache 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 ".git/HEAD", so that we can always see what the +last committed state was. + +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 +link:git-cat-file.html[git-cat-file] 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 <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 $(cat .git/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 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 <origtree> <target1tree> <target2tree> + +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". + +NOTE! Because the merge is done in the index file, and not in your +working directory, your working directory will no longer match your +index. You can use "git-checkout-cache -f -a" to make the effect of +the merge be seen in your working directory. + +NOTE2! 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. + + +[ fixme: talk about resolving merges here ] diff --git a/apply.c b/apply.c new file mode 100644 index 0000000000..a3474b788b --- /dev/null +++ b/apply.c @@ -0,0 +1,1470 @@ +/* + * apply.c + * + * Copyright (C) Linus Torvalds, 2005 + * + * This applies patches on top of some (arbitrary) version of the SCM. + * + * NOTE! It does all its work in the index file, and only cares about + * the files in the working directory if you tell it to "merge" the + * patch apply. + * + * Even when merging it always takes the source from the index, and + * uses the working tree as a "branch" for a 3-way merge. + */ +#include <ctype.h> + +#include "cache.h" + +// We default to the merge behaviour, since that's what most people would +// expect. +// +// --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 +// --show-files shows the directory changes +// +static int merge_patch = 1; +static int check_index = 0; +static int write_index = 0; +static int diffstat = 0; +static int summary = 0; +static int check = 0; +static int apply = 1; +static int show_files = 0; +static const char apply_usage[] = +"git-apply [--no-merge] [--stat] [--summary] [--check] [--index] [--apply] [--show-files] <patch>..."; + +/* + * 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; + +struct fragment { + unsigned long oldpos, oldlines; + unsigned long newpos, newlines; + const char *patch; + int size; + struct fragment *next; +}; + +struct patch { + char *new_name, *old_name, *def_name; + unsigned int old_mode, new_mode; + int is_rename, is_copy, is_new, is_delete; + int lines_added, lines_deleted; + int score; + struct fragment *fragments; + char *result; + unsigned long resultsize; + struct patch *next; +}; + +#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 = read(fd, buffer + size, nr); + if (!nr) + break; + if (nr < 0) { + if (errno == EAGAIN) + continue; + 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(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; + + 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) +{ + int p_value = 1; + 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) +{ + int len; + const char *name; + + if (!orig_name && !isnull) + return find_name(line, NULL, 1, 0); + + name = "/dev/null"; + len = 9; + if (orig_name) { + name = orig_name; + len = strlen(name); + if (isnull) + die("git-apply: bad git-diff - expected /dev/null, got %s on line %d", name, linenr); + } + + if (*name == '/') + goto absolute_path; + + for (;;) { + char c = *line++; + if (c == '\n') + break; + if (c != '/') + continue; +absolute_path: + if (memcmp(line, name, len) || line[len] != '\n') + break; + return orig_name; + } + die("git-apply: bad git-diff - inconsistent %s filename on line %d", oldnew, 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; +} + +/* + * 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 char *git_header_name(char *line) +{ + int len; + char *name, *second; + + /* + * Find the first '/' + */ + name = line; + for (;;) { + char c = *name++; + if (c == '\n') + return NULL; + if (c == '/') + break; + } + + /* + * We don't accept absolute paths (/dev/null) as possibly valid + */ + if (name == line+1) + return NULL; + + /* + * Accept a name only if it shows up twice, exactly the same + * form. + */ + for (len = 0 ; ; len++) { + char c = name[len]; + + switch (c) { + default: + continue; + case '\n': + break; + 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 + strlen("diff --git ")); + + 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 }, + { "", gitdiff_unrecognized }, + }; + int i; + + len = linelen(line, size); + if (!len || line[len-1] != '\n') + break; + for (i = 0; i < sizeof(optable) / sizeof(optable[0]); 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 = *p1; + 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 fragmants. + * 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; + error("patch fragment without header at line %d: %.*s", linenr, 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; +} + +/* + * 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; + + offset = parse_fragment_header(line, len, fragment); + if (offset < 0) + return -1; + oldlines = fragment->oldlines; + newlines = fragment->newlines; + + if (patch->is_new < 0) { + patch->is_new = !oldlines; + if (!oldlines) + patch->old_name = NULL; + } + if (patch->is_delete < 0) { + patch->is_delete = !newlines; + if (!newlines) + patch->new_name = NULL; + } + + if (patch->is_new != !oldlines) + return error("new file depends on old contents"); + if (patch->is_delete != !newlines) { + if (newlines) + return error("deleted file still has contents"); + fprintf(stderr, "** warning: file %s becomes empty but is not deleted\n", patch->new_name); + } + + /* Parse the thing.. */ + line += len; + size -= len; + linenr++; + added = deleted = 0; + for (offset = len; size > 0; 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 ' ': + oldlines--; + newlines--; + break; + case '-': + deleted++; + oldlines--; + break; + case '+': + added++; + newlines--; + break; + /* We allow "\ No newline at end of file" */ + case '\\': + if (len < 12 || memcmp(line, "\\ No newline", 12)) + return -1; + break; + } + } + patch->lines_added += added; + patch->lines_deleted += deleted; + return offset; +} + +static int parse_single_patch(char *line, unsigned long size, struct patch *patch) +{ + unsigned long offset = 0; + struct fragment **fragp = &patch->fragments; + + while (size > 4 && !memcmp(line, "@@ -", 4)) { + struct fragment *fragment; + int len; + + fragment = xmalloc(sizeof(*fragment)); + memset(fragment, 0, 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; + + *fragp = fragment; + fragp = &fragment->next; + + offset += len; + line += len; + size -= len; + } + return offset; +} + +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); + + return offset + hdrsize + patchsize; +} + +static const char pluses[] = "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"; +static const char minuses[]= "----------------------------------------------------------------------"; + +static void show_stats(struct patch *patch) +{ + char *name = patch->new_name; + int len, max, add, del, total; + + if (!name) + name = patch->old_name; + + /* + * "scale" the filename + */ + len = strlen(name); + max = max_len; + if (max > 50) + max = 50; + if (len > max) + name += len - max; + 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; + } + printf(" %-*s |%5d %.*s%.*s\n", + len, name, patch->lines_added + patch->lines_deleted, + add, pluses, del, minuses); +} + +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 = read(fd, buf + got, size - got); + if (ret < 0) { + if (errno == EAGAIN) + continue; + break; + } + if (!ret) + 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 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; + return try; + } + + /* + * We should start searching forward and backward. + */ + return -1; +} + +struct buffer_desc { + char *buffer; + unsigned long size; + unsigned long alloc; +}; + +static int apply_one_fragment(struct buffer_desc *desc, struct fragment *frag) +{ + char *buf = desc->buffer; + const char *patch = frag->patch; + int offset, size = frag->size; + char *old = xmalloc(size); + char *new = xmalloc(size); + int oldsize = 0, newsize = 0; + + while (size > 0) { + 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--; + switch (*patch) { + case ' ': + case '-': + memcpy(old + oldsize, patch + 1, plen); + oldsize += plen; + if (*patch == '-') + break; + /* Fall-through for ' ' */ + case '+': + memcpy(new + newsize, patch + 1, plen); + newsize += plen; + break; + case '@': case '\\': + /* Ignore it, we already handled it */ + break; + default: + return -1; + } + patch += len; + size -= len; + } + + offset = find_offset(buf, desc->size, old, oldsize, frag->newpos); + if (offset >= 0) { + int diff = newsize - oldsize; + unsigned long size = desc->size + diff; + unsigned long alloc = desc->alloc; + + 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, new, newsize); + offset = 0; + } + + free(old); + free(new); + return offset; +} + +static int apply_fragments(struct buffer_desc *desc, struct patch *patch) +{ + struct fragment *frag = patch->fragments; + + while (frag) { + if (apply_one_fragment(desc, frag) < 0) + return error("patch failed: %s:%d", patch->old_name, frag->oldpos); + frag = frag->next; + } + return 0; +} + +static int apply_data(struct patch *patch, struct stat *st) +{ + char *buf; + unsigned long size, alloc; + struct buffer_desc desc; + + size = 0; + alloc = 0; + buf = NULL; + 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; + patch->result = desc.buffer; + patch->resultsize = desc.size; + + if (patch->is_delete && patch->resultsize) + return error("removal patch leaves file contents"); + + return 0; +} + +static int check_patch(struct patch *patch) +{ + struct stat st; + const char *old_name = patch->old_name; + const char *new_name = patch->new_name; + + if (old_name) { + int changed; + + if (lstat(old_name, &st) < 0) + return error("%s: %s", old_name, strerror(errno)); + 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); + changed = ce_match_stat(active_cache[pos], &st); + if (changed) + return error("%s: does not match index", old_name); + } + if (patch->is_new < 0) + patch->is_new = 0; + st.st_mode = ntohl(create_ce_mode(st.st_mode)); + if (!patch->old_mode) + patch->old_mode = st.st_mode; + if ((st.st_mode ^ patch->old_mode) & S_IFMT) + return error("%s: wrong type", old_name); + if (st.st_mode != patch->old_mode) + fprintf(stderr, "warning: %s has type %o, expected %o\n", + old_name, st.st_mode, patch->old_mode); + } + + if (new_name && (patch->is_new | patch->is_rename | patch->is_copy)) { + if (check_index && cache_name_pos(new_name, strlen(new_name)) >= 0) + return error("%s: already exists in index", new_name); + if (!lstat(new_name, &st)) + return error("%s: already exists in working directory", new_name); + if (errno != ENOENT) + return error("%s: %s", new_name, strerror(errno)); + if (!patch->new_mode) + patch->new_mode = S_IFREG | 0644; + } + + 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) < 0) + return error("%s: patch does not apply", old_name); + return 0; +} + +static int check_patch_list(struct patch *patch) +{ + int error = 0; + + for (;patch ; patch = patch->next) + error |= check_patch(patch); + return error; +} + +static void show_file(int c, unsigned int mode, const char *name) +{ + printf("%c %o %s\n", c, mode, name); +} + +static void show_file_list(struct patch *patch) +{ + for (;patch ; patch = patch->next) { + if (patch->is_rename) { + show_file('-', patch->old_mode, patch->old_name); + show_file('+', patch->new_mode, patch->new_name); + continue; + } + if (patch->is_copy || patch->is_new) { + show_file('+', patch->new_mode, patch->new_name); + continue; + } + if (patch->is_delete) { + show_file('-', patch->old_mode, patch->old_name); + continue; + } + if (patch->old_mode && patch->new_mode && patch->old_mode != patch->new_mode) { + printf("M %o:%o %s\n", patch->old_mode, patch->new_mode, patch->old_name); + continue; + } + printf("M %o %s\n", patch->old_mode, patch->old_name); + } +} + +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 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 = strlen(patch->old_name); + if (len > max_len) + max_len = len; + } + if (patch->new_name) { + int 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); + } + unlink(patch->old_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 = xmalloc(ce_size); + memset(ce, 0, ce_size); + memcpy(ce->name, path, namelen); + ce->ce_mode = create_ce_mode(mode); + ce->ce_flags = htons(namelen); + 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", 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 void create_subdirectories(const char *path) +{ + 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) < 0) { + if (errno != EEXIST) + break; + } + } + free(buf); +} + +static int try_create_file(const char *path, unsigned int mode, const char *buf, unsigned long size) +{ + int fd; + + if (S_ISLNK(mode)) + return symlink(buf, path); + fd = open(path, O_CREAT | O_EXCL | O_WRONLY | O_TRUNC, (mode & 0100) ? 0777 : 0666); + if (fd < 0) + return -1; + while (size) { + int written = write(fd, buf, size); + if (written < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + 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(const char *path, unsigned mode, const char *buf, unsigned long size) +{ + if (!try_create_file(path, mode, buf, size)) + return; + + if (errno == ENOENT) { + create_subdirectories(path); + if (!try_create_file(path, mode, buf, size)) + return; + } + + 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; + } + } + die("unable to write file %s mode %o", path, mode); +} + +static void create_file(struct patch *patch) +{ + const 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); +} + +static void write_out_one_result(struct patch *patch) +{ + if (patch->is_delete > 0) { + remove_file(patch); + return; + } + if (patch->is_new > 0 || patch->is_copy) { + create_file(patch); + return; + } + /* + * Rename or modification boils down to the same + * thing: remove the old, write the new + */ + remove_file(patch); + create_file(patch); +} + +static void write_out_results(struct patch *list) +{ + if (!list) + die("No changes"); + + while (list) { + write_out_one_result(list); + list = list->next; + } +} + +static struct cache_file cache_file; + +static int apply_patch(int fd) +{ + int newfd; + unsigned long offset, size; + char *buffer = read_patch_file(fd, &size); + struct patch *list = NULL, **listp = &list; + + if (!buffer) + return -1; + offset = 0; + while (size > 0) { + struct patch *patch; + int nr; + + patch = xmalloc(sizeof(*patch)); + memset(patch, 0, sizeof(*patch)); + nr = parse_chunk(buffer + offset, size, patch); + if (nr < 0) + break; + patch_stats(patch); + *listp = patch; + listp = &patch->next; + offset += nr; + size -= nr; + } + + newfd = -1; + write_index = check_index && apply; + if (write_index) + newfd = hold_index_file_for_update(&cache_file, get_index_file()); + if (check_index) { + if (read_cache() < 0) + die("unable to read index file"); + } + + if ((check || apply) && check_patch_list(list) < 0) + exit(1); + + if (apply) + write_out_results(list); + + if (write_index) { + if (write_cache(newfd, active_cache, active_nr) || + commit_index_file(&cache_file)) + die("Unable to write new cachefile"); + } + + if (show_files) + show_file_list(list); + + if (diffstat) + stat_patch_list(list); + + if (summary) + summary_patch_list(list); + + free(buffer); + return 0; +} + +int main(int argc, char **argv) +{ + int i; + int read_stdin = 1; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + int fd; + + if (!strcmp(arg, "-")) { + apply_patch(0); + read_stdin = 0; + continue; + } + /* NEEDSWORK: this does not do anything at this moment. */ + if (!strcmp(arg, "--no-merge")) { + merge_patch = 0; + continue; + } + if (!strcmp(arg, "--stat")) { + apply = 0; + diffstat = 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, "--apply")) { + apply = 1; + continue; + } + if (!strcmp(arg, "--show-files")) { + show_files = 1; + continue; + } + fd = open(arg, O_RDONLY); + if (fd < 0) + usage(apply_usage); + read_stdin = 0; + apply_patch(fd); + close(fd); + } + if (read_stdin) + apply_patch(0); + return 0; +} diff --git a/blob.c b/blob.c new file mode 100644 index 0000000000..ea52ad5c9d --- /dev/null +++ b/blob.c @@ -0,0 +1,52 @@ +#include "blob.h" +#include "cache.h" +#include <stdlib.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 = xmalloc(sizeof(struct blob)); + memset(ret, 0, sizeof(struct blob)); + created_object(sha1, &ret->object); + ret->object.type = blob_type; + return ret; + } + if (!obj->type) + obj->type = blob_type; + if (obj->type != blob_type) { + error("Object %s is a %s, not a blob", + sha1_to_hex(sha1), 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/cache.h b/cache.h new file mode 100644 index 0000000000..150e144c5e --- /dev/null +++ b/cache.h @@ -0,0 +1,311 @@ +#ifndef CACHE_H +#define CACHE_H + +#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/mman.h> +#include <sys/param.h> +#include <netinet/in.h> +#include <sys/types.h> +#include <dirent.h> + +#include SHA1_HEADER +#include <zlib.h> + +#if ZLIB_VERNUM < 0x1200 +#define deflateBound(c,s) ((s) + (((s) + 7) >> 3) + (((s) + 63) >> 6) + 11) +#endif + +#ifdef DT_UNKNOWN +#define DTYPE(de) ((de)->d_type) +#else +#define DT_UNKNOWN 0 +#define DT_DIR 1 +#define DT_REG 2 +#define DT_LNK 3 +#define DTYPE(de) DT_UNKNOWN +#endif + +#ifdef __GNUC__ +#define NORETURN __attribute__((__noreturn__)) +#else +#define NORETURN +#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. + */ +#define DEFAULT_GIT_PORT 9418 + +/* + * Environment variables transition. + * We accept older names for now but warn. + */ +extern char *gitenv_bc(const char *); +#define gitenv(e) (getenv(e) ? : gitenv_bc(e)) + +/* + * 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[0]; +}; + +#define CE_NAMEMASK (0x0fff) +#define CE_STAGEMASK (0x3000) +#define CE_UPDATE (0x4000) +#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 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; + +#define GIT_DIR_ENVIRONMENT "GIT_DIR" +#define DEFAULT_GIT_DIR_ENVIRONMENT ".git" +#define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" +#define INDEX_ENVIRONMENT "GIT_INDEX_FILE" + +extern char *get_object_directory(void); +extern char *get_refs_directory(void); +extern char *get_index_file(void); + +#define ALTERNATE_DB_ENVIRONMENT "GIT_ALTERNATE_OBJECT_DIRECTORIES" + +#define alloc_nr(x) (((x)+16)*3/2) + +/* Initialize and use the cache information */ +extern int read_cache(void); +extern int write_cache(int newfd, struct cache_entry **cache, int entries); +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 int remove_cache_entry_at(int pos); +extern int remove_file_from_cache(char *path); +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); +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 void fill_stat_cache_info(struct cache_entry *ce, struct stat *st); + +struct cache_file { + struct cache_file *next; + char lockfile[PATH_MAX]; +}; +extern int hold_index_file_for_update(struct cache_file *, const char *path); +extern int commit_index_file(struct cache_file *); +extern void rollback_index_file(struct cache_file *); + +#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, ...); +extern char *git_path(const char *fmt, ...); +extern char *sha1_file_name(const unsigned char *sha1); + +int safe_create_leading_directories(char *path); + +/* Read and unpack a sha1 file into memory, write memory to a sha1 file */ +extern int unpack_sha1_header(z_stream *stream, void *map, unsigned long mapsize, void *buffer, unsigned long size); +extern int parse_sha1_header(char *hdr, char *type, unsigned long *sizep); +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 write_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *return_sha1); +extern char *write_sha1_file_prepare(void *buf, + unsigned long len, + const char *type, + unsigned char *sha1, + unsigned char *hdr, + int *hdrlen); + +extern int check_sha1_signature(const unsigned char *sha1, void *buf, unsigned long size, const char *type); + +/* Read a tree into the cache */ +extern int read_tree(void *buffer, unsigned long size, int stage, const char **paths); + +extern int write_sha1_from_fd(const unsigned char *sha1, int fd); +extern int write_sha1_to_fd(int fd, const unsigned char *sha1); + +extern int has_sha1_pack(const unsigned char *sha1); +extern int has_sha1_file(const unsigned char *sha1); + +/* Convert to/from hex/sha1 representation */ +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! */ + +/* General helper functions */ +extern void usage(const char *err) NORETURN; +extern void die(const char *err, ...) NORETURN; +extern int error(const char *err, ...); + +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); +void parse_date(const char *date, char *buf, int bufsize); +void datestamp(char *buf, int bufsize); + +extern int setup_ident(void); +extern char *get_ident(const char *name, const char *email, const char *date_str); +extern char *git_author_info(void); +extern char *git_committer_info(void); + +static inline void *xmalloc(size_t size) +{ + void *ret = malloc(size); + if (!ret) + die("Out of memory, malloc failed"); + return ret; +} + +static inline void *xrealloc(void *ptr, size_t size) +{ + void *ret = realloc(ptr, size); + 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) + die("Out of memory, calloc failed"); + return ret; +} + +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); + +extern struct alternate_object_database { + char *base; + char *name; +} *alt_odb; +extern void prepare_alt_odb(void); + +extern struct packed_git { + struct packed_git *next; + unsigned long index_size; + unsigned long pack_size; + unsigned int *index_base; + void *pack_base; + unsigned int pack_last_used; + unsigned int pack_use_cnt; + char pack_name[0]; /* something like ".git/objects/pack/xxxxx.pack" */ +} *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]; + char name[0]; +}; + +extern int 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 get_ack(int fd, unsigned char *result_sha1); +extern struct ref **get_remote_heads(int in, struct ref **list, int nr_match, char **match); + +extern void prepare_packed_git(void); +extern int use_packed_git(struct packed_git *); +extern void unuse_packed_git(struct packed_git *); +extern struct packed_git *add_packed_git(char *, 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 int find_pack_entry_one(const unsigned char *, struct pack_entry *, struct packed_git *); +extern void *unpack_entry_gently(struct pack_entry *, char *, unsigned long *); +extern void packed_object_info_detail(struct pack_entry *, char *, unsigned long *, unsigned long *, int *, unsigned char *); + +#endif /* CACHE_H */ diff --git a/cat-file.c b/cat-file.c new file mode 100644 index 0000000000..ef58970d82 --- /dev/null +++ b/cat-file.c @@ -0,0 +1,55 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" + +int main(int argc, char **argv) +{ + unsigned char sha1[20]; + char type[20]; + void *buf; + unsigned long size; + + if (argc != 3 || get_sha1(argv[2], sha1)) + usage("git-cat-file [-t | -s | <type>] <sha1>"); + + if (!strcmp("-t", argv[1]) || !strcmp("-s", argv[1])) { + if (!sha1_object_info(sha1, type, + argv[1][1] == 's' ? &size : NULL)) { + switch (argv[1][1]) { + case 't': + printf("%s\n", type); + break; + case 's': + printf("%lu\n", size); + break; + } + return 0; + } + buf = NULL; + } else { + buf = read_object_with_reference(sha1, argv[1], &size, NULL); + } + + if (!buf) + die("git-cat-file %s: bad file", argv[2]); + + while (size > 0) { + long ret = write(1, buf, size); + if (ret < 0) { + if (errno == EAGAIN) + continue; + /* Ignore epipe */ + if (errno == EPIPE) + break; + die("git-cat-file: %s", strerror(errno)); + } else if (!ret) { + die("git-cat-file: disk full?"); + } + size -= ret; + buf += ret; + } + return 0; +} diff --git a/check-files.c b/check-files.c new file mode 100644 index 0000000000..6fd69e79d6 --- /dev/null +++ b/check-files.c @@ -0,0 +1,47 @@ +/* + * check-files.c + * + * Check that a set of files are up-to-date in the filesystem or + * do not exist. Used to verify a patch target before doing a patch. + * + * Copyright (C) 2005 Linus Torvalds + */ +#include "cache.h" + +static void check_file(const char *path) +{ + int fd = open(path, O_RDONLY); + struct cache_entry *ce; + struct stat st; + int pos, changed; + + /* Nonexistent is fine */ + if (fd < 0) { + if (errno != ENOENT) + die("%s: %s", path, strerror(errno)); + return; + } + + /* Exists but is not in the cache is not fine */ + pos = cache_name_pos(path, strlen(path)); + if (pos < 0) + die("preparing to update existing file '%s' not in cache", path); + ce = active_cache[pos]; + + if (lstat(path, &st) < 0) + die("lstat(%s): %s", path, strerror(errno)); + + changed = ce_match_stat(ce, &st); + if (changed) + die("preparing to update file '%s' not uptodate in cache", path); +} + +int main(int argc, char **argv) +{ + int i; + + read_cache(); + for (i = 1; i < argc ; i++) + check_file(argv[i]); + return 0; +} diff --git a/checkout-cache.c b/checkout-cache.c new file mode 100644 index 0000000000..d6c4ff1042 --- /dev/null +++ b/checkout-cache.c @@ -0,0 +1,150 @@ +/* + * Check-out files from the "current cache directory" + * + * Copyright (C) 2005 Linus Torvalds + * + * Careful: order of argument flags does matter. For example, + * + * git-checkout-cache -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-cache" does nothing. You probably + * meant "git-checkout-cache -a". And if you want to force it, you + * want "git-checkout-cache -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-cache -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 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" + +static struct checkout state = { + .base_dir = "", + .base_dir_len = 0, + .force = 0, + .quiet = 0, + .not_new = 0, + .refresh_cache = 0, +}; + +static int checkout_file(const char *name) +{ + int pos = cache_name_pos(name, strlen(name)); + if (pos < 0) { + if (!state.quiet) { + pos = -pos - 1; + fprintf(stderr, + "git-checkout-cache: %s is %s.\n", + name, + (pos < active_nr && + !strcmp(active_cache[pos]->name, name)) ? + "unmerged" : "not in the cache"); + } + return -1; + } + return checkout_entry(active_cache[pos], &state); +} + +static int checkout_all(void) +{ + int i; + + for (i = 0; i < active_nr ; i++) { + struct cache_entry *ce = active_cache[i]; + if (ce_stage(ce)) + continue; + if (checkout_entry(ce, &state) < 0) + return -1; + } + return 0; +} + +static const char *checkout_cache_usage = +"git-checkout-cache [-u] [-q] [-a] [-f] [-n] [--prefix=<string>] [--] <file>..."; + +int main(int argc, char **argv) +{ + int i, force_filename = 0; + struct cache_file cache_file; + int newfd = -1; + + if (read_cache() < 0) { + die("invalid cache"); + } + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!force_filename) { + if (!strcmp(arg, "-a")) { + checkout_all(); + continue; + } + if (!strcmp(arg, "--")) { + force_filename = 1; + continue; + } + if (!strcmp(arg, "-f")) { + state.force = 1; + continue; + } + if (!strcmp(arg, "-q")) { + state.quiet = 1; + continue; + } + if (!strcmp(arg, "-n")) { + state.not_new = 1; + continue; + } + if (!strcmp(arg, "-u")) { + state.refresh_cache = 1; + if (newfd < 0) + newfd = hold_index_file_for_update + (&cache_file, + get_index_file()); + if (newfd < 0) + die("cannot open index.lock file."); + continue; + } + if (!memcmp(arg, "--prefix=", 9)) { + state.base_dir = arg+9; + state.base_dir_len = strlen(state.base_dir); + continue; + } + if (arg[0] == '-') + usage(checkout_cache_usage); + } + if (state.base_dir_len) { + /* when --prefix is specified we do not + * want to update cache. + */ + if (state.refresh_cache) { + close(newfd); newfd = -1; + rollback_index_file(&cache_file); + } + state.refresh_cache = 0; + } + checkout_file(arg); + } + + if (0 <= newfd && + (write_cache(newfd, active_cache, active_nr) || + commit_index_file(&cache_file))) + die("Unable to write new cachefile"); + return 0; +} diff --git a/clone-pack.c b/clone-pack.c new file mode 100644 index 0000000000..e9c20dee71 --- /dev/null +++ b/clone-pack.c @@ -0,0 +1,182 @@ +#include "cache.h" +#include "refs.h" +#include "pkt-line.h" +#include <sys/wait.h> + +static int quiet; +static const char clone_pack_usage[] = "git-clone-pack [-q] [--exec=<git-upload-pack>] [<host>:]<directory> [<heads>]*"; +static const char *exec = "git-upload-pack"; + +static void clone_handshake(int fd[2], struct ref *ref) +{ + unsigned char sha1[20]; + + while (ref) { + packet_write(fd[1], "want %s\n", sha1_to_hex(ref->old_sha1)); + ref = ref->next; + } + packet_flush(fd[1]); + + /* We don't have nuttin' */ + packet_write(fd[1], "done\n"); + if (get_ack(fd[0], sha1)) + error("Huh! git-clone-pack got positive ack for %s", sha1_to_hex(sha1)); +} + +static int is_master(struct ref *ref) +{ + return !strcmp(ref->name, "refs/heads/master"); +} + +static void write_one_ref(struct ref *ref) +{ + char *path = git_path(ref->name); + int fd; + char *hex; + + if (safe_create_leading_directories(path)) + die("unable to create leading directory for %s", ref->name); + fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0666); + if (fd < 0) + die("unable to create ref %s", ref->name); + hex = sha1_to_hex(ref->old_sha1); + hex[40] = '\n'; + if (write(fd, hex, 41) != 41) + die("unable to write ref %s", ref->name); + close(fd); +} + +static void write_refs(struct ref *ref) +{ + struct ref *head = NULL, *head_ptr, *master_ref; + char *head_path; + + if (!strcmp(ref->name, "HEAD")) { + head = ref; + ref = ref->next; + } + head_ptr = NULL; + master_ref = NULL; + while (ref) { + if (is_master(ref)) + master_ref = ref; + if (head && !memcmp(ref->old_sha1, head->old_sha1, 20)) { + if (!head_ptr || ref == master_ref) + head_ptr = ref; + } + write_one_ref(ref); + ref = ref->next; + } + if (!head) + return; + + head_path = git_path("HEAD"); + if (!head_ptr) { + /* + * If we had a master ref, and it wasn't HEAD, we need to undo the + * symlink, and write a standalone HEAD. Give a warning, because that's + * really really wrong. + */ + if (master_ref) { + error("HEAD doesn't point to any refs! Making standalone HEAD"); + unlink(head_path); + } + write_one_ref(head); + return; + } + + /* We reset to the master branch if it's available */ + if (master_ref) + return; + + /* + * Uhhuh. Other end didn't have master. We start HEAD off with + * the first branch with the same value. + */ + unlink(head_path); + if (symlink(head_ptr->name, head_path) < 0) + die("unable to link HEAD to %s", head_ptr->name); +} + +static int clone_pack(int fd[2], int nr_match, char **match) +{ + struct ref *refs; + int status; + pid_t pid; + + get_remote_heads(fd[0], &refs, nr_match, match); + if (!refs) { + packet_flush(fd[1]); + die("no matching remote head"); + } + clone_handshake(fd, refs); + pid = fork(); + if (pid < 0) + die("git-clone-pack: unable to fork off git-unpack-objects"); + if (!pid) { + dup2(fd[0], 0); + close(fd[0]); + close(fd[1]); + execlp("git-unpack-objects", "git-unpack-objects", + quiet ? "-q" : NULL, NULL); + die("git-unpack-objects exec failed"); + } + close(fd[0]); + close(fd[1]); + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) + die("waiting for git-unpack-objects: %s", strerror(errno)); + } + if (WIFEXITED(status)) { + int code = WEXITSTATUS(status); + if (code) + die("git-unpack-objects died with error code %d", code); + write_refs(refs); + return 0; + } + if (WIFSIGNALED(status)) { + int sig = WTERMSIG(status); + die("git-unpack-objects died of signal %d", sig); + } + die("Sherlock Holmes! git-unpack-objects died of unnatural causes %d!", status); +} + +int main(int argc, char **argv) +{ + int i, ret, nr_heads; + char *dest = NULL, **heads; + int fd[2]; + pid_t pid; + + nr_heads = 0; + heads = NULL; + for (i = 1; i < argc; i++) { + char *arg = argv[i]; + + if (*arg == '-') { + if (!strcmp("-q", arg)) { + quiet = 1; + continue; + } + if (!strncmp("--exec=", arg, 7)) { + exec = arg + 7; + continue; + } + usage(clone_pack_usage); + } + dest = arg; + heads = argv + i + 1; + nr_heads = argc - i - 1; + break; + } + if (!dest) + usage(clone_pack_usage); + pid = git_connect(fd, dest, exec); + if (pid < 0) + return 1; + ret = clone_pack(fd, nr_heads, heads); + close(fd[0]); + close(fd[1]); + finish_connect(pid); + return ret; +} diff --git a/commit-tree.c b/commit-tree.c new file mode 100644 index 0000000000..d545f62aaa --- /dev/null +++ b/commit-tree.c @@ -0,0 +1,131 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" + +#include <pwd.h> +#include <time.h> +#include <ctype.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; + alloc = (size + 32767) & ~32767; + buf = *bufp; + if (newsize > alloc) { + alloc = (newsize + 32767) & ~32767; + buf = xrealloc(buf, alloc); + *bufp = buf; + } + *sizep = newsize; + memcpy(buf + size, one_line, len); +} + +static void check_valid(unsigned char *sha1, const char *expect) +{ + void *buf; + char type[20]; + unsigned long size; + + buf = read_sha1_file(sha1, type, &size); + if (!buf || strcmp(type, expect)) + die("%s is not a valid '%s' object", sha1_to_hex(sha1), expect); + free(buf); +} + +/* + * 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 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 (!memcmp(parent_sha1[i], sha1, 20)) { + error("duplicate parent %s ignored", sha1_to_hex(sha1)); + return 0; + } + } + return 1; +} + +int main(int argc, char **argv) +{ + int i; + int parents = 0; + unsigned char tree_sha1[20]; + unsigned char commit_sha1[20]; + char comment[1000]; + char *buffer; + unsigned int size; + + if (argc < 2 || get_sha1_hex(argv[1], tree_sha1) < 0) + usage(commit_tree_usage); + + check_valid(tree_sha1, "tree"); + for (i = 2; i < argc; i += 2) { + char *a, *b; + a = argv[i]; b = argv[i+1]; + if (!b || strcmp(a, "-p") || get_sha1(b, parent_sha1[parents])) + usage(commit_tree_usage); + check_valid(parent_sha1[parents], "commit"); + if (new_parent(parents)) + parents++; + } + if (!parents) + fprintf(stderr, "Committing initial tree %s\n", argv[1]); + setup_ident(); + + 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()); + add_buffer(&buffer, &size, "committer %s\n\n", git_committer_info()); + + /* And add the comment */ + while (fgets(comment, sizeof(comment), stdin) != NULL) + add_buffer(&buffer, &size, "%s", comment); + + write_sha1_file(buffer, size, "commit", commit_sha1); + printf("%s\n", sha1_to_hex(commit_sha1)); + return 0; +} diff --git a/commit.c b/commit.c new file mode 100644 index 0000000000..caee5bc218 --- /dev/null +++ b/commit.c @@ -0,0 +1,456 @@ +#include <ctype.h> +#include "tag.h" +#include "commit.h" +#include "cache.h" + +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"; + +enum cmit_fmt get_commit_format(const char *arg) +{ + if (!*arg) + return CMIT_FMT_DEFAULT; + if (!strcmp(arg, "=raw")) + return CMIT_FMT_RAW; + if (!strcmp(arg, "=medium")) + return CMIT_FMT_MEDIUM; + if (!strcmp(arg, "=short")) + return CMIT_FMT_SHORT; + if (!strcmp(arg, "=full")) + return CMIT_FMT_FULL; + die("invalid --pretty format"); +} + +static struct commit *check_commit(struct object *obj, const unsigned char *sha1) +{ + if (obj->type != commit_type) { + error("Object %s is a %s, not a commit", + sha1_to_hex(sha1), obj->type); + return NULL; + } + return (struct commit *) obj; +} + +struct commit *lookup_commit_reference(const unsigned char *sha1) +{ + struct object *obj = parse_object(sha1); + + if (!obj) + return NULL; + while (obj->type == tag_type) + obj = parse_object(((struct tag *)obj)->tagged->sha1); + + return check_commit(obj, sha1); +} + +struct commit *lookup_commit(const unsigned char *sha1) +{ + struct object *obj = lookup_object(sha1); + if (!obj) { + struct commit *ret = xmalloc(sizeof(struct commit)); + memset(ret, 0, sizeof(struct commit)); + created_object(sha1, &ret->object); + ret->object.type = commit_type; + return ret; + } + if (!obj->type) + obj->type = commit_type; + return check_commit(obj, sha1); +} + +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; +} + +int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size) +{ + void *bufptr = buffer; + unsigned char parent[20]; + struct commit_list **pptr; + + if (item->object.parsed) + return 0; + item->object.parsed = 1; + get_sha1_hex(bufptr + 5, parent); + item->tree = lookup_tree(parent); + if (item->tree) + add_ref(&item->object, &item->tree->object); + bufptr += 46; /* "tree " + "hex sha1" + "\n" */ + pptr = &item->parents; + while (!memcmp(bufptr, "parent ", 7) && + !get_sha1_hex(bufptr + 7, parent)) { + struct commit *new_parent = lookup_commit(parent); + if (new_parent) { + pptr = &commit_list_insert(new_parent, pptr)->next; + add_ref(&item->object, &new_parent->object); + } + bufptr += 48; + } + item->date = parse_commit_date(bufptr); + 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 (!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; +} + +/* + * 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++; + ret++; + if (c == '\n') + break; + if (!c) + return 0; + } + return ret; +} + +static int add_user_info(const char *what, enum cmit_fmt fmt, char *buf, const char *line) +{ + char *date; + unsigned int namelen; + unsigned long time; + int tz, ret; + + date = strchr(line, '>'); + if (!date) + return 0; + namelen = ++date - line; + time = strtoul(date, &date, 10); + tz = strtol(date, NULL, 10); + + ret = sprintf(buf, "%s: %.*s\n", what, namelen, line); + if (fmt == CMIT_FMT_MEDIUM) + ret += sprintf(buf + ret, "Date: %s\n", show_date(time, tz)); + return ret; +} + +static int is_empty_line(const char *line, int len) +{ + while (len && isspace(line[len-1])) + len--; + return !len; +} + +static int add_parent_info(enum cmit_fmt fmt, char *buf, const char *line, int parents) +{ + int offset = 0; + switch (parents) { + case 1: + break; + case 2: + /* Go back to the previous line: 40 characters of previous parent, and one '\n' */ + offset = sprintf(buf, "Merge: %.40s\n", line-41); + /* Fallthrough */ + default: + /* Replace the previous '\n' with a space */ + buf[offset-1] = ' '; + offset += sprintf(buf + offset, "%.40s\n", line+7); + } + return offset; +} + +unsigned long pretty_print_commit(enum cmit_fmt fmt, const char *msg, unsigned long len, char *buf, unsigned long space) +{ + int hdr = 1, body = 0; + unsigned long offset = 0; + int parents = 0; + + 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; + 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"); + offset += add_parent_info(fmt, buf + offset, line, ++parents); + } + if (!memcmp(line, "author ", 7)) + offset += add_user_info("Author", fmt, buf + offset, line + 7); + if (fmt == CMIT_FMT_FULL) { + if (!memcmp(line, "committer ", 10)) + offset += add_user_info("Commit", fmt, buf + offset, line + 10); + } + continue; + } + + if (is_empty_line(line, linelen)) { + if (!body) + continue; + if (fmt == CMIT_FMT_SHORT) + break; + } else { + body = 1; + } + memset(buf + offset, ' ', 4); + memcpy(buf + offset + 4, line, linelen); + offset += linelen + 4; + } + /* Make sure there is an EOLN */ + if (buf[offset - 1] != '\n') + buf[offset++] = '\n'; + buf[offset] = '\0'; + 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 = 0; + struct commit_list * parents = commit->parents; + for (count=0;parents; parents=parents->next,count++) + ; + return count; +} + +/* + * Performs an in-place topological sort on the list supplied. + */ +void sort_in_topological_order(struct commit_list ** list) +{ + struct commit_list * next = *list; + struct commit_list * work = NULL; + 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++; + } + /* 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; + next->item->object.util = 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 *)parent->object.util; + + 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; + while (next) { + struct sort_node * node = (struct sort_node *)next->item->object.util; + + if (node->indegree == 0) { + commit_list_insert(next->item, &work); + } + next=next->next; + } + /* process the list in topological order */ + while (work) { + struct commit * work_item = pop_commit(&work); + struct sort_node * work_node = (struct sort_node *)work_item->object.util; + struct commit_list * parents = work_item->parents; + + while (parents) { + struct commit * parent=parents->item; + struct sort_node * pn = (struct sort_node *)parent->object.util; + + if (pn) { + /* + * parents are only enqueued for emission + * when all their children have been emitted thereby + * guaranteeing topological order. + */ + pn->indegree--; + if (!pn->indegree) + 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; + work_item->object.util = NULL; + } + free(nodes); +} diff --git a/commit.h b/commit.h new file mode 100644 index 0000000000..c24ab21061 --- /dev/null +++ b/commit.h @@ -0,0 +1,70 @@ +#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; + unsigned long date; + struct commit_list *parents; + struct tree *tree; + char *buffer; +}; + +extern const char *commit_type; + +struct commit *lookup_commit(const unsigned char *sha1); +struct commit *lookup_commit_reference(const unsigned char *sha1); + +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, +}; + +extern enum cmit_fmt get_commit_format(const char *arg); +extern unsigned long pretty_print_commit(enum cmit_fmt fmt, const char *msg, unsigned long len, char *buf, unsigned long space); + +/** 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); + +int count_parents(struct commit * commit); + +/* + * Performs an in-place topological sort of list supplied. + * + * Pre-conditions: + * all commits in input list and all parents of those + * commits must have object.util == NULL + * + * Post-conditions: + * invariant of resulting list is: + * a reachable from b => ord(b) < ord(a) + */ +void sort_in_topological_order(struct commit_list ** list); +#endif /* COMMIT_H */ diff --git a/connect.c b/connect.c new file mode 100644 index 0000000000..6bf737cbfa --- /dev/null +++ b/connect.c @@ -0,0 +1,215 @@ +#include "cache.h" +#include "pkt-line.h" +#include "quote.h" +#include <sys/wait.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <netdb.h> + +/* + * Read all the refs from the other end + */ +struct ref **get_remote_heads(int in, struct ref **list, int nr_match, char **match) +{ + *list = NULL; + for (;;) { + struct ref *ref; + unsigned char old_sha1[20]; + static char buffer[1000]; + char *name; + int 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; + if (nr_match && !path_match(name, nr_match, match)) + continue; + ref = xmalloc(sizeof(*ref) + len - 40); + memcpy(ref->old_sha1, old_sha1, 20); + memset(ref->new_sha1, 0, 20); + memcpy(ref->name, buffer + 41, len - 40); + ref->next = NULL; + *list = ref; + list = &ref->next; + } + return list; +} + +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 ", 3)) { + if (!get_sha1_hex(line+4, result_sha1)) + 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 1; + } + 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; + 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); +} + +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; + } + } + + lookup_host(host, &addr); + ((struct sockaddr_in *)&addr)->sin_port = htons(port); + + 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); + return 0; +} + +/* + * Yeah, yeah, fixme. Need to pass in the heads etc. + */ +int git_connect(int fd[2], char *url, const char *prog) +{ + char command[1024]; + char *host, *path; + char *colon; + int pipefd[2][2]; + pid_t pid; + enum protocol protocol; + + host = NULL; + path = url; + colon = strchr(url, ':'); + protocol = PROTO_LOCAL; + if (colon) { + *colon = 0; + host = url; + path = colon+1; + protocol = PROTO_SSH; + if (!memcmp(path, "//", 2)) { + char *slash = strchr(path + 2, '/'); + if (slash) { + int nr = slash - path - 2; + memmove(path, path+2, nr); + path[nr] = 0; + protocol = get_protocol(url); + host = path; + path = slash; + } + } + } + + if (protocol == PROTO_GIT) + return git_tcp_connect(fd, prog, host, path); + + if (pipe(pipefd[0]) < 0 || pipe(pipefd[1]) < 0) + die("unable to create pipe pair for communication"); + pid = fork(); + if (!pid) { + snprintf(command, sizeof(command), "%s %s", prog, + sq_quote(path)); + 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) + execlp("ssh", "ssh", host, command, NULL); + else + 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]); + return pid; +} + +int finish_connect(pid_t pid) +{ + int ret; + + for (;;) { + ret = waitpid(pid, NULL, 0); + if (!ret) + break; + if (errno != EINTR) + break; + } + return ret; +} diff --git a/convert-cache.c b/convert-cache.c new file mode 100644 index 0000000000..ee599f1c02 --- /dev/null +++ b/convert-cache.c @@ -0,0 +1,341 @@ +#define _XOPEN_SOURCE /* glibc2 needs this */ +#include <time.h> +#include <ctype.h> +#include "cache.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 = xmalloc(sizeof(struct entry)); + memset(new, 0, sizeof(*new)); + memcpy(new->old_sha1, sha1, 20); + 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 = memcmp(sha1, n->old_sha1, 20); + 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); + memcpy(buffer, entry->new_sha1, 20); +} + +static void convert_ascii_sha1(void *buffer) +{ + unsigned char sha1[20]; + struct entry *entry; + + if (get_sha1_hex(buffer, sha1)) + die("bad sha1"); + entry = convert_entry(sha1); + memcpy(buffer, sha1_to_hex(entry->new_sha1), 40); +} + +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"); + 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'; + memcpy(new + newlen, buffer + len - 20, 20); + newlen += 20; + + used += len; + size -= len; + 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 += len; + } + + write_sha1_file(new, newlen, "tree", result_sha1); + free(new); + return used; +} + +static int convert_mode(char *buffer) +{ + char *end; + unsigned short mode = strtoul(buffer, &end, 8); + unsigned short newmode; + char num[10]; + int len; + + if (*end != ' ') + die("corrupt tree object"); + switch (mode) { + case S_IFREG | 0644: + case S_IFREG | 0755: + case S_IFLNK: + case S_IFDIR: + return 0; + } + newmode = 0; + if (S_ISREG(mode)) + newmode = (mode & 0100) ? 0755 : 0644; + newmode |= mode & S_IFMT; + len = sprintf(num, "%o", newmode); + if (len != end - buffer) + return error("unable to convert tree entry mode %o to %o", mode, newmode); + memcpy(buffer, num, len); + return 0; +} + +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_mode(buffer); + convert_binary_sha1(buffer + len); + + len += 20; + if (len > size) + die("corrupt tree object"); + size -= len; + 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 += 46; + size -= 46; + + // "parent <sha1>\n" + while (!memcmp(buffer, "parent ", 7)) { + memcpy(new + newlen, buffer, 48); + newlen += 48; + 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", 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; + + convert_ascii_sha1(buffer+5); + buffer += 46; /* "tree " + "hex sha1" + "\n" */ + while (!memcmp(buffer, "parent ", 7)) { + convert_ascii_sha1(buffer+7); + 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")) { + write_sha1_file(buffer, size, "blob", entry->new_sha1); + } else if (!strcmp(type, "tree")) + convert_tree(buffer, size, entry->new_sha1); + else if (!strcmp(type, "commit")) + 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); + return entry; +} + +int main(int argc, char **argv) +{ + unsigned char sha1[20]; + struct entry *entry; + + if (argc != 2 || get_sha1(argv[1], sha1)) + usage("git-convert-cache <sha1>"); + + entry = convert_entry(sha1); + printf("new sha1: %s\n", sha1_to_hex(entry->new_sha1)); + return 0; +} diff --git a/count-delta.c b/count-delta.c new file mode 100644 index 0000000000..7559ff68b1 --- /dev/null +++ b/count-delta.c @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2005 Junio C Hamano + * The delta-parsing part is almost straight copy of patch-delta.c + * which is (C) 2005 Nicolas Pitre <nico@cam.org>. + */ +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#include "delta.h" +#include "count-delta.h" + +/* + * NOTE. We do not _interpret_ delta fully. As an approximation, we + * just count the number of bytes that are copied from the source, and + * the number of literal data bytes that are inserted. + * + * Number of bytes that are _not_ copied from the source is deletion, + * and number of inserted literal bytes are addition, so sum of them + * is the extent of damage. xdelta can express an edit that copies + * data inside of the destination which originally came from the + * source. We do not count that in the following routine, so we are + * undercounting the source material that remains in the final output + * that way. + */ +int count_delta(void *delta_buf, unsigned long delta_size, + unsigned long *src_copied, unsigned long *literal_added) +{ + unsigned long copied_from_source, added_literal; + const unsigned char *data, *top; + unsigned char cmd; + unsigned long src_size, dst_size, out; + + if (delta_size < DELTA_SIZE_MIN) + return -1; + + data = delta_buf; + top = delta_buf + delta_size; + + src_size = get_delta_hdr_size(&data); + dst_size = get_delta_hdr_size(&data); + + added_literal = copied_from_source = out = 0; + 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 (cp_size == 0) cp_size = 0x10000; + + if (cmd & 0x40) + /* copy from dst */ + ; + else + copied_from_source += cp_size; + out += cp_size; + } else { + /* write literal into dst */ + added_literal += cmd; + out += cmd; + data += cmd; + } + } + + /* sanity check */ + if (data != top || out != dst_size) + return -1; + + /* delete size is what was _not_ copied from source. + * edit size is that and literal additions. + */ + *src_copied = copied_from_source; + *literal_added = added_literal; + return 0; +} diff --git a/count-delta.h b/count-delta.h new file mode 100644 index 0000000000..7359629827 --- /dev/null +++ b/count-delta.h @@ -0,0 +1,10 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#ifndef COUNT_DELTA_H +#define COUNT_DELTA_H + +int count_delta(void *, unsigned long, + unsigned long *src_copied, unsigned long *literal_added); + +#endif diff --git a/csum-file.c b/csum-file.c new file mode 100644 index 0000000000..907efbf860 --- /dev/null +++ b/csum-file.c @@ -0,0 +1,147 @@ +/* + * 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 int sha1flush(struct sha1file *f, unsigned int count) +{ + void *buf = f->buffer; + + for (;;) { + int ret = write(f->fd, buf, count); + if (ret > 0) { + buf += ret; + count -= ret; + if (count) + continue; + return 0; + } + if (!ret) + die("sha1 file '%s' write error. Out of diskspace", f->name); + if (errno == EAGAIN || errno == EINTR) + continue; + 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) + memcpy(result, f->buffer, 20); + if (update) + sha1flush(f, 20); + if (close(f->fd)) + die("%s: sha1 file error on close (%s)", f->name, strerror(errno)); + 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 += 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, Z_DEFAULT_COMPRESSION); + 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..776cfb152a --- /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, ...); +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/daemon.c b/daemon.c new file mode 100644 index 0000000000..0ae9af0ad4 --- /dev/null +++ b/daemon.c @@ -0,0 +1,287 @@ +#include "cache.h" +#include "pkt-line.h" +#include <signal.h> +#include <sys/wait.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +static const char daemon_usage[] = "git-daemon [--inetd | --port=n]"; + +static int upload(char *dir, int dirlen) +{ + if (chdir(dir) < 0) + return -1; + chdir(".git"); + + /* + * 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. + */ + if (access("git-daemon-export-ok", F_OK) || + access("objects/00", X_OK) || + access("HEAD", R_OK)) + return -1; + + /* + * We'll ignore SIGTERM from now on, we have a + * good client. + */ + signal(SIGTERM, SIG_IGN); + + /* git-upload-pack only ever reads stuff, so this is safe */ + execlp("git-upload-pack", "git-upload-pack", ".", NULL); + return -1; +} + +static int execute(void) +{ + static char line[1000]; + int len; + + len = packet_read_line(0, line, sizeof(line)); + + if (len && line[len-1] == '\n') + line[--len] = 0; + + if (!strncmp("git-upload-pack /", line, 17)) + return upload(line + 16, len - 16); + + fprintf(stderr, "got bad connection '%s'\n", 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 = 0; +pid_t dead_child[MAX_CHILDREN]; + +/* These are updated by the main loop */ +static unsigned int children_spawned = 0; +static unsigned int children_deleted = 0; + +struct child { + pid_t pid; + int addrlen; + struct sockaddr_in address; +} live_child[MAX_CHILDREN]; + +static void add_child(int idx, pid_t pid, struct sockaddr_in *addr, int addrlen) +{ + live_child[idx].pid = pid; + live_child[idx].addrlen = addrlen; + live_child[idx].address = *addr; +} + +/* + * 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_in *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()); +} + +static void child_handler(int signo) +{ + for (;;) { + pid_t pid = waitpid(-1, NULL, WNOHANG); + + if (pid > 0) { + unsigned reaped = children_reaped; + dead_child[reaped % MAX_CHILDREN] = pid; + children_reaped = reaped + 1; + continue; + } + break; + } +} + +static int serve(int port) +{ + int sockfd; + struct sockaddr_in addr; + + signal(SIGCHLD, child_handler); + sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_IP); + if (sockfd < 0) + die("unable to open socket (%s)", strerror(errno)); + memset(&addr, 0, sizeof(addr)); + addr.sin_port = htons(port); + addr.sin_family = AF_INET; + if (bind(sockfd, (void *)&addr, sizeof(addr)) < 0) + die("unable to bind to port %d (%s)", port, strerror(errno)); + if (listen(sockfd, 5) < 0) + die("unable to listen to port %d (%s)", port, strerror(errno)); + + for (;;) { + struct sockaddr_in in; + socklen_t addrlen = sizeof(in); + int incoming = accept(sockfd, (void *)&in, &addrlen); + + if (incoming < 0) { + switch (errno) { + case EAGAIN: + case EINTR: + case ECONNABORTED: + continue; + default: + die("accept returned %s", strerror(errno)); + } + } + handle(incoming, &in, addrlen); + } +} + +int main(int argc, char **argv) +{ + int port = DEFAULT_GIT_PORT; + int inetd_mode = 0; + int i; + + for (i = 1; i < argc; i++) { + char *arg = argv[i]; + + if (!strncmp(arg, "--port=", 7)) { + char *end; + unsigned long n; + n = strtoul(arg+7, &end, 0); + if (arg[7] && !*end) { + port = n; + continue; + } + } + + if (!strcmp(arg, "--inetd")) { + inetd_mode = 1; + continue; + } + + usage(daemon_usage); + } + + if (inetd_mode) + return execute(); + + return serve(port); +} diff --git a/date.c b/date.c new file mode 100644 index 0000000000..b46f2ce344 --- /dev/null +++ b/date.c @@ -0,0 +1,459 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ + +#include <ctype.h> +#include <time.h> + +#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[] = { + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" +}; + +/* + * 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" + */ +const char *show_date(unsigned long time, int tz) +{ + struct tm *tm; + time_t t; + static char timebuf[200]; + int minutes; + + minutes = tz < 0 ? -tz : tz; + minutes = (minutes / 100)*60 + (minutes % 100); + minutes = tz < 0 ? -minutes : minutes; + t = time + minutes * 60; + tm = gmtime(&t); + 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; +} + +/* + * 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 */ +}; + +#define NR_TZ (sizeof(timezone_names) / sizeof(timezone_names[0])) + +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 < NR_TZ; 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) { + if (tm->tm_hour > 0 && tm->tm_hour < 12) + tm->tm_hour += 12; + return 2; + } + + /* BAD CRAP */ + return skip_alpha(date); +} + +static int is_date(int year, int month, int day, struct tm *tm) +{ + if (month > 0 && month < 13 && day > 0 && day < 32) { + if (year == -1) { + tm->tm_mon = month-1; + tm->tm_mday = day; + return 1; + } + if (year >= 1970 && year < 2100) { + year -= 1900; + } else if (year > 70 && year < 100) { + /* ok */ + } else if (year < 38) { + year += 100; + } else + return 0; + + tm->tm_mon = month-1; + tm->tm_mday = day; + tm->tm_year = year; + return 1; + } + return 0; +} + +static int match_multi_number(unsigned long num, char c, const char *date, char *end, struct tm *tm) +{ + 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 '/': + if (num > 70) { + /* yyyy-mm-dd? */ + if (is_date(num, num2, num3, tm)) + break; + /* yyyy-dd-mm? */ + if (is_date(num, num3, num2, tm)) + break; + } + /* mm/dd/yy ? */ + if (is_date(num3, num2, num, tm)) + break; + /* dd/mm/yy ? */ + if (is_date(num3, num, num2, 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 '-': + 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 <= 1200 && *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 numebers 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; +} + +/* 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. */ +void parse_date(const char *date, char *result, int maxlen) +{ + struct tm tm; + int offset, sign, 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; + + if (!tm_gmt) + then -= offset * 60; + + sign = '+'; + if (offset < 0) { + offset = -offset; + sign = '-'; + } + + snprintf(result, maxlen, "%lu %c%02d%02d", then, sign, offset/60, offset % 60); +} + +void datestamp(char *buf, int bufsize) +{ + time_t now; + int offset; + + time(&now); + + offset = my_mktime(localtime(&now)) - now; + offset /= 60; + + snprintf(buf, bufsize, "%lu %+05d", now, offset/60*100 + offset%60); +} diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000000..18bd6a77fa --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +git-core (0.99-0) unstable; urgency=low + + * Initial deb package support + + -- Eric Biederman <ebiederm@xmission.com> Tue, 12 Jul 2005 10:57:51 -0600 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000000..b62268c6a3 --- /dev/null +++ b/debian/control @@ -0,0 +1,17 @@ +Source: git-core +Section: devel +Priority: optional +Maintainer: Linus Torvalds <torvalds@osdl.org> +Build-Depends-Indep: libz-dev, libssl-dev, libcurl3-dev, asciidoc > 6.0.3, xmlto, debhelper (>= 4.0.0) +Standards-Version: 3.6.1 + +Package: git-core +Architecture: any +Depends: ${shlibs:Depends}, shellutils, diff, rysnc, rcs +Description: The git content addressable filesystem + GIT comes in two layers. The bottom layer is merely an extremely fast + and flexible filesystem-based database designed to store directory trees + with regard to their history. The top layer is a SCM-like tool which + enables human beings to work with the database in a manner to a degree + similar to other SCM tools (like CVS, BitKeeper or Monotone). + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000000..32b7e9c540 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,3 @@ +License: + +GPL v2 (see COPYING for details) diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000000..a252d0f2ca --- /dev/null +++ b/debian/docs @@ -0,0 +1,3 @@ +README +COPYING + diff --git a/debian/git-core.doc-base b/debian/git-core.doc-base new file mode 100644 index 0000000000..e104671bac --- /dev/null +++ b/debian/git-core.doc-base @@ -0,0 +1,12 @@ +Document: git-core +Title: git-core +Author: +Abstract: This manual describes git +Section: Devel + +Format: HTML +Index: /usr/share/doc/git-core/html/git.html +Files: /usr/share/doc/git-core/html/*.html + +Format: text +Files: /usr/share/doc/git-core/git-core.txt diff --git a/debian/rules b/debian/rules new file mode 100644 index 0000000000..9445101601 --- /dev/null +++ b/debian/rules @@ -0,0 +1,81 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +CFLAGS = -g -Wall +ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS))) + CFLAGS += -O0 +else + CFLAGS += -O2 +endif +export CFLAGS + +PREFIX := /usr +MANDIR := /usr/share/man/ + +SRC := ./ +DOC := Documentation/ +DESTDIR := $(CURDIR)/debian/tmp +DOC_DESTDIR := $(DESTDIR)/usr/share/doc/git-core/ +MAN_DESTDIR := $(DESTDIR)/$(MANDIR) + +build: build-stamp +build-stamp: + dh_testdir + $(MAKE) all doc + touch debian/build-stamp + +debian-clean: + dh_testdir + dh_testroot + rm -f debian/build-stamp + dh_clean + +clean: debian-clean + $(MAKE) clean + +install: debian/build-stamp + dh_testdir + dh_testroot + dh_clean -k + dh_installdirs + + make dest=$(DESTDIR) prefix=$(PREFIX) mandir=$(MANDIR) install install-doc + + mkdir -p $(DOC_DESTDIR) + find $(DOC) '(' -name '*.txt' -o -name '*.html' ')' -exec install {} $(DOC_DESTDIR) ';' + + dh_install --sourcedir=$(DESTDIR) + +binary: + dh_testdir + dh_testroot + dh_installchangelogs + dh_installdocs + dh_installexamples +# dh_installmenu +# dh_installdebconf +# dh_installlogrotate +# dh_installemacsen +# dh_installpam +# dh_installmime +# dh_installinit +# dh_installcron +# dh_installinfo + dh_installman + dh_link + dh_strip + dh_compress + dh_fixperms +# dh_perl +# dh_python + dh_makeshlibs + dh_installdeb + dh_shlibdeps + dh_gencontrol + dh_md5sums + dh_builddeb + +.PHONY: build clean binary install clean debian-clean diff --git a/delta.h b/delta.h new file mode 100644 index 0000000000..31d1820f80 --- /dev/null +++ b/delta.h @@ -0,0 +1,34 @@ +#ifndef DELTA_H +#define DELTA_H + +/* handling of delta buffers */ +extern void *diff_delta(void *from_buf, unsigned long from_size, + void *to_buf, unsigned long to_size, + unsigned long *delta_size, unsigned long max_size); +extern void *patch_delta(void *src_buf, unsigned long src_size, + 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 reference buffer size, and again to get the result buffer size. + */ +static inline unsigned long get_delta_hdr_size(const unsigned char **datap) +{ + const unsigned char *data = *datap; + unsigned char cmd = *data++; + unsigned long size = cmd & ~0x80; + int i = 7; + while (cmd & 0x80) { + cmd = *data++; + size |= (cmd & ~0x80) << i; + i += 7; + } + *datap = data; + return size; +} + +#endif diff --git a/diff-cache.c b/diff-cache.c new file mode 100644 index 0000000000..e3c4c8155c --- /dev/null +++ b/diff-cache.c @@ -0,0 +1,294 @@ +#include "cache.h" +#include "diff.h" + +static int cached_only = 0; +static int diff_output_format = DIFF_FORMAT_RAW; +static int diff_line_termination = '\n'; +static int match_nonexisting = 0; +static int detect_rename = 0; +static int find_copies_harder = 0; +static int diff_setup_opt = 0; +static int diff_score_opt = 0; +static const char *pickaxe = NULL; +static int pickaxe_opts = 0; +static int diff_break_opt = -1; +static const char *orderfile = NULL; +static const char *diff_filter = NULL; + +/* A file entry went away or appeared */ +static void show_file(const char *prefix, struct cache_entry *ce, unsigned char *sha1, unsigned int mode) +{ + diff_addremove(prefix[0], ntohl(mode), sha1, ce->name, NULL); +} + +static int get_stat_data(struct cache_entry *ce, unsigned char **sha1p, unsigned int *modep) +{ + unsigned char *sha1 = ce->sha1; + unsigned int mode = ce->ce_mode; + + if (!cached_only) { + static unsigned char no_sha1[20]; + int changed; + struct stat st; + if (lstat(ce->name, &st) < 0) { + if (errno == ENOENT && match_nonexisting) { + *sha1p = sha1; + *modep = mode; + return 0; + } + return -1; + } + changed = ce_match_stat(ce, &st); + if (changed) { + mode = create_ce_mode(st.st_mode); + sha1 = no_sha1; + } + } + + *sha1p = sha1; + *modep = mode; + return 0; +} + +static void show_new_file(struct cache_entry *new) +{ + 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) < 0) + return; + + show_file("+", new, sha1, mode); +} + +static int show_modified(struct cache_entry *old, + struct cache_entry *new, + int report_missing) +{ + unsigned int mode, oldmode; + unsigned char *sha1; + + if (get_stat_data(new, &sha1, &mode) < 0) { + if (report_missing) + show_file("-", old, old->sha1, old->ce_mode); + return -1; + } + + oldmode = old->ce_mode; + if (mode == oldmode && !memcmp(sha1, old->sha1, 20) && + !find_copies_harder) + return 0; + + mode = ntohl(mode); + oldmode = ntohl(oldmode); + + diff_change(oldmode, mode, + old->sha1, sha1, old->name, NULL); + return 0; +} + +static int diff_cache(struct cache_entry **ac, int entries, const char **pathspec) +{ + 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(ce); + break; + } + /* Show difference between old and new */ + show_modified(ac[1], ce, 1); + break; + case 1: + /* No stage 3 (merge) entry? That means it's been deleted */ + if (!same) { + show_file("-", 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-mising 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_only && !show_modified(ce, ac[1], 0)) + break; + /* fallthru */ + case 3: + diff_unmerge(ce->name); + 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); + } +} + +static char *diff_cache_usage = +"git-diff-cache [-m] [--cached] " +"[<common diff options>] <tree-ish> [<path>...]" +COMMON_DIFF_OPTIONS_HELP; + +int main(int argc, const char **argv) +{ + const char *tree_name = NULL; + unsigned char sha1[20]; + const char **pathspec = NULL; + void *tree; + unsigned long size; + int ret; + int allow_options = 1; + int i; + + read_cache(); + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (!allow_options || *arg != '-') { + if (tree_name) { + pathspec = argv + i; + break; + } + tree_name = arg; + continue; + } + + if (!strcmp(arg, "--")) { + allow_options = 0; + continue; + } + if (!strcmp(arg, "-r")) { + /* We accept the -r flag just to look like git-diff-tree */ + continue; + } + /* We accept the -u flag as a synonym for "-p" */ + if (!strcmp(arg, "-p") || !strcmp(arg, "-u")) { + diff_output_format = DIFF_FORMAT_PATCH; + continue; + } + if (!strncmp(arg, "-B", 2)) { + if ((diff_break_opt = diff_scoreopt_parse(arg)) == -1) + usage(diff_cache_usage); + continue; + } + if (!strncmp(arg, "-M", 2)) { + detect_rename = DIFF_DETECT_RENAME; + if ((diff_score_opt = diff_scoreopt_parse(arg)) == -1) + usage(diff_cache_usage); + continue; + } + if (!strncmp(arg, "-C", 2)) { + detect_rename = DIFF_DETECT_COPY; + if ((diff_score_opt = diff_scoreopt_parse(arg)) == -1) + usage(diff_cache_usage); + continue; + } + if (!strcmp(arg, "--find-copies-harder")) { + find_copies_harder = 1; + continue; + } + if (!strcmp(arg, "-z")) { + diff_line_termination = 0; + continue; + } + if (!strcmp(arg, "--name-only")) { + diff_output_format = DIFF_FORMAT_NAME; + continue; + } + if (!strcmp(arg, "-R")) { + diff_setup_opt |= DIFF_SETUP_REVERSE; + continue; + } + if (!strncmp(arg, "-S", 2)) { + pickaxe = arg + 2; + continue; + } + if (!strncmp(arg, "--diff-filter=", 14)) { + diff_filter = arg + 14; + continue; + } + if (!strncmp(arg, "-O", 2)) { + orderfile = arg + 2; + continue; + } + if (!strcmp(arg, "--pickaxe-all")) { + pickaxe_opts = DIFF_PICKAXE_ALL; + continue; + } + if (!strcmp(arg, "-m")) { + match_nonexisting = 1; + continue; + } + if (!strcmp(arg, "--cached")) { + cached_only = 1; + continue; + } + usage(diff_cache_usage); + } + + if (find_copies_harder && detect_rename != DIFF_DETECT_COPY) + usage(diff_cache_usage); + + if (!tree_name || get_sha1(tree_name, sha1)) + usage(diff_cache_usage); + + /* The rest is for paths restriction. */ + diff_setup(diff_setup_opt); + + mark_merge_entries(); + + tree = read_object_with_reference(sha1, "tree", &size, NULL); + if (!tree) + die("bad tree object %s", tree_name); + if (read_tree(tree, size, 1, pathspec)) + die("unable to read tree object %s", tree_name); + + ret = diff_cache(active_cache, active_nr, pathspec); + + diffcore_std(pathspec, + detect_rename, diff_score_opt, + pickaxe, pickaxe_opts, + diff_break_opt, + orderfile, diff_filter); + diff_flush(diff_output_format, diff_line_termination); + return ret; +} diff --git a/diff-delta.c b/diff-delta.c new file mode 100644 index 0000000000..b2ae7b5e6c --- /dev/null +++ b/diff-delta.c @@ -0,0 +1,334 @@ +/* + * 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 <stdlib.h> +#include "delta.h" + + +/* block size: min = 16, max = 64k, power of 2 */ +#define BLK_SIZE 16 + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) + +#define GR_PRIME 0x9e370001 +#define HASH(v, b) (((unsigned int)(v) * GR_PRIME) >> (32 - (b))) + +/* largest prime smaller than 65536 */ +#define BASE 65521 + +/* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */ +#define NMAX 5552 + +#define DO1(buf, i) { s1 += buf[i]; s2 += s1; } +#define DO2(buf, i) DO1(buf, i); DO1(buf, i + 1); +#define DO4(buf, i) DO2(buf, i); DO2(buf, i + 2); +#define DO8(buf, i) DO4(buf, i); DO4(buf, i + 4); +#define DO16(buf) DO8(buf, 0); DO8(buf, 8); + +static unsigned int adler32(unsigned int adler, const unsigned char *buf, int len) +{ + int k; + unsigned int s1 = adler & 0xffff; + unsigned int s2 = adler >> 16; + + while (len > 0) { + k = MIN(len, NMAX); + len -= k; + while (k >= 16) { + DO16(buf); + buf += 16; + k -= 16; + } + if (k != 0) + do { + s1 += *buf++; + s2 += s1; + } while (--k); + s1 %= BASE; + s2 %= BASE; + } + + return (s2 << 16) | s1; +} + +static unsigned int hashbits(unsigned int size) +{ + unsigned int val = 1, bits = 0; + while (val < size && bits < 32) { + val <<= 1; + bits++; + } + return bits ? bits: 1; +} + +typedef struct s_chanode { + struct s_chanode *next; + int icurr; +} chanode_t; + +typedef struct s_chastore { + chanode_t *head, *tail; + int isize, nsize; + chanode_t *ancur; + chanode_t *sncur; + int scurr; +} chastore_t; + +static void cha_init(chastore_t *cha, int isize, int icount) +{ + cha->head = cha->tail = NULL; + cha->isize = isize; + cha->nsize = icount * isize; + cha->ancur = cha->sncur = NULL; + cha->scurr = 0; +} + +static void *cha_alloc(chastore_t *cha) +{ + chanode_t *ancur; + void *data; + + ancur = cha->ancur; + if (!ancur || ancur->icurr == cha->nsize) { + ancur = malloc(sizeof(chanode_t) + cha->nsize); + if (!ancur) + 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 = (void *)ancur + sizeof(chanode_t) + ancur->icurr; + ancur->icurr += cha->isize; + return data; +} + +static void cha_free(chastore_t *cha) +{ + chanode_t *cur = cha->head; + while (cur) { + chanode_t *tmp = cur; + cur = cur->next; + free(tmp); + } +} + +typedef struct s_bdrecord { + struct s_bdrecord *next; + unsigned int fp; + const unsigned char *ptr; +} bdrecord_t; + +typedef struct s_bdfile { + const unsigned char *data, *top; + chastore_t cha; + unsigned int fphbits; + bdrecord_t **fphash; +} bdfile_t; + +static int delta_prepare(const unsigned char *buf, int bufsize, bdfile_t *bdf) +{ + unsigned int fphbits; + int i, hsize; + const unsigned char *base, *data, *top; + bdrecord_t *brec; + bdrecord_t **fphash; + + fphbits = hashbits(bufsize / BLK_SIZE + 1); + hsize = 1 << fphbits; + fphash = malloc(hsize * sizeof(bdrecord_t *)); + if (!fphash) + return -1; + for (i = 0; i < hsize; i++) + fphash[i] = NULL; + cha_init(&bdf->cha, sizeof(bdrecord_t), hsize / 4 + 1); + + bdf->data = data = base = buf; + bdf->top = top = buf + bufsize; + data += (bufsize / BLK_SIZE) * BLK_SIZE; + if (data == top) + data -= BLK_SIZE; + + for ( ; data >= base; data -= BLK_SIZE) { + brec = cha_alloc(&bdf->cha); + if (!brec) { + cha_free(&bdf->cha); + free(fphash); + return -1; + } + brec->fp = adler32(0, data, MIN(BLK_SIZE, top - data)); + brec->ptr = data; + i = HASH(brec->fp, fphbits); + brec->next = fphash[i]; + fphash[i] = brec; + } + + bdf->fphbits = fphbits; + bdf->fphash = fphash; + + return 0; +} + +static void delta_cleanup(bdfile_t *bdf) +{ + free(bdf->fphash); + cha_free(&bdf->cha); +} + +#define COPYOP_SIZE(o, s) \ + (!!(o & 0xff) + !!(o & 0xff00) + !!(o & 0xff0000) + !!(o & 0xff000000) + \ + !!(s & 0xff) + !!(s & 0xff00) + 1) + +void *diff_delta(void *from_buf, unsigned long from_size, + void *to_buf, unsigned long to_size, + unsigned long *delta_size, + unsigned long max_size) +{ + int i, outpos, outsize, inscnt, csize, msize, moff; + unsigned int fp; + const unsigned char *data, *top, *ptr1, *ptr2; + unsigned char *out, *orig; + bdrecord_t *brec; + bdfile_t bdf; + + if (!from_size || !to_size || delta_prepare(from_buf, from_size, &bdf)) + return NULL; + + outpos = 0; + outsize = 8192; + out = malloc(outsize); + if (!out) { + delta_cleanup(&bdf); + return NULL; + } + + data = to_buf; + top = to_buf + to_size; + + /* store reference buffer size */ + out[outpos++] = from_size; + from_size >>= 7; + while (from_size) { + out[outpos - 1] |= 0x80; + out[outpos++] = from_size; + from_size >>= 7; + } + + /* store target buffer size */ + out[outpos++] = to_size; + to_size >>= 7; + while (to_size) { + out[outpos - 1] |= 0x80; + out[outpos++] = to_size; + to_size >>= 7; + } + + inscnt = 0; + moff = 0; + while (data < top) { + msize = 0; + fp = adler32(0, data, MIN(top - data, BLK_SIZE)); + i = HASH(fp, bdf.fphbits); + for (brec = bdf.fphash[i]; brec; brec = brec->next) { + if (brec->fp == fp) { + csize = bdf.top - brec->ptr; + if (csize > top - data) + csize = top - data; + for (ptr1 = brec->ptr, ptr2 = data; + csize && *ptr1 == *ptr2; + csize--, ptr1++, ptr2++); + + csize = ptr1 - brec->ptr; + if (csize > msize) { + moff = brec->ptr - bdf.data; + msize = csize; + if (msize >= 0x10000) { + msize = 0x10000; + break; + } + } + } + } + + if (!msize || msize < COPYOP_SIZE(moff, msize)) { + if (!inscnt) + outpos++; + out[outpos++] = *data++; + inscnt++; + if (inscnt == 0x7f) { + out[outpos - inscnt - 1] = inscnt; + inscnt = 0; + } + } else { + if (inscnt) { + out[outpos - inscnt - 1] = inscnt; + inscnt = 0; + } + + data += msize; + orig = 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; } + + *orig = i; + } + + if (max_size && outpos > max_size) { + free(out); + delta_cleanup(&bdf); + return NULL; + } + + /* next time around the largest possible output is 1 + 4 + 3 */ + if (outpos > outsize - 8) { + void *tmp = out; + outsize = outsize * 3 / 2; + out = realloc(out, outsize); + if (!out) { + free(tmp); + delta_cleanup(&bdf); + return NULL; + } + } + } + + if (inscnt) + out[outpos - inscnt - 1] = inscnt; + + delta_cleanup(&bdf); + *delta_size = outpos; + return out; +} diff --git a/diff-files.c b/diff-files.c new file mode 100644 index 0000000000..39dd54f80a --- /dev/null +++ b/diff-files.c @@ -0,0 +1,157 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "diff.h" + +static const char *diff_files_usage = +"git-diff-files [-q] " +"[<common diff options>] [<path>...]" +COMMON_DIFF_OPTIONS_HELP; + +static int diff_output_format = DIFF_FORMAT_RAW; +static int diff_line_termination = '\n'; +static int detect_rename = 0; +static int find_copies_harder = 0; +static int diff_setup_opt = 0; +static int diff_score_opt = 0; +static const char *pickaxe = NULL; +static int pickaxe_opts = 0; +static int diff_break_opt = -1; +static const char *orderfile = NULL; +static const char *diff_filter = NULL; +static int silent = 0; + +static void show_unmerge(const char *path) +{ + diff_unmerge(path); +} + +static void show_file(int pfx, struct cache_entry *ce) +{ + diff_addremove(pfx, ntohl(ce->ce_mode), ce->sha1, ce->name, NULL); +} + +static void show_modified(int oldmode, int mode, + const unsigned char *old_sha1, const unsigned char *sha1, + char *path) +{ + diff_change(oldmode, mode, old_sha1, sha1, path, NULL); +} + +int main(int argc, const char **argv) +{ + static const unsigned char null_sha1[20] = { 0, }; + const char **pathspec; + int entries = read_cache(); + int i; + + while (1 < argc && argv[1][0] == '-') { + if (!strcmp(argv[1], "-p") || !strcmp(argv[1], "-u")) + diff_output_format = DIFF_FORMAT_PATCH; + else if (!strcmp(argv[1], "-q")) + silent = 1; + else if (!strcmp(argv[1], "-r")) + ; /* no-op */ + else if (!strcmp(argv[1], "-s")) + ; /* no-op */ + else if (!strcmp(argv[1], "-z")) + diff_line_termination = 0; + else if (!strcmp(argv[1], "--name-only")) + diff_output_format = DIFF_FORMAT_NAME; + else if (!strcmp(argv[1], "-R")) + diff_setup_opt |= DIFF_SETUP_REVERSE; + else if (!strncmp(argv[1], "-S", 2)) + pickaxe = argv[1] + 2; + else if (!strncmp(argv[1], "-O", 2)) + orderfile = argv[1] + 2; + else if (!strncmp(argv[1], "--diff-filter=", 14)) + diff_filter = argv[1] + 14; + else if (!strcmp(argv[1], "--pickaxe-all")) + pickaxe_opts = DIFF_PICKAXE_ALL; + else if (!strncmp(argv[1], "-B", 2)) { + if ((diff_break_opt = + diff_scoreopt_parse(argv[1])) == -1) + usage(diff_files_usage); + } + else if (!strncmp(argv[1], "-M", 2)) { + if ((diff_score_opt = + diff_scoreopt_parse(argv[1])) == -1) + usage(diff_files_usage); + detect_rename = DIFF_DETECT_RENAME; + } + else if (!strncmp(argv[1], "-C", 2)) { + if ((diff_score_opt = + diff_scoreopt_parse(argv[1])) == -1) + usage(diff_files_usage); + detect_rename = DIFF_DETECT_COPY; + } + else if (!strcmp(argv[1], "--find-copies-harder")) + find_copies_harder = 1; + else + usage(diff_files_usage); + argv++; argc--; + } + + /* Do we have a pathspec? */ + pathspec = (argc > 1) ? argv + 1 : NULL; + + if (find_copies_harder && detect_rename != DIFF_DETECT_COPY) + usage(diff_files_usage); + + /* At this point, if argc == 1, then we are doing everything. + * Otherwise argv[1] .. argv[argc-1] have the explicit paths. + */ + if (entries < 0) { + perror("read_cache"); + exit(1); + } + + diff_setup(diff_setup_opt); + + for (i = 0; i < entries; i++) { + struct stat st; + unsigned int oldmode; + struct cache_entry *ce = active_cache[i]; + int changed; + + if (!ce_path_match(ce, pathspec)) + continue; + + if (ce_stage(ce)) { + show_unmerge(ce->name); + while (i < entries && + !strcmp(ce->name, active_cache[i]->name)) + i++; + i--; /* compensate for loop control increments */ + continue; + } + + if (lstat(ce->name, &st) < 0) { + if (errno != ENOENT && errno != ENOTDIR) { + perror(ce->name); + continue; + } + if (silent) + continue; + show_file('-', ce); + continue; + } + changed = ce_match_stat(ce, &st); + if (!changed && !find_copies_harder) + continue; + oldmode = ntohl(ce->ce_mode); + show_modified(oldmode, DIFF_FILE_CANON_MODE(st.st_mode), + ce->sha1, (changed ? null_sha1 : ce->sha1), + ce->name); + } + diffcore_std(pathspec, + detect_rename, diff_score_opt, + pickaxe, pickaxe_opts, + diff_break_opt, + orderfile, diff_filter); + diff_flush(diff_output_format, diff_line_termination); + return 0; +} diff --git a/diff-helper.c b/diff-helper.c new file mode 100644 index 0000000000..6649fb548f --- /dev/null +++ b/diff-helper.c @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "strbuf.h" +#include "diff.h" + +static const char *pickaxe = NULL; +static int pickaxe_opts = 0; +static const char *orderfile = NULL; +static const char *diff_filter = NULL; +static int line_termination = '\n'; +static int inter_name_termination = '\t'; + +static void flush_them(int ac, const char **av) +{ + diffcore_std_no_resolve(av + 1, + pickaxe, pickaxe_opts, + orderfile, diff_filter); + diff_flush(DIFF_FORMAT_PATCH, '\n'); +} + +static const char *diff_helper_usage = +"git-diff-helper [-z] [-O<orderfile>] [-S<string>] [--pickaxe-all] [<path>...]"; + +int main(int ac, const char **av) { + struct strbuf sb; + const char *garbage_flush_format; + + strbuf_init(&sb); + + while (1 < ac && av[1][0] == '-') { + if (av[1][1] == 'z') + line_termination = inter_name_termination = 0; + else if (av[1][1] == 'S') { + pickaxe = av[1] + 2; + } + else if (!strcmp(av[1], "--pickaxe-all")) + pickaxe_opts = DIFF_PICKAXE_ALL; + else if (!strncmp(av[1], "--diff-filter=", 14)) + diff_filter = av[1] + 14; + else if (!strncmp(av[1], "-O", 2)) + orderfile = av[1] + 2; + else + usage(diff_helper_usage); + ac--; av++; + } + garbage_flush_format = (line_termination == 0) ? "%s" : "%s\n"; + + /* the remaining parameters are paths patterns */ + + diff_setup(0); + while (1) { + unsigned old_mode, new_mode; + unsigned char old_sha1[20], new_sha1[20]; + char old_path[PATH_MAX]; + int status, score, two_paths; + char new_path[PATH_MAX]; + + int ch; + char *cp, *ep; + + read_line(&sb, stdin, line_termination); + if (sb.eof) + break; + switch (sb.buf[0]) { + case ':': + /* parse the first part up to the status */ + cp = sb.buf + 1; + old_mode = new_mode = 0; + while ((ch = *cp) && ('0' <= ch && ch <= '7')) { + old_mode = (old_mode << 3) | (ch - '0'); + cp++; + } + if (*cp++ != ' ') + break; + while ((ch = *cp) && ('0' <= ch && ch <= '7')) { + new_mode = (new_mode << 3) | (ch - '0'); + cp++; + } + if (*cp++ != ' ') + break; + if (get_sha1_hex(cp, old_sha1)) + break; + cp += 40; + if (*cp++ != ' ') + break; + if (get_sha1_hex(cp, new_sha1)) + break; + cp += 40; + if (*cp++ != ' ') + break; + status = *cp++; + if (!strchr("MCRNDU", status)) + break; + two_paths = score = 0; + if (status == 'R' || status == 'C') + two_paths = 1; + + /* pick up score if exists */ + if (sscanf(cp, "%d", &score) != 1) + score = 0; + cp = strchr(cp, + inter_name_termination); + if (!cp) + break; + if (*cp++ != inter_name_termination) + break; + + /* first pathname */ + if (!line_termination) { + read_line(&sb, stdin, line_termination); + if (sb.eof) + break; + strcpy(old_path, sb.buf); + } + else if (!two_paths) + strcpy(old_path, cp); + else { + ep = strchr(cp, inter_name_termination); + if (!ep) + break; + strncpy(old_path, cp, ep-cp); + old_path[ep-cp] = 0; + cp = ep + 1; + } + + /* second pathname */ + if (!two_paths) + strcpy(new_path, old_path); + else { + if (!line_termination) { + read_line(&sb, stdin, + line_termination); + if (sb.eof) + break; + strcpy(new_path, sb.buf); + } + else + strcpy(new_path, cp); + } + diff_helper_input(old_mode, new_mode, + old_sha1, new_sha1, + old_path, status, score, + new_path); + continue; + } + flush_them(ac, av); + printf(garbage_flush_format, sb.buf); + } + flush_them(ac, av); + return 0; +} diff --git a/diff-stages.c b/diff-stages.c new file mode 100644 index 0000000000..cbaba552b3 --- /dev/null +++ b/diff-stages.c @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2005 Junio C Hamano + */ + +#include "cache.h" +#include "diff.h" + +static int diff_output_format = DIFF_FORMAT_RAW; +static int diff_line_termination = '\n'; +static int detect_rename = 0; +static int find_copies_harder = 0; +static int diff_setup_opt = 0; +static int diff_score_opt = 0; +static const char *pickaxe = NULL; +static int pickaxe_opts = 0; +static int diff_break_opt = -1; +static const char *orderfile = NULL; +static const char *diff_filter = NULL; + +static 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) +{ + int i = 0; + while (i < active_nr) { + struct cache_entry *ce, *stages[4] = { NULL, }; + struct cache_entry *one, *two; + const char *name; + int len; + ce = active_cache[i]; + 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 (!one && !two) + continue; + if (!one) + diff_addremove('+', ntohl(two->ce_mode), + two->sha1, name, NULL); + else if (!two) + diff_addremove('-', ntohl(one->ce_mode), + one->sha1, name, NULL); + else if (memcmp(one->sha1, two->sha1, 20) || + (one->ce_mode != two->ce_mode) || + find_copies_harder) + diff_change(ntohl(one->ce_mode), ntohl(two->ce_mode), + one->sha1, two->sha1, name, NULL); + } +} + +int main(int ac, const char **av) +{ + int stage1, stage2; + + read_cache(); + while (1 < ac && av[1][0] == '-') { + const char *arg = av[1]; + if (!strcmp(arg, "-r")) + ; /* as usual */ + else if (!strcmp(arg, "-p") || !strcmp(arg, "-u")) + diff_output_format = DIFF_FORMAT_PATCH; + else if (!strncmp(arg, "-B", 2)) { + if ((diff_break_opt = diff_scoreopt_parse(arg)) == -1) + usage(diff_stages_usage); + } + else if (!strncmp(arg, "-M", 2)) { + detect_rename = DIFF_DETECT_RENAME; + if ((diff_score_opt = diff_scoreopt_parse(arg)) == -1) + usage(diff_stages_usage); + } + else if (!strncmp(arg, "-C", 2)) { + detect_rename = DIFF_DETECT_COPY; + if ((diff_score_opt = diff_scoreopt_parse(arg)) == -1) + usage(diff_stages_usage); + } + else if (!strcmp(arg, "--find-copies-harder")) + find_copies_harder = 1; + else if (!strcmp(arg, "-z")) + diff_line_termination = 0; + else if (!strcmp(arg, "--name-only")) + diff_output_format = DIFF_FORMAT_NAME; + else if (!strcmp(arg, "-R")) + diff_setup_opt |= DIFF_SETUP_REVERSE; + else if (!strncmp(arg, "-S", 2)) + pickaxe = arg + 2; + else if (!strncmp(arg, "-O", 2)) + orderfile = arg + 2; + else if (!strncmp(arg, "--diff-filter=", 14)) + diff_filter = arg + 14; + else if (!strcmp(arg, "--pickaxe-all")) + pickaxe_opts = DIFF_PICKAXE_ALL; + else + usage(diff_stages_usage); + ac--; av++; + } + + if (ac < 3 || + sscanf(av[1], "%d", &stage1) != 1 || + ! (0 <= stage1 && stage1 <= 3) || + sscanf(av[2], "%d", &stage2) != 1 || + ! (0 <= stage2 && stage2 <= 3) || + (find_copies_harder && detect_rename != DIFF_DETECT_COPY)) + usage(diff_stages_usage); + + av += 3; /* The rest from av[0] are for paths restriction. */ + diff_setup(diff_setup_opt); + + diff_stages(stage1, stage2); + + diffcore_std(av, + detect_rename, diff_score_opt, + pickaxe, pickaxe_opts, + diff_break_opt, + orderfile, + diff_filter); + diff_flush(diff_output_format, diff_line_termination); + return 0; +} diff --git a/diff-tree.c b/diff-tree.c new file mode 100644 index 0000000000..5b9467d58b --- /dev/null +++ b/diff-tree.c @@ -0,0 +1,559 @@ +#include <ctype.h> +#include "cache.h" +#include "diff.h" +#include "commit.h" + +static int show_root_diff = 0; +static int verbose_header = 0; +static int ignore_merges = 1; +static int recursive = 0; +static int show_tree_entry_in_recursive = 0; +static int read_stdin = 0; +static int diff_output_format = DIFF_FORMAT_RAW; +static int diff_line_termination = '\n'; +static int detect_rename = 0; +static int find_copies_harder = 0; +static int diff_setup_opt = 0; +static int diff_score_opt = 0; +static const char *pickaxe = NULL; +static int pickaxe_opts = 0; +static int diff_break_opt = -1; +static const char *orderfile = NULL; +static const char *diff_filter = NULL; +static const char *header = NULL; +static const char *header_prefix = ""; +static enum cmit_fmt commit_format = CMIT_FMT_RAW; + +// What paths are we interested in? +static int nr_paths = 0; +static const char **paths = NULL; +static int *pathlens = NULL; + +static int diff_tree_sha1(const unsigned char *old, const unsigned char *new, const char *base); + +static void update_tree_entry(void **bufp, unsigned long *sizep) +{ + void *buf = *bufp; + unsigned long size = *sizep; + int len = strlen(buf) + 1 + 20; + + if (size < len) + die("corrupt tree file"); + *bufp = buf + len; + *sizep = size - len; +} + +static const unsigned char *extract(void *tree, unsigned long size, const char **pathp, unsigned int *modep) +{ + int len = strlen(tree)+1; + const unsigned char *sha1 = tree + len; + const char *path = strchr(tree, ' '); + unsigned int mode; + + if (!path || size < len + 20 || sscanf(tree, "%o", &mode) != 1) + die("corrupt tree file"); + *pathp = path+1; + *modep = DIFF_FILE_CANON_MODE(mode); + return sha1; +} + +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_file(const char *prefix, void *tree, unsigned long size, const char *base); +static void show_tree(const char *prefix, void *tree, unsigned long size, const char *base); + +/* A file entry went away or appeared */ +static void show_file(const char *prefix, void *tree, unsigned long size, const char *base) +{ + unsigned mode; + const char *path; + const unsigned char *sha1 = extract(tree, size, &path, &mode); + + if (recursive && S_ISDIR(mode)) { + char type[20]; + unsigned long size; + char *newbase = malloc_base(base, path, strlen(path)); + void *tree; + + tree = read_sha1_file(sha1, type, &size); + if (!tree || strcmp(type, "tree")) + die("corrupt tree sha %s", sha1_to_hex(sha1)); + + show_tree(prefix, tree, size, newbase); + + free(tree); + free(newbase); + return; + } + + diff_addremove(prefix[0], mode, sha1, base, path); +} + +static int compare_tree_entry(void *tree1, unsigned long size1, void *tree2, unsigned long size2, const char *base) +{ + unsigned mode1, mode2; + const char *path1, *path2; + const unsigned char *sha1, *sha2; + int cmp, pathlen1, pathlen2; + + sha1 = extract(tree1, size1, &path1, &mode1); + sha2 = extract(tree2, size2, &path2, &mode2); + + pathlen1 = strlen(path1); + pathlen2 = strlen(path2); + cmp = base_name_compare(path1, pathlen1, mode1, path2, pathlen2, mode2); + if (cmp < 0) { + show_file("-", tree1, size1, base); + return -1; + } + if (cmp > 0) { + show_file("+", tree2, size2, base); + return 1; + } + if (!find_copies_harder && !memcmp(sha1, sha2, 20) && 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_file("-", tree1, size1, base); + show_file("+", tree2, size2, base); + return 0; + } + + if (recursive && S_ISDIR(mode1)) { + int retval; + char *newbase = malloc_base(base, path1, pathlen1); + if (show_tree_entry_in_recursive) + diff_change(mode1, mode2, sha1, sha2, base, path1); + retval = diff_tree_sha1(sha1, sha2, newbase); + free(newbase); + return retval; + } + + diff_change(mode1, mode2, sha1, sha2, base, path1); + return 0; +} + +static int interesting(void *tree, unsigned long size, const char *base) +{ + const char *path; + unsigned mode; + int i; + int baselen, pathlen; + + if (!nr_paths) + return 1; + + (void)extract(tree, size, &path, &mode); + + pathlen = strlen(path); + baselen = strlen(base); + + for (i=0; i < nr_paths; i++) { + const char *match = paths[i]; + int matchlen = 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(const char *prefix, void *tree, unsigned long size, const char *base) +{ + while (size) { + if (interesting(tree, size, base)) + show_file(prefix, tree, size, base); + update_tree_entry(&tree, &size); + } +} + +static int diff_tree(void *tree1, unsigned long size1, void *tree2, unsigned long size2, const char *base) +{ + while (size1 | size2) { + if (nr_paths && size1 && !interesting(tree1, size1, base)) { + update_tree_entry(&tree1, &size1); + continue; + } + if (nr_paths && size2 && !interesting(tree2, size2, base)) { + update_tree_entry(&tree2, &size2); + continue; + } + if (!size1) { + show_file("+", tree2, size2, base); + update_tree_entry(&tree2, &size2); + continue; + } + if (!size2) { + show_file("-", tree1, size1, base); + update_tree_entry(&tree1, &size1); + continue; + } + switch (compare_tree_entry(tree1, size1, tree2, size2, base)) { + case -1: + update_tree_entry(&tree1, &size1); + continue; + case 0: + update_tree_entry(&tree1, &size1); + /* Fallthrough */ + case 1: + update_tree_entry(&tree2, &size2); + continue; + } + die("git-diff-tree: internal error"); + } + return 0; +} + +static int diff_tree_sha1(const unsigned char *old, const unsigned char *new, const char *base) +{ + void *tree1, *tree2; + unsigned long size1, size2; + int retval; + + tree1 = read_object_with_reference(old, "tree", &size1, NULL); + if (!tree1) + die("unable to read source tree (%s)", sha1_to_hex(old)); + tree2 = read_object_with_reference(new, "tree", &size2, NULL); + if (!tree2) + die("unable to read destination tree (%s)", sha1_to_hex(new)); + retval = diff_tree(tree1, size1, tree2, size2, base); + free(tree1); + free(tree2); + return retval; +} + +static void call_diff_setup(void) +{ + diff_setup(diff_setup_opt); +} + +static int call_diff_flush(void) +{ + diffcore_std(NULL, + detect_rename, diff_score_opt, + pickaxe, pickaxe_opts, + diff_break_opt, + orderfile, + diff_filter); + if (diff_queue_is_empty()) { + diff_flush(DIFF_FORMAT_NO_OUTPUT, diff_line_termination); + return 0; + } + if (header) { + printf("%s%c", header, diff_line_termination); + header = NULL; + } + diff_flush(diff_output_format, diff_line_termination); + return 1; +} + +static int diff_tree_sha1_top(const unsigned char *old, + const unsigned char *new, const char *base) +{ + int ret; + + call_diff_setup(); + ret = diff_tree_sha1(old, new, base); + call_diff_flush(); + return ret; +} + +static int diff_root_tree(const unsigned char *new, const char *base) +{ + int retval; + void *tree; + unsigned long size; + + call_diff_setup(); + tree = read_object_with_reference(new, "tree", &size, NULL); + if (!tree) + die("unable to read root tree (%s)", sha1_to_hex(new)); + retval = diff_tree("", 0, tree, size, base); + free(tree); + call_diff_flush(); + return retval; +} + +static const char *generate_header(const char *commit, const char *parent, const char *msg, unsigned long len) +{ + static char this_header[16384]; + int offset; + + if (!verbose_header) + return commit; + + offset = sprintf(this_header, "%s%s (from %s)\n", header_prefix, commit, parent); + offset += pretty_print_commit(commit_format, msg, len, this_header + offset, sizeof(this_header) - offset); + return this_header; +} + +static int diff_tree_commit(const unsigned char *commit, const char *name) +{ + unsigned long size, offset; + char *buf = read_object_with_reference(commit, "commit", &size, NULL); + + if (!buf) + return -1; + + if (!name) { + static char commit_name[60]; + strcpy(commit_name, sha1_to_hex(commit)); + name = commit_name; + } + + /* Root commit? */ + if (show_root_diff && memcmp(buf + 46, "parent ", 7)) { + header = generate_header(name, "root", buf, size); + diff_root_tree(commit, ""); + } + + /* More than one parent? */ + if (ignore_merges) { + if (!memcmp(buf + 46 + 48, "parent ", 7)) + return 0; + } + + offset = 46; + while (offset + 48 < size && !memcmp(buf + offset, "parent ", 7)) { + unsigned char parent[20]; + if (get_sha1_hex(buf + offset + 7, parent)) + return -1; + header = generate_header(name, sha1_to_hex(parent), buf, size); + diff_tree_sha1_top(parent, commit, ""); + if (!header && verbose_header) { + header_prefix = "\ndiff-tree "; + /* + * Don't print multiple merge entries if we + * don't print the diffs. + */ + } + offset += 48; + } + return 0; +} + +static int diff_tree_stdin(char *line) +{ + int len = strlen(line); + unsigned char commit[20], parent[20]; + static char this_header[1000]; + + if (!len || line[len-1] != '\n') + return -1; + line[len-1] = 0; + if (get_sha1_hex(line, commit)) + return -1; + if (isspace(line[40]) && !get_sha1_hex(line+41, parent)) { + line[40] = 0; + line[81] = 0; + sprintf(this_header, "%s (from %s)\n", line, line+41); + header = this_header; + return diff_tree_sha1_top(parent, commit, ""); + } + line[40] = 0; + return diff_tree_commit(commit, line); +} + +static char *diff_tree_usage = +"git-diff-tree [--stdin] [-m] [-s] [-v] [--pretty] [-t] " +"[<common diff options>] <tree-ish> <tree-ish>" +COMMON_DIFF_OPTIONS_HELP; + +int main(int argc, const char **argv) +{ + int nr_sha1; + char line[1000]; + unsigned char sha1[2][20]; + + nr_sha1 = 0; + for (;;) { + const char *arg; + + argv++; + argc--; + arg = *argv; + if (!arg) + break; + + if (*arg != '-') { + if (nr_sha1 < 2 && !get_sha1(arg, sha1[nr_sha1])) { + nr_sha1++; + continue; + } + break; + } + + if (!strcmp(arg, "--")) { + argv++; + argc--; + break; + } + if (!strcmp(arg, "-r")) { + recursive = 1; + continue; + } + if (!strcmp(arg, "-t")) { + recursive = show_tree_entry_in_recursive = 1; + continue; + } + if (!strcmp(arg, "-R")) { + diff_setup_opt |= DIFF_SETUP_REVERSE; + continue; + } + if (!strcmp(arg, "-p") || !strcmp(arg, "-u")) { + diff_output_format = DIFF_FORMAT_PATCH; + recursive = 1; + continue; + } + if (!strncmp(arg, "-S", 2)) { + pickaxe = arg + 2; + continue; + } + if (!strncmp(arg, "-O", 2)) { + orderfile = arg + 2; + continue; + } + if (!strncmp(arg, "--diff-filter=", 14)) { + diff_filter = arg + 14; + continue; + } + if (!strcmp(arg, "--pickaxe-all")) { + pickaxe_opts = DIFF_PICKAXE_ALL; + continue; + } + if (!strncmp(arg, "-M", 2)) { + detect_rename = DIFF_DETECT_RENAME; + if ((diff_score_opt = diff_scoreopt_parse(arg)) == -1) + usage(diff_tree_usage); + continue; + } + if (!strncmp(arg, "-C", 2)) { + detect_rename = DIFF_DETECT_COPY; + if ((diff_score_opt = diff_scoreopt_parse(arg)) == -1) + usage(diff_tree_usage); + continue; + } + if (!strncmp(arg, "-B", 2)) { + if ((diff_break_opt = diff_scoreopt_parse(arg)) == -1) + usage(diff_tree_usage); + continue; + } + if (!strcmp(arg, "--find-copies-harder")) { + find_copies_harder = 1; + continue; + } + if (!strcmp(arg, "--name-only")) { + diff_output_format = DIFF_FORMAT_NAME; + continue; + } + if (!strcmp(arg, "-z")) { + diff_line_termination = 0; + continue; + } + if (!strcmp(arg, "-m")) { + ignore_merges = 0; + continue; + } + if (!strcmp(arg, "-s")) { + diff_output_format = DIFF_FORMAT_NO_OUTPUT; + continue; + } + if (!strcmp(arg, "-v")) { + verbose_header = 1; + header_prefix = "diff-tree "; + continue; + } + if (!strncmp(arg, "--pretty", 8)) { + verbose_header = 1; + header_prefix = "diff-tree "; + commit_format = get_commit_format(arg+8); + continue; + } + if (!strcmp(arg, "--stdin")) { + read_stdin = 1; + continue; + } + if (!strcmp(arg, "--root")) { + show_root_diff = 1; + continue; + } + usage(diff_tree_usage); + } + if (find_copies_harder && detect_rename != DIFF_DETECT_COPY) + usage(diff_tree_usage); + + if (argc > 0) { + int i; + + paths = argv; + nr_paths = argc; + pathlens = xmalloc(nr_paths * sizeof(int)); + for (i=0; i<nr_paths; i++) + pathlens[i] = strlen(paths[i]); + } + + switch (nr_sha1) { + case 0: + if (!read_stdin) + usage(diff_tree_usage); + break; + case 1: + diff_tree_commit(sha1[0], NULL); + break; + case 2: + diff_tree_sha1_top(sha1[0], sha1[1], ""); + break; + } + + if (!read_stdin) + return 0; + + if (detect_rename) + diff_setup_opt |= (DIFF_SETUP_USE_SIZE_CACHE | + DIFF_SETUP_USE_CACHE); + while (fgets(line, sizeof(line), stdin)) + diff_tree_stdin(line); + + return 0; +} diff --git a/diff.c b/diff.c new file mode 100644 index 0000000000..3cb41c5333 --- /dev/null +++ b/diff.c @@ -0,0 +1,1193 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include <sys/types.h> +#include <sys/wait.h> +#include <signal.h> +#include "cache.h" +#include "quote.h" +#include "diff.h" +#include "diffcore.h" + +static const char *diff_opts = "-pu"; +static unsigned char null_sha1[20] = { 0, }; + +static int reverse_diff; +static int use_size_cache; + +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; + + /* + * Default values above are meant to match the + * Linux kernel development style. Examples of + * alternative styles you can specify via environment + * variables are: + * + * GIT_DIFF_OPTS="-c"; + */ + if (gitenv("GIT_EXTERNAL_DIFF")) + external_diff_cmd = gitenv("GIT_EXTERNAL_DIFF"); + + /* In case external diff fails... */ + diff_opts = gitenv("GIT_DIFF_OPTS") ? : diff_opts; + + done_preparing = 1; + return external_diff_cmd; +} + +static struct diff_tempfile { + const char *name; /* filename external diff should read from */ + char hex[41]; + char mode[10]; + char tmp_path[50]; +} diff_temp[2]; + +static int count_lines(const char *filename) +{ + FILE *in; + int count, ch, completely_empty = 1, nl_just_seen = 0; + in = fopen(filename, "r"); + count = 0; + while ((ch = fgetc(in)) != EOF) + if (ch == '\n') { + count++; + nl_just_seen = 1; + completely_empty = 0; + } + else { + nl_just_seen = 0; + completely_empty = 0; + } + fclose(in); + 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 *filename) +{ + FILE *in; + int ch, nl_just_seen = 1; + in = fopen(filename, "r"); + while ((ch = fgetc(in)) != EOF) { + if (nl_just_seen) + putchar(prefix); + putchar(ch); + if (ch == '\n') + nl_just_seen = 1; + else + nl_just_seen = 0; + } + fclose(in); + 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_tempfile *temp) +{ + /* Use temp[i].name as input, name_a and name_b as labels */ + int lc_a, lc_b; + lc_a = count_lines(temp[0].name); + lc_b = count_lines(temp[1].name); + printf("--- %s\n+++ %s\n@@ -", name_a, name_b); + print_line_count(lc_a); + printf(" +"); + print_line_count(lc_b); + printf(" @@\n"); + if (lc_a) + copy_file('-', temp[0].name); + if (lc_b) + copy_file('+', temp[1].name); +} + +static void builtin_diff(const char *name_a, + const char *name_b, + struct diff_tempfile *temp, + const char *xfrm_msg, + int complete_rewrite) +{ + int i, next_at, cmd_size; + const char *diff_cmd = "diff -L%s%s -L%s%s"; + const char *diff_arg = "%s %s||:"; /* "||:" is to return 0 */ + const char *input_name_sq[2]; + const char *path0[2]; + const char *path1[2]; + const char *name_sq[2]; + char *cmd; + + name_sq[0] = sq_quote(name_a); + name_sq[1] = sq_quote(name_b); + + /* diff_cmd and diff_arg have 6 %s in total which makes + * the sum of these strings 12 bytes larger than required. + * we use 2 spaces around diff-opts, and we need to count + * terminating NUL, so we subtract 9 here. + */ + cmd_size = (strlen(diff_cmd) + strlen(diff_opts) + + strlen(diff_arg) - 9); + for (i = 0; i < 2; i++) { + input_name_sq[i] = sq_quote(temp[i].name); + if (!strcmp(temp[i].name, "/dev/null")) { + path0[i] = "/dev/null"; + path1[i] = ""; + } else { + path0[i] = i ? "b/" : "a/"; + path1[i] = name_sq[i]; + } + cmd_size += (strlen(path0[i]) + strlen(path1[i]) + + strlen(input_name_sq[i])); + } + + cmd = xmalloc(cmd_size); + + next_at = 0; + next_at += snprintf(cmd+next_at, cmd_size-next_at, + diff_cmd, + path0[0], path1[0], path0[1], path1[1]); + next_at += snprintf(cmd+next_at, cmd_size-next_at, + " %s ", diff_opts); + next_at += snprintf(cmd+next_at, cmd_size-next_at, + diff_arg, input_name_sq[0], input_name_sq[1]); + + printf("diff --git a/%s b/%s\n", name_a, name_b); + if (!path1[0][0]) { + printf("new file mode %s\n", temp[1].mode); + if (xfrm_msg && xfrm_msg[0]) + puts(xfrm_msg); + } + else if (!path1[1][0]) { + printf("deleted file mode %s\n", temp[0].mode); + if (xfrm_msg && xfrm_msg[0]) + puts(xfrm_msg); + } + else { + if (strcmp(temp[0].mode, temp[1].mode)) { + printf("old mode %s\n", temp[0].mode); + printf("new mode %s\n", temp[1].mode); + } + if (xfrm_msg && xfrm_msg[0]) + puts(xfrm_msg); + if (strncmp(temp[0].mode, temp[1].mode, 3)) + /* we do not run diff between different kind + * of objects. + */ + exit(0); + if (complete_rewrite) { + fflush(NULL); + emit_rewrite_diff(name_a, name_b, temp); + exit(0); + } + } + fflush(NULL); + execlp("/bin/sh","sh", "-c", cmd, NULL); +} + +struct diff_filespec *alloc_filespec(const char *path) +{ + int namelen = strlen(path); + struct diff_filespec *spec = xmalloc(sizeof(*spec) + namelen + 1); + spec->path = (char *)(spec + 1); + strcpy(spec->path, path); + spec->should_free = spec->should_munmap = 0; + spec->xfrm_flags = 0; + spec->size = 0; + spec->data = NULL; + spec->mode = 0; + memset(spec->sha1, 0, 20); + return spec; +} + +void fill_filespec(struct diff_filespec *spec, const unsigned char *sha1, + unsigned short mode) +{ + if (mode) { + spec->mode = DIFF_FILE_CANON_MODE(mode); + memcpy(spec->sha1, sha1, 20); + spec->sha1_valid = !!memcmp(sha1, null_sha1, 20); + } +} + +/* + * 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 work_tree_matches(const char *name, const unsigned char *sha1) +{ + 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; + + 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) || + memcmp(sha1, ce->sha1, 20)) + 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 = memcmp(e->sha1, sha1, 20); + 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; + memcpy(e->sha1, sha1, 20); + 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 || + work_tree_matches(s->path, s->sha1)) { + struct stat st; + int fd; + if (lstat(s->path, &st) < 0) { + if (errno == ENOENT) { + err_empty: + err = -1; + empty: + s->data = ""; + 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 = mmap(NULL, s->size, PROT_READ, MAP_PRIVATE, fd, 0); + s->should_munmap = 1; + close(fd); + } + 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; +} + +static void prep_temp_blob(struct diff_tempfile *temp, + void *blob, + unsigned long size, + unsigned char *sha1, + int mode) +{ + int fd; + + strcpy(temp->tmp_path, ".diff_XXXXXX"); + fd = mkstemp(temp->tmp_path); + if (fd < 0) + die("unable to create temp-file"); + if (write(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 || + work_tree_matches(name, one->sha1)) { + 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, buf_[1024]; + buf = ((sizeof(buf_) < st.st_size) ? + xmalloc(st.st_size) : buf_); + 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(); +} + +/* 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) +{ + struct diff_tempfile *temp = diff_temp; + pid_t pid; + int status; + static int atexit_asked = 0; + + if (one && two) { + prepare_temp_file(name, &temp[0], one); + prepare_temp_file(other ? : name, &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); + } + + fflush(NULL); + pid = fork(); + if (pid < 0) + die("unable to fork"); + if (!pid) { + if (pgm) { + if (one && two) { + const char *exec_arg[10]; + const char **arg = &exec_arg[0]; + *arg++ = pgm; + *arg++ = name; + *arg++ = temp[0].name; + *arg++ = temp[0].hex; + *arg++ = temp[0].mode; + *arg++ = temp[1].name; + *arg++ = temp[1].hex; + *arg++ = temp[1].mode; + if (other) { + *arg++ = other; + *arg++ = xfrm_msg; + } + *arg = NULL; + execvp(pgm, (char *const*) exec_arg); + } + else + execlp(pgm, pgm, name, NULL); + } + /* + * otherwise we use the built-in one. + */ + if (one && two) + builtin_diff(name, other ? : name, temp, xfrm_msg, + complete_rewrite); + else + printf("* Unmerged path %s\n", name); + exit(0); + } + if (waitpid(pid, &status, 0) < 0 || + !WIFEXITED(status) || WEXITSTATUS(status)) { + /* Earlier we did not check the exit status because + * diff exits non-zero if files are different, and + * we are not interested in knowing that. It was a + * mistake which made it harder to quit a diff-* + * session that uses the git-apply-patch-script as + * the GIT_EXTERNAL_DIFF. A custom GIT_EXTERNAL_DIFF + * should also exit non-zero only when it wants to + * abort the entire diff-* session. + */ + remove_tempfile(); + fprintf(stderr, "external diff died, stopping at %s.\n", name); + exit(1); + } + remove_tempfile(); +} + +static void run_diff(struct diff_filepair *p) +{ + const char *pgm = external_diff(); + char msg_[PATH_MAX*2+200], *xfrm_msg; + struct diff_filespec *one; + struct diff_filespec *two; + const char *name; + const char *other; + int complete_rewrite = 0; + + if (DIFF_PAIR_UNMERGED(p)) { + /* unmerged */ + run_external_diff(pgm, p->one->path, NULL, NULL, NULL, NULL, + 0); + return; + } + + name = p->one->path; + other = (strcmp(name, p->two->path) ? p->two->path : NULL); + one = p->one; two = p->two; + switch (p->status) { + case 'C': + sprintf(msg_, + "similarity index %d%%\n" + "copy from %s\n" + "copy to %s", + (int)(0.5 + p->score * 100.0/MAX_SCORE), + name, other); + xfrm_msg = msg_; + break; + case 'R': + sprintf(msg_, + "similarity index %d%%\n" + "rename from %s\n" + "rename to %s", + (int)(0.5 + p->score * 100.0/MAX_SCORE), + name, other); + xfrm_msg = msg_; + break; + case 'M': + if (p->score) { + sprintf(msg_, + "dissimilarity index %d%%", + (int)(0.5 + p->score * 100.0/MAX_SCORE)); + xfrm_msg = msg_; + complete_rewrite = 1; + break; + } + /* fallthru */ + default: + xfrm_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_external_diff(NULL, name, other, one, null, xfrm_msg, 0); + free(null); + null = alloc_filespec(one->path); + run_external_diff(NULL, name, other, null, two, xfrm_msg, 0); + free(null); + } + else + run_external_diff(pgm, name, other, one, two, xfrm_msg, + complete_rewrite); +} + +void diff_setup(int flags) +{ + if (flags & DIFF_SETUP_REVERSE) + reverse_diff = 1; + if (flags & 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 oufselves either. + */ + read_cache(); + } + if (flags & DIFF_SETUP_USE_SIZE_CACHE) + use_size_cache = 1; + +} + +static int parse_num(const char **cp_p) +{ + int num, scale, ch, cnt; + const char *cp = *cp_p; + + cnt = num = 0; + scale = 1; + while ('0' <= (ch = *cp) && ch <= '9') { + if (cnt++ < 5) { + /* We simply ignore more than 5 digits precision. */ + scale *= 10; + num = num * 10 + ch - '0'; + } + *cp++; + } + *cp_p = cp; + + /* user says num divided by scale and we say internally that + * is MAX_SCORE * num / scale. + */ + return (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 = xmalloc(sizeof(*dp)); + dp->one = one; + dp->two = two; + dp->score = 0; + dp->status = 0; + dp->source_stays = 0; + dp->broken_pair = 0; + 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); +} + +static void diff_flush_raw(struct diff_filepair *p, + int line_termination, + int inter_name_termination) +{ + int two_paths; + char status[10]; + + if (line_termination) { + const char *err = "path %s cannot be expressed without -z"; + if (strchr(p->one->path, line_termination) || + strchr(p->one->path, inter_name_termination)) + die(err, p->one->path); + if (strchr(p->two->path, line_termination) || + strchr(p->two->path, inter_name_termination)) + die(err, p->two->path); + } + + 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 'C': case 'R': + two_paths = 1; + break; + case 'N': case 'D': + two_paths = 0; + break; + default: + two_paths = 0; + break; + } + printf(":%06o %06o %s ", + p->one->mode, p->two->mode, sha1_to_hex(p->one->sha1)); + printf("%s %s%c%s", + sha1_to_hex(p->two->sha1), + status, + inter_name_termination, + p->one->path); + if (two_paths) + printf("%c%s", inter_name_termination, p->two->path); + putchar(line_termination); +} + +static void diff_flush_name(struct diff_filepair *p, + int line_termination) +{ + printf("%s%c", p->two->path, line_termination); +} + +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 && + !memcmp(one->sha1, two->sha1, sizeof(one->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) +{ + 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); +} + +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 ? : "", + 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 ? : "", + 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->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 = 'U'; + else if (!DIFF_FILE_VALID(p->one)) + p->status = 'N'; + else if (!DIFF_FILE_VALID(p->two)) + p->status = 'D'; + else if (DIFF_PAIR_TYPE_CHANGED(p)) + p->status = 'T'; + + /* 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 = 'C'; + 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 a rename. + */ + 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 = 'C'; + break; + } + if (!p->status) + p->status = 'R'; + } + else if (memcmp(p->one->sha1, p->two->sha1, 20) || + p->one->mode != p->two->mode) + p->status = 'M'; + 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 = 'X'; + } + } + diff_debug_queue("resolve-rename-copy done", q); +} + +void diff_flush(int diff_output_style, int line_termination) +{ + struct diff_queue_struct *q = &diff_queued_diff; + int i; + int inter_name_termination = '\t'; + + if (!line_termination) + inter_name_termination = 0; + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if ((diff_output_style == DIFF_FORMAT_NO_OUTPUT) || + (p->status == 'X')) + continue; + if (p->status == 0) + die("internal error in diff-resolve-rename-copy"); + switch (diff_output_style) { + case DIFF_FORMAT_PATCH: + diff_flush_patch(p); + break; + case DIFF_FORMAT_RAW: + diff_flush_raw(p, line_termination, + inter_name_termination); + break; + case DIFF_FORMAT_NAME: + diff_flush_name(p, line_termination); + break; + } + } + for (i = 0; i < q->nr; i++) + diff_free_filepair(q->queue[i]); + 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, 'A')) { + /* All-or-none */ + int found; + for (i = found = 0; !found && i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (((p->status == 'M') && + ((p->score && strchr(filter, 'B')) || + (!p->score && strchr(filter, 'M')))) || + ((p->status != 'M') && 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 == 'M') && + ((p->score && strchr(filter, 'B')) || + (!p->score && strchr(filter, 'M')))) || + ((p->status != 'M') && strchr(filter, p->status))) + diff_q(&outq, p); + else + diff_free_filepair(p); + } + } + free(q->queue); + *q = outq; +} + +void diffcore_std(const char **paths, + int detect_rename, int rename_score, + const char *pickaxe, int pickaxe_opts, + int break_opt, + const char *orderfile, + const char *filter) +{ + if (paths && paths[0]) + diffcore_pathspec(paths); + if (break_opt != -1) + diffcore_break(break_opt); + if (detect_rename) + diffcore_rename(detect_rename, rename_score); + if (break_opt != -1) + diffcore_merge_broken(); + if (pickaxe) + diffcore_pickaxe(pickaxe, pickaxe_opts); + if (orderfile) + diffcore_order(orderfile); + diff_resolve_rename_copy(); + diffcore_apply_filter(filter); +} + + +void diffcore_std_no_resolve(const char **paths, + const char *pickaxe, int pickaxe_opts, + const char *orderfile, + const char *filter) +{ + if (paths && paths[0]) + diffcore_pathspec(paths); + if (pickaxe) + diffcore_pickaxe(pickaxe, pickaxe_opts); + if (orderfile) + diffcore_order(orderfile); + diffcore_apply_filter(filter); +} + +void diff_addremove(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 (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_helper_input(unsigned old_mode, + unsigned new_mode, + const unsigned char *old_sha1, + const unsigned char *new_sha1, + const char *old_path, + int status, + int score, + const char *new_path) +{ + struct diff_filespec *one, *two; + struct diff_filepair *dp; + + one = alloc_filespec(old_path); + two = alloc_filespec(new_path); + if (old_mode) + fill_filespec(one, old_sha1, old_mode); + if (new_mode) + fill_filespec(two, new_sha1, new_mode); + dp = diff_queue(&diff_queued_diff, one, two); + dp->score = score * MAX_SCORE / 100; + dp->status = status; +} + +void diff_change(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 (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(const char *path) +{ + struct diff_filespec *one, *two; + one = alloc_filespec(path); + two = alloc_filespec(path); + diff_queue(&diff_queued_diff, one, two); +} diff --git a/diff.h b/diff.h new file mode 100644 index 0000000000..e0a3207a62 --- /dev/null +++ b/diff.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#ifndef DIFF_H +#define DIFF_H + +#define DIFF_FILE_CANON_MODE(mode) \ + (S_ISREG(mode) ? (S_IFREG | ce_permissions(mode)) : \ + S_ISLNK(mode) ? S_IFLNK : S_IFDIR) + +extern void diff_addremove(int addremove, + unsigned mode, + const unsigned char *sha1, + const char *base, + const char *path); + +extern void diff_change(unsigned mode1, unsigned mode2, + const unsigned char *sha1, + const unsigned char *sha2, + const char *base, const char *path); + +extern void diff_helper_input(unsigned mode1, + unsigned mode2, + const unsigned char *sha1, + const unsigned char *sha2, + const char *path1, + int status, + int score, + const char *path2); + +extern void diff_unmerge(const char *path); + +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 void diff_setup(int flags); + +#define DIFF_DETECT_RENAME 1 +#define DIFF_DETECT_COPY 2 + +#define DIFF_PICKAXE_ALL 1 + +extern void diffcore_std(const char **paths, + int detect_rename, int rename_score, + const char *pickaxe, int pickaxe_opts, + int break_opt, + const char *orderfile, const char *filter); + +extern void diffcore_std_no_resolve(const char **paths, + const char *pickaxe, int pickaxe_opts, + const char *orderfile, const char *filter); + +#define COMMON_DIFF_OPTIONS_HELP \ +"\ncommon diff options:\n" \ +" -r diff recursively (only meaningful in diff-tree)\n" \ +" -z output diff-raw with lines terminated with NUL.\n" \ +" -p output patch format.\n" \ +" -u synonym for -p.\n" \ +" --name-only show only names of changed files.\n" \ +" --name-only-z\n" \ +" same as --name-only but terminate lines with NUL.\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" \ +" -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" + +extern int diff_queue_is_empty(void); + +#define DIFF_FORMAT_RAW 1 +#define DIFF_FORMAT_PATCH 2 +#define DIFF_FORMAT_NO_OUTPUT 3 +#define DIFF_FORMAT_NAME 4 + +extern void diff_flush(int output_style, int line_terminator); + +#endif /* DIFF_H */ diff --git a/diffcore-break.c b/diffcore-break.c new file mode 100644 index 0000000000..06f9a7f0ee --- /dev/null +++ b/diffcore-break.c @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" +#include "delta.h" +#include "count-delta.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. + */ + void *delta; + unsigned long delta_size, base_size, src_copied, literal_added; + int to_break = 0; + + *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 (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); + + delta = diff_delta(src->data, src->size, + dst->data, dst->size, + &delta_size, 0); + + /* Estimate the edit size by interpreting delta. */ + if (count_delta(delta, delta_size, + &src_copied, &literal_added)) { + free(delta); + return 0; /* we cannot tell */ + } + free(delta); + + /* 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. + */ + if (src->size <= src_copied) + ; /* all copied, nothing removed */ + else { + delta_size = src->size - src_copied; + *merge_score_p = delta_size * MAX_SCORE / src->size; + } + + /* Extent of damage, which counts both inserts and + * deletes. + */ + if (src->size + literal_added <= src_copied) + delta_size = 0; /* avoid wrapping around */ + else + delta_size = (src->size - src_copied) + literal_added; + + /* We break if the edit exceeds the minimum. + * i.e. (break_score / MAX_SCORE < delta_size / base_size) + */ + if (break_score * base_size < delta_size * MAX_SCORE) + to_break = 1; + + return to_break; +} + +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) && + MINIMUM_BREAK_SIZE <= p->one->size) { + /* 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-order.c b/diffcore-order.c new file mode 100644 index 0000000000..a03862c1ce --- /dev/null +++ b/diffcore-order.c @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" +#include <fnmatch.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 (-1 == (int)(long)map) + return; + endp = 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 = xmalloc(sizeof(*o) * q->nr); + int i; + + 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-pathspec.c b/diffcore-pathspec.c new file mode 100644 index 0000000000..a48acbc965 --- /dev/null +++ b/diffcore-pathspec.c @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" + +struct path_spec { + const char *spec; + int len; +}; + +static int matches_pathspec(const char *name, struct path_spec *s, int cnt) +{ + int i; + int namelen; + + if (cnt == 0) + return 1; + + namelen = strlen(name); + for (i = 0; i < cnt; i++) { + int len = s[i].len; + if (namelen < len) + continue; + if (memcmp(s[i].spec, name, len)) + continue; + if (s[i].spec[len-1] == '/' || + name[len] == 0 || + name[len] == '/') + return 1; + } + return 0; +} + +void diffcore_pathspec(const char **pathspec) +{ + struct diff_queue_struct *q = &diff_queued_diff; + int i, speccnt; + struct diff_queue_struct outq; + struct path_spec *spec; + + outq.queue = NULL; + outq.nr = outq.alloc = 0; + + for (i = 0; pathspec[i]; i++) + ; + speccnt = i; + spec = xmalloc(sizeof(*spec) * speccnt); + for (i = 0; pathspec[i]; i++) { + spec[i].spec = pathspec[i]; + spec[i].len = strlen(pathspec[i]); + } + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (matches_pathspec(p->two->path, spec, speccnt)) + diff_q(&outq, p); + else + diff_free_filepair(p); + } + free(q->queue); + *q = outq; + return; +} diff --git a/diffcore-pickaxe.c b/diffcore-pickaxe.c new file mode 100644 index 0000000000..4c26b422f9 --- /dev/null +++ b/diffcore-pickaxe.c @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" + +static int contains(struct diff_filespec *one, + const char *needle, unsigned long len) +{ + unsigned long offset, sz; + const char *data; + if (diff_populate_filespec(one, 0)) + return 0; + sz = one->size; + data = one->data; + for (offset = 0; offset + len <= sz; offset++) + if (!strncmp(needle, data + offset, len)) + return 1; + return 0; +} + +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; + struct diff_queue_struct outq; + outq.queue = NULL; + outq.nr = outq.alloc = 0; + + 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)) + has_changes++; + } + else if (!DIFF_FILE_VALID(p->two)) { + if (contains(p->one, needle, len)) + has_changes++; + } + else if (!diff_unmodified_pair(p) && + contains(p->one, needle, len) != + contains(p->two, needle, len)) + 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)) + has_changes = 1; + } + else if (!DIFF_FILE_VALID(p->two)) { + if (contains(p->one, needle, len)) + has_changes = 1; + } + else if (!diff_unmodified_pair(p) && + contains(p->one, needle, len) != + contains(p->two, needle, len)) + has_changes = 1; + + if (has_changes) + diff_q(&outq, p); + else + diff_free_filepair(p); + } + + free(q->queue); + *q = outq; + return; +} diff --git a/diffcore-rename.c b/diffcore-rename.c new file mode 100644 index 0000000000..6a52699f73 --- /dev/null +++ b/diffcore-rename.c @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "diff.h" +#include "diffcore.h" +#include "delta.h" +#include "count-delta.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 = two; + 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 src_stays : 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_stays) +{ + 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].src_stays = src_stays; + return &(rename_src[first]); +} + +static int is_exact_match(struct diff_filespec *src, struct diff_filespec *dst) +{ + if (src->sha1_valid && dst->sha1_valid && + !memcmp(src->sha1, dst->sha1, 20)) + return 1; + if (diff_populate_filespec(src, 1) || diff_populate_filespec(dst, 1)) + return 0; + if (src->size != dst->size) + return 0; + 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. + */ + void *delta; + unsigned long 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; + + delta_size = ((src->size < dst->size) ? + (dst->size - src->size) : (src->size - dst->size)); + base_size = ((src->size < dst->size) ? src->size : dst->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; + delta = diff_delta(src->data, src->size, + dst->data, dst->size, + &delta_size, delta_limit); + if (!delta) + /* If delta_limit is exceeded, we have too much differences */ + return 0; + + /* A delta that has a lot of literal additions would have + * big delta_size no matter what else it does. + */ + if (base_size * (MAX_SCORE-minimum_score) < delta_size * MAX_SCORE) + return 0; + + /* Estimate the edit size by interpreting delta. */ + if (count_delta(delta, delta_size, &src_copied, &literal_added)) { + free(delta); + return 0; + } + free(delta); + + /* Extent of damage */ + if (src->size + literal_added < src_copied) + delta_size = 0; + else + delta_size = (src->size - src_copied) + literal_added; + + /* + * Now we will give some score to it. 100% edit gets 0 points + * and 0% edit gets MAX_SCORE points. + */ + score = MAX_SCORE - (MAX_SCORE * delta_size / base_size); + if (score < 0) return 0; + if (MAX_SCORE < score) return MAX_SCORE; + return score; +} + +static void record_rename_pair(struct diff_queue_struct *renq, + 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(renq, one, two); + dp->score = score; + dp->source_stays = rename_src[src_index].src_stays; + 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; +} + +void diffcore_rename(int detect_rename, int minimum_score) +{ + struct diff_queue_struct *q = &diff_queued_diff; + struct diff_queue_struct renq, outq; + struct diff_score *mx; + int i, j; + int num_create, num_src, dst_cnt; + + if (!minimum_score) + minimum_score = DEFAULT_RENAME_SCORE; + renq.queue = NULL; + renq.nr = renq.alloc = 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 + 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); + } + else if (detect_rename == DIFF_DETECT_COPY) + register_rename_src(p->one, 1); + } + if (rename_dst_nr == 0) + goto cleanup; /* nothing to do */ + + /* We really want to cull the candidates list early + * with cheap tests in order to avoid doing deltas. + */ + for (i = 0; i < rename_dst_nr; i++) { + struct diff_filespec *two = rename_dst[i].two; + for (j = 0; j < rename_src_nr; j++) { + struct diff_filespec *one = rename_src[j].one; + if (!is_exact_match(one, two)) + continue; + record_rename_pair(&renq, i, j, MAX_SCORE); + break; /* we are done with this entry */ + } + } + diff_debug_queue("done detecting exact", &renq); + + /* Have we run out the created file pool? If so we can avoid + * doing the delta matrix altogether. + */ + if (renq.nr == rename_dst_nr) + goto cleanup; + + num_create = (rename_dst_nr - renq.nr); + 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); + } + 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(&renq, mx[i].dst, mx[i].src, mx[i].score); + } + free(mx); + diff_debug_queue("done detecting fuzzy", &renq); + + cleanup: + /* At this point, we have found some renames and copies and they + * are kept in renq. 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 renq does + * not have a rename/copy to move p->one->path + * out. + * + * 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 < renq.nr; j++) + if (!strcmp(renq.queue[j]->one->path, + p->one->path)) + break; + if (j < renq.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(renq.queue); + free(q->queue); + *q = outq; + diff_debug_queue("done collapsing", q); + + 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..f1b5ca748c --- /dev/null +++ b/diffcore.h @@ -0,0 +1,104 @@ +/* + * 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 +#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 48000 /* maximum for break-merge to happen (80%)*/ + +#define MINIMUM_BREAK_SIZE 400 /* do not break a file smaller than this */ + +struct diff_filespec { + unsigned char sha1[20]; + char *path; + void *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; +}; +#define DIFF_PAIR_UNMERGED(p) \ + (!DIFF_FILE_VALID((p)->one) && !DIFF_FILE_VALID((p)->two)) + +#define DIFF_PAIR_RENAME(p) (strcmp((p)->one->path, (p)->two->path)) + +#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(int rename_copy, int); +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 + +#endif diff --git a/entry.c b/entry.c new file mode 100644 index 0000000000..15b8fda0c6 --- /dev/null +++ b/entry.c @@ -0,0 +1,156 @@ +#include <sys/types.h> +#include <dirent.h> +#include "cache.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_TRUNC | O_CREAT | O_EXCL, mode); +} + +static int write_entry(struct cache_entry *ce, const char *path, struct checkout *state) +{ + int fd; + void *new; + unsigned long size; + long wrote; + char type[20]; + char target[1024]; + + new = read_sha1_file(ce->sha1, type, &size); + if (!new || strcmp(type, "blob")) { + if (new) + free(new); + return error("git-checkout-cache: unable to read sha1 file of %s (%s)", + path, sha1_to_hex(ce->sha1)); + } + switch (ntohl(ce->ce_mode) & S_IFMT) { + case S_IFREG: + fd = create_file(path, ntohl(ce->ce_mode)); + if (fd < 0) { + free(new); + return error("git-checkout-cache: unable to create file %s (%s)", + path, strerror(errno)); + } + wrote = write(fd, new, size); + close(fd); + free(new); + if (wrote != size) + return error("git-checkout-cache: unable to write file %s", path); + break; + case S_IFLNK: + memcpy(target, new, size); + target[size] = '\0'; + if (symlink(target, path)) { + free(new); + return error("git-checkout-cache: unable to create symlink %s (%s)", + path, strerror(errno)); + } + free(new); + break; + default: + free(new); + return error("git-checkout-cache: 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) +{ + struct stat st; + static char path[MAXPATHLEN+1]; + int len = state->base_dir_len; + + memcpy(path, state->base_dir, len); + strcpy(path + len, ce->name); + + if (!lstat(path, &st)) { + unsigned changed = ce_match_stat(ce, &st); + if (!changed) + return 0; + if (!state->force) { + if (!state->quiet) + fprintf(stderr, "git-checkout-cache: %s already exists\n", path); + return 0; + } + + /* + * 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); +} + + diff --git a/epoch.c b/epoch.c new file mode 100644 index 0000000000..db44f5ca9f --- /dev/null +++ b/epoch.c @@ -0,0 +1,639 @@ +/* + * Copyright (c) 2005, Jon Seymour + * + * For more information about epoch theory on which this module is based, + * refer to http://blackcubes.dyndns.org/epoch/. That web page defines + * terms such as "epoch" and "minimal, non-linear epoch" and provides rationales + * for some of the algorithms used here. + * + */ +#include <stdlib.h> + +/* Provides arbitrary precision integers required to accurately represent + * fractional mass: */ +#include <openssl/bn.h> + +#include "cache.h" +#include "commit.h" +#include "epoch.h" + +struct fraction { + BIGNUM numerator; + BIGNUM denominator; +}; + +#define HAS_EXACTLY_ONE_PARENT(n) ((n)->parents && !(n)->parents->next) + +static BN_CTX *context = NULL; +static struct fraction *one = NULL; +static struct fraction *zero = NULL; + +static BN_CTX *get_BN_CTX(void) +{ + if (!context) { + context = BN_CTX_new(); + } + return context; +} + +static struct fraction *new_zero(void) +{ + struct fraction *result = xmalloc(sizeof(*result)); + BN_init(&result->numerator); + BN_init(&result->denominator); + BN_zero(&result->numerator); + BN_one(&result->denominator); + return result; +} + +static void clear_fraction(struct fraction *fraction) +{ + BN_clear(&fraction->numerator); + BN_clear(&fraction->denominator); +} + +static struct fraction *divide(struct fraction *result, struct fraction *fraction, int divisor) +{ + BIGNUM bn_divisor; + + BN_init(&bn_divisor); + BN_set_word(&bn_divisor, divisor); + + BN_copy(&result->numerator, &fraction->numerator); + BN_mul(&result->denominator, &fraction->denominator, &bn_divisor, get_BN_CTX()); + + BN_clear(&bn_divisor); + return result; +} + +static struct fraction *init_fraction(struct fraction *fraction) +{ + BN_init(&fraction->numerator); + BN_init(&fraction->denominator); + BN_zero(&fraction->numerator); + BN_one(&fraction->denominator); + return fraction; +} + +static struct fraction *get_one(void) +{ + if (!one) { + one = new_zero(); + BN_one(&one->numerator); + } + return one; +} + +static struct fraction *get_zero(void) +{ + if (!zero) { + zero = new_zero(); + } + return zero; +} + +static struct fraction *copy(struct fraction *to, struct fraction *from) +{ + BN_copy(&to->numerator, &from->numerator); + BN_copy(&to->denominator, &from->denominator); + return to; +} + +static struct fraction *add(struct fraction *result, struct fraction *left, struct fraction *right) +{ + BIGNUM a, b, gcd; + + BN_init(&a); + BN_init(&b); + BN_init(&gcd); + + BN_mul(&a, &left->numerator, &right->denominator, get_BN_CTX()); + BN_mul(&b, &left->denominator, &right->numerator, get_BN_CTX()); + BN_mul(&result->denominator, &left->denominator, &right->denominator, get_BN_CTX()); + BN_add(&result->numerator, &a, &b); + + BN_gcd(&gcd, &result->denominator, &result->numerator, get_BN_CTX()); + BN_div(&result->denominator, NULL, &result->denominator, &gcd, get_BN_CTX()); + BN_div(&result->numerator, NULL, &result->numerator, &gcd, get_BN_CTX()); + + BN_clear(&a); + BN_clear(&b); + BN_clear(&gcd); + + return result; +} + +static int compare(struct fraction *left, struct fraction *right) +{ + BIGNUM a, b; + int result; + + BN_init(&a); + BN_init(&b); + + BN_mul(&a, &left->numerator, &right->denominator, get_BN_CTX()); + BN_mul(&b, &left->denominator, &right->numerator, get_BN_CTX()); + + result = BN_cmp(&a, &b); + + BN_clear(&a); + BN_clear(&b); + + return result; +} + +struct mass_counter { + struct fraction seen; + struct fraction pending; +}; + +static struct mass_counter *new_mass_counter(struct commit *commit, struct fraction *pending) +{ + struct mass_counter *mass_counter = xmalloc(sizeof(*mass_counter)); + memset(mass_counter, 0, sizeof(*mass_counter)); + + init_fraction(&mass_counter->seen); + init_fraction(&mass_counter->pending); + + copy(&mass_counter->pending, pending); + copy(&mass_counter->seen, get_zero()); + + if (commit->object.util) { + die("multiple attempts to initialize mass counter for %s", + sha1_to_hex(commit->object.sha1)); + } + + commit->object.util = mass_counter; + + return mass_counter; +} + +static void free_mass_counter(struct mass_counter *counter) +{ + clear_fraction(&counter->seen); + clear_fraction(&counter->pending); + free(counter); +} + +/* + * Finds the base commit of a list of commits. + * + * One property of the commit being searched for is that every commit reachable + * from the base commit is reachable from the commits in the starting list only + * via paths that include the base commit. + * + * This algorithm uses a conservation of mass approach to find the base commit. + * + * We start by injecting one unit of mass into the graph at each + * of the commits in the starting list. Injecting mass into a commit + * is achieved by adding to its pending mass counter and, if it is not already + * enqueued, enqueuing the commit in a list of pending commits, in latest + * commit date first order. + * + * The algorithm then preceeds to visit each commit in the pending queue. + * Upon each visit, the pending mass is added to the mass already seen for that + * commit and then divided into N equal portions, where N is the number of + * parents of the commit being visited. The divided portions are then injected + * into each of the parents. + * + * The algorithm continues until we discover a commit which has seen all the + * mass originally injected or until we run out of things to do. + * + * If we find a commit that has seen all the original mass, we have found + * the common base of all the commits in the starting list. + * + * The algorithm does _not_ depend on accurate timestamps for correct operation. + * However, reasonably sane (e.g. non-random) timestamps are required in order + * to prevent an exponential performance characteristic. The occasional + * timestamp inaccuracy will not dramatically affect performance but may + * result in more nodes being processed than strictly necessary. + * + * This procedure sets *boundary to the address of the base commit. It returns + * non-zero if, and only if, there was a problem parsing one of the + * commits discovered during the traversal. + */ +static int find_base_for_list(struct commit_list *list, struct commit **boundary) +{ + int ret = 0; + struct commit_list *cleaner = NULL; + struct commit_list *pending = NULL; + struct fraction injected; + init_fraction(&injected); + *boundary = NULL; + + for (; list; list = list->next) { + struct commit *item = list->item; + + if (!item->object.util) { + new_mass_counter(list->item, get_one()); + add(&injected, &injected, get_one()); + + commit_list_insert(list->item, &cleaner); + commit_list_insert(list->item, &pending); + } + } + + while (!*boundary && pending && !ret) { + struct commit *latest = pop_commit(&pending); + struct mass_counter *latest_node = (struct mass_counter *) latest->object.util; + int num_parents; + + if ((ret = parse_commit(latest))) + continue; + add(&latest_node->seen, &latest_node->seen, &latest_node->pending); + + num_parents = count_parents(latest); + if (num_parents) { + struct fraction distribution; + struct commit_list *parents; + + divide(init_fraction(&distribution), &latest_node->pending, num_parents); + + for (parents = latest->parents; parents; parents = parents->next) { + struct commit *parent = parents->item; + struct mass_counter *parent_node = (struct mass_counter *) parent->object.util; + + if (!parent_node) { + parent_node = new_mass_counter(parent, &distribution); + insert_by_date(parent, &pending); + commit_list_insert(parent, &cleaner); + } else { + if (!compare(&parent_node->pending, get_zero())) + insert_by_date(parent, &pending); + add(&parent_node->pending, &parent_node->pending, &distribution); + } + } + + clear_fraction(&distribution); + } + + if (!compare(&latest_node->seen, &injected)) + *boundary = latest; + copy(&latest_node->pending, get_zero()); + } + + while (cleaner) { + struct commit *next = pop_commit(&cleaner); + free_mass_counter((struct mass_counter *) next->object.util); + next->object.util = NULL; + } + + if (pending) + free_commit_list(pending); + + clear_fraction(&injected); + return ret; +} + + +/* + * Finds the base of an minimal, non-linear epoch, headed at head, by + * applying the find_base_for_list to a list consisting of the parents + */ +static int find_base(struct commit *head, struct commit **boundary) +{ + int ret = 0; + struct commit_list *pending = NULL; + struct commit_list *next; + + for (next = head->parents; next; next = next->next) { + commit_list_insert(next->item, &pending); + } + ret = find_base_for_list(pending, boundary); + free_commit_list(pending); + + return ret; +} + +/* + * This procedure traverses to the boundary of the first epoch in the epoch + * sequence of the epoch headed at head_of_epoch. This is either the end of + * the maximal linear epoch or the base of a minimal non-linear epoch. + * + * The queue of pending nodes is sorted in reverse date order and each node + * is currently in the queue at most once. + */ +static int find_next_epoch_boundary(struct commit *head_of_epoch, struct commit **boundary) +{ + int ret; + struct commit *item = head_of_epoch; + + ret = parse_commit(item); + if (ret) + return ret; + + if (HAS_EXACTLY_ONE_PARENT(item)) { + /* + * We are at the start of a maximimal linear epoch. + * Traverse to the end. + */ + while (HAS_EXACTLY_ONE_PARENT(item) && !ret) { + item = item->parents->item; + ret = parse_commit(item); + } + *boundary = item; + + } else { + /* + * Otherwise, we are at the start of a minimal, non-linear + * epoch - find the common base of all parents. + */ + ret = find_base(item, boundary); + } + + return ret; +} + +/* + * Returns non-zero if parent is known to be a parent of child. + */ +static int is_parent_of(struct commit *parent, struct commit *child) +{ + struct commit_list *parents; + for (parents = child->parents; parents; parents = parents->next) { + if (!memcmp(parent->object.sha1, parents->item->object.sha1, + sizeof(parents->item->object.sha1))) + return 1; + } + return 0; +} + +/* + * Pushes an item onto the merge order stack. If the top of the stack is + * marked as being a possible "break", we check to see whether it actually + * is a break. + */ +static void push_onto_merge_order_stack(struct commit_list **stack, struct commit *item) +{ + struct commit_list *top = *stack; + if (top && (top->item->object.flags & DISCONTINUITY)) { + if (is_parent_of(top->item, item)) { + top->item->object.flags &= ~DISCONTINUITY; + } + } + commit_list_insert(item, stack); +} + +/* + * Marks all interesting, visited commits reachable from this commit + * as uninteresting. We stop recursing when we reach the epoch boundary, + * an unvisited node or a node that has already been marking uninteresting. + * + * This doesn't actually mark all ancestors between the start node and the + * epoch boundary uninteresting, but does ensure that they will eventually + * be marked uninteresting when the main sort_first_epoch() traversal + * eventually reaches them. + */ +static void mark_ancestors_uninteresting(struct commit *commit) +{ + unsigned int flags = commit->object.flags; + int visited = flags & VISITED; + int boundary = flags & BOUNDARY; + int uninteresting = flags & UNINTERESTING; + struct commit_list *next; + + commit->object.flags |= UNINTERESTING; + + /* + * We only need to recurse if + * we are not on the boundary and + * we have not already been marked uninteresting and + * we have already been visited. + * + * The main sort_first_epoch traverse will mark unreachable + * all uninteresting, unvisited parents as they are visited + * so there is no need to duplicate that traversal here. + * + * Similarly, if we are already marked uninteresting + * then either all ancestors have already been marked + * uninteresting or will be once the sort_first_epoch + * traverse reaches them. + */ + + if (uninteresting || boundary || !visited) + return; + + for (next = commit->parents; next; next = next->next) + mark_ancestors_uninteresting(next->item); +} + +/* + * Sorts the nodes of the first epoch of the epoch sequence of the epoch headed at head + * into merge order. + */ +static void sort_first_epoch(struct commit *head, struct commit_list **stack) +{ + struct commit_list *parents; + + head->object.flags |= VISITED; + + /* + * TODO: By sorting the parents in a different order, we can alter the + * merge order to show contemporaneous changes in parallel branches + * occurring after "local" changes. This is useful for a developer + * when a developer wants to see all changes that were incorporated + * into the same merge as her own changes occur after her own + * changes. + */ + + for (parents = head->parents; parents; parents = parents->next) { + struct commit *parent = parents->item; + + if (head->object.flags & UNINTERESTING) { + /* + * Propagates the uninteresting bit to all parents. + * if we have already visited this parent, then + * the uninteresting bit will be propagated to each + * reachable commit that is still not marked + * uninteresting and won't otherwise be reached. + */ + mark_ancestors_uninteresting(parent); + } + + if (!(parent->object.flags & VISITED)) { + if (parent->object.flags & BOUNDARY) { + if (*stack) { + die("something else is on the stack - %s", + sha1_to_hex((*stack)->item->object.sha1)); + } + push_onto_merge_order_stack(stack, parent); + parent->object.flags |= VISITED; + + } else { + sort_first_epoch(parent, stack); + if (parents) { + /* + * This indicates a possible + * discontinuity it may not be be + * actual discontinuity if the head + * of parent N happens to be the tail + * of parent N+1. + * + * The next push onto the stack will + * resolve the question. + */ + (*stack)->item->object.flags |= DISCONTINUITY; + } + } + } + } + + push_onto_merge_order_stack(stack, head); +} + +/* + * Emit the contents of the stack. + * + * The stack is freed and replaced by NULL. + * + * Sets the return value to STOP if no further output should be generated. + */ +static int emit_stack(struct commit_list **stack, emitter_func emitter, int include_last) +{ + unsigned int seen = 0; + int action = CONTINUE; + + while (*stack && (action != STOP)) { + struct commit *next = pop_commit(stack); + seen |= next->object.flags; + if (*stack || include_last) { + if (!*stack) + next->object.flags |= BOUNDARY; + action = emitter(next); + } + } + + if (*stack) { + free_commit_list(*stack); + *stack = NULL; + } + + return (action == STOP || (seen & UNINTERESTING)) ? STOP : CONTINUE; +} + +/* + * Sorts an arbitrary epoch into merge order by sorting each epoch + * of its epoch sequence into order. + * + * Note: this algorithm currently leaves traces of its execution in the + * object flags of nodes it discovers. This should probably be fixed. + */ +static int sort_in_merge_order(struct commit *head_of_epoch, emitter_func emitter) +{ + struct commit *next = head_of_epoch; + int ret = 0; + int action = CONTINUE; + + ret = parse_commit(head_of_epoch); + + next->object.flags |= BOUNDARY; + + while (next && next->parents && !ret && (action != STOP)) { + struct commit *base = NULL; + + ret = find_next_epoch_boundary(next, &base); + if (ret) + return ret; + next->object.flags |= BOUNDARY; + if (base) + base->object.flags |= BOUNDARY; + + if (HAS_EXACTLY_ONE_PARENT(next)) { + while (HAS_EXACTLY_ONE_PARENT(next) + && (action != STOP) + && !ret) { + if (next->object.flags & UNINTERESTING) { + action = STOP; + } else { + action = emitter(next); + } + if (action != STOP) { + next = next->parents->item; + ret = parse_commit(next); + } + } + + } else { + struct commit_list *stack = NULL; + sort_first_epoch(next, &stack); + action = emit_stack(&stack, emitter, (base == NULL)); + next = base; + } + } + + if (next && (action != STOP) && !ret) { + emitter(next); + } + + return ret; +} + +/* + * Sorts the nodes reachable from a starting list in merge order, we + * first find the base for the starting list and then sort all nodes + * in this subgraph using the sort_first_epoch algorithm. Once we have + * reached the base we can continue sorting using sort_in_merge_order. + */ +int sort_list_in_merge_order(struct commit_list *list, emitter_func emitter) +{ + struct commit_list *stack = NULL; + struct commit *base; + int ret = 0; + int action = CONTINUE; + struct commit_list *reversed = NULL; + + for (; list; list = list->next) + commit_list_insert(list->item, &reversed); + + if (!reversed) + return ret; + else if (!reversed->next) { + /* + * If there is only one element in the list, we can sort it + * using sort_in_merge_order. + */ + base = reversed->item; + } else { + /* + * Otherwise, we search for the base of the list. + */ + ret = find_base_for_list(reversed, &base); + if (ret) + return ret; + if (base) + base->object.flags |= BOUNDARY; + + while (reversed) { + struct commit * next = pop_commit(&reversed); + + if (!(next->object.flags & VISITED) && next!=base) { + sort_first_epoch(next, &stack); + if (reversed) { + /* + * If we have more commits + * to push, then the first + * push for the next parent may + * (or may * not) represent a + * discontinuity with respect + * to the parent currently on + * the top of the stack. + * + * Mark it for checking here, + * and check it with the next + * push. See sort_first_epoch() + * for more details. + */ + stack->item->object.flags |= DISCONTINUITY; + } + } + } + + action = emit_stack(&stack, emitter, (base==NULL)); + } + + if (base && (action != STOP)) { + ret = sort_in_merge_order(base, emitter); + } + + return ret; +} diff --git a/epoch.h b/epoch.h new file mode 100644 index 0000000000..7493d5a241 --- /dev/null +++ b/epoch.h @@ -0,0 +1,21 @@ +#ifndef EPOCH_H +#define EPOCH_H + + +// return codes for emitter_func +#define STOP 0 +#define CONTINUE 1 +#define DO 2 +typedef int (*emitter_func) (struct commit *); + +int sort_list_in_merge_order(struct commit_list *list, emitter_func emitter); + +/* Low bits are used by rev-list */ +#define UNINTERESTING (1u<<10) +#define BOUNDARY (1u<<11) +#define VISITED (1u<<12) +#define DISCONTINUITY (1u<<13) +#define LAST_EPOCH_FLAG (1u<<14) + + +#endif /* EPOCH_H */ diff --git a/export.c b/export.c new file mode 100644 index 0000000000..ce10b5a298 --- /dev/null +++ b/export.c @@ -0,0 +1,81 @@ +#include "cache.h" +#include "commit.h" + +/* + * Show one commit + */ +static void show_commit(struct commit *commit) +{ + char cmdline[400]; + char hex[100]; + + strcpy(hex, sha1_to_hex(commit->object.sha1)); + printf("Id: %s\n", hex); + fflush(NULL); + sprintf(cmdline, "git-cat-file commit %s", hex); + system(cmdline); + if (commit->parents) { + char *against = sha1_to_hex(commit->parents->item->object.sha1); + printf("\n\n======== diff against %s ========\n", against); + fflush(NULL); + sprintf(cmdline, "git-diff-tree -p %s %s", against, hex); + system(cmdline); + } + printf("======== end ========\n\n"); +} + +/* + * Show all unseen commits, depth-first + */ +static void show_unseen(struct commit *top) +{ + struct commit_list *parents; + + if (top->object.flags & 2) + return; + top->object.flags |= 2; + parents = top->parents; + while (parents) { + show_unseen(parents->item); + parents = parents->next; + } + show_commit(top); +} + +static void export(struct commit *top, struct commit *base) +{ + mark_reachable(&top->object, 1); + if (base) + mark_reachable(&base->object, 2); + show_unseen(top); +} + +static struct commit *get_commit(unsigned char *sha1) +{ + struct commit *commit = lookup_commit(sha1); + if (!commit->object.parsed) { + struct commit_list *parents; + + if (parse_commit(commit) < 0) + die("unable to parse commit %s", sha1_to_hex(sha1)); + parents = commit->parents; + while (parents) { + get_commit(parents->item->object.sha1); + parents = parents->next; + } + } + return commit; +} + +int main(int argc, char **argv) +{ + unsigned char base_sha1[20]; + unsigned char top_sha1[20]; + + if (argc < 2 || argc > 4 || + get_sha1(argv[1], top_sha1) || + (argc == 3 && get_sha1(argv[2], base_sha1))) + usage("git-export top [base]"); + export(get_commit(top_sha1), argc==3 ? get_commit(base_sha1) : NULL); + return 0; +} diff --git a/fetch-pack.c b/fetch-pack.c new file mode 100644 index 0000000000..65e007639a --- /dev/null +++ b/fetch-pack.c @@ -0,0 +1,144 @@ +#include "cache.h" +#include "refs.h" +#include "pkt-line.h" +#include <sys/wait.h> + +static int quiet; +static const char fetch_pack_usage[] = "git-fetch-pack [-q] [--exec=upload-pack] [host:]directory [heads]* < mycommitlist"; +static const char *exec = "git-upload-pack"; + +static int find_common(int fd[2], unsigned char *result_sha1, unsigned char *remote) +{ + static char line[1000]; + int count = 0, flushes = 0, retval; + FILE *revs; + + revs = popen("git-rev-list $(git-rev-parse --all)", "r"); + if (!revs) + die("unable to run 'git-rev-list'"); + packet_write(fd[1], "want %s\n", sha1_to_hex(remote)); + packet_flush(fd[1]); + flushes = 1; + retval = -1; + while (fgets(line, sizeof(line), revs) != NULL) { + unsigned char sha1[20]; + if (get_sha1_hex(line, sha1)) + die("git-fetch-pack: expected object name, got crud"); + packet_write(fd[1], "have %s\n", sha1_to_hex(sha1)); + if (!(31 & ++count)) { + 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; + if (get_ack(fd[0], result_sha1)) { + flushes = 0; + retval = 0; + break; + } + flushes--; + } + } + pclose(revs); + packet_write(fd[1], "done\n"); + while (flushes) { + flushes--; + if (get_ack(fd[0], result_sha1)) + return 0; + } + return retval; +} + +/* + * Eventually we'll want to be able to fetch multiple heads. + * + * Right now we'll just require a single match. + */ +static int fetch_pack(int fd[2], int nr_match, char **match) +{ + struct ref *ref; + unsigned char sha1[20]; + int status; + pid_t pid; + + get_remote_heads(fd[0], &ref, nr_match, match); + if (!ref) { + packet_flush(fd[1]); + die("no matching remote head"); + } + if (ref->next) { + packet_flush(fd[1]); + die("multiple remote heads"); + } + if (find_common(fd, sha1, ref->old_sha1) < 0) + die("git-fetch-pack: no common commits"); + pid = fork(); + if (pid < 0) + die("git-fetch-pack: unable to fork off git-unpack-objects"); + if (!pid) { + dup2(fd[0], 0); + close(fd[0]); + close(fd[1]); + execlp("git-unpack-objects", "git-unpack-objects", + quiet ? "-q" : NULL, NULL); + die("git-unpack-objects exec failed"); + } + close(fd[0]); + close(fd[1]); + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) + die("waiting for git-unpack-objects: %s", strerror(errno)); + } + if (WIFEXITED(status)) { + int code = WEXITSTATUS(status); + if (code) + die("git-unpack-objects died with error code %d", code); + puts(sha1_to_hex(ref->old_sha1)); + return 0; + } + if (WIFSIGNALED(status)) { + int sig = WTERMSIG(status); + die("git-unpack-objects died of signal %d", sig); + } + die("Sherlock Holmes! git-unpack-objects died of unnatural causes %d!", status); +} + +int main(int argc, char **argv) +{ + int i, ret, nr_heads; + char *dest = NULL, **heads; + int fd[2]; + pid_t pid; + + nr_heads = 0; + heads = NULL; + for (i = 1; i < argc; i++) { + char *arg = argv[i]; + + if (*arg == '-') { + if (!strncmp("--exec=", arg, 7)) { + exec = arg + 7; + 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, exec); + if (pid < 0) + return 1; + ret = fetch_pack(fd, nr_heads, heads); + close(fd[0]); + close(fd[1]); + finish_connect(pid); + return ret; +} diff --git a/fsck-cache.c b/fsck-cache.c new file mode 100644 index 0000000000..8e21bf1327 --- /dev/null +++ b/fsck-cache.c @@ -0,0 +1,490 @@ +#include <sys/types.h> +#include <dirent.h> + +#include "cache.h" +#include "commit.h" +#include "tree.h" +#include "blob.h" +#include "tag.h" +#include "refs.h" +#include "pack.h" + +#define REACHABLE 0x0001 + +static int show_root = 0; +static int show_tags = 0; +static int show_unreachable = 0; +static int standalone = 0; +static int check_full = 0; +static int keep_cache_objects = 0; +static unsigned char head_sha1[20]; + +static void check_connectivity(void) +{ + int i; + + /* Look up all the requirements, warn about missing objects.. */ + for (i = 0; i < nr_objs; i++) { + struct object *obj = objs[i]; + struct object_list *refs; + + if (!obj->parsed) { + if (!standalone && has_sha1_file(obj->sha1)) + ; /* it is in pack */ + else + printf("missing %s %s\n", + obj->type, sha1_to_hex(obj->sha1)); + continue; + } + + for (refs = obj->refs; refs; refs = refs->next) { + if (refs->item->parsed || + (!standalone && has_sha1_file(refs->item->sha1))) + continue; + printf("broken link from %7s %s\n", + obj->type, sha1_to_hex(obj->sha1)); + printf(" to %7s %s\n", + refs->item->type, sha1_to_hex(refs->item->sha1)); + } + + if (show_unreachable && !(obj->flags & REACHABLE)) { + printf("unreachable %s %s\n", + obj->type, sha1_to_hex(obj->sha1)); + continue; + } + + if (!obj->used) { + printf("dangling %s %s\n", obj->type, + sha1_to_hex(obj->sha1)); + } + } +} + +/* + * 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(struct tree_entry_list *a, struct tree_entry_list *b) +{ + int len1 = strlen(a->name); + int len2 = strlen(b->name); + int len = len1 < len2 ? len1 : len2; + unsigned char c1, c2; + int cmp; + + cmp = memcmp(a->name, b->name, 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 = a->name[len]; + c2 = b->name[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 && a->directory) + c1 = '/'; + if (!c2 && b->directory) + c2 = '/'; + return c1 < c2 ? 0 : TREE_UNORDERED; +} + +static int fsck_tree(struct tree *item) +{ + int has_full_path = 0; + struct tree_entry_list *entry, *last; + + last = NULL; + for (entry = item->entries; entry; entry = entry->next) { + if (strchr(entry->name, '/')) + has_full_path = 1; + + switch (entry->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: + break; + default: + printf("tree %s has entry %o %s\n", + sha1_to_hex(item->object.sha1), + entry->mode, entry->name); + } + + if (last) { + switch (verify_ordered(last, entry)) { + case TREE_UNORDERED: + fprintf(stderr, "tree %s not ordered\n", + sha1_to_hex(item->object.sha1)); + return -1; + case TREE_HAS_DUPS: + fprintf(stderr, "tree %s has duplicate entries for '%s'\n", + sha1_to_hex(item->object.sha1), + entry->name); + return -1; + default: + break; + } + } + + last = entry; + } + + if (has_full_path) { + fprintf(stderr, "warning: git-fsck-cache: tree %s " + "has full pathnames in it\n", + sha1_to_hex(item->object.sha1)); + } + + return 0; +} + +static int fsck_commit(struct commit *commit) +{ + free(commit->buffer); + commit->buffer = NULL; + if (!commit->tree) + return -1; + 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) { + printf("bad object in tag %s\n", sha1_to_hex(tag->object.sha1)); + return -1; + } + if (!show_tags) + return 0; + + printf("tagged %s %s", 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 -1; + if (obj->type == blob_type) + return 0; + if (obj->type == tree_type) + return fsck_tree((struct tree *) obj); + if (obj->type == commit_type) + return fsck_commit((struct commit *) obj); + if (obj->type == tag_type) + return fsck_tag((struct tag *) obj); + return -1; +} + +/* + * 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; + + 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; + if (fsck_sha1(sha1) < 0) + fprintf(stderr, "bad sha1 entry '%s'\n", sha1_to_hex(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; + memcpy(entry->sha1, sha1, 20); + nr = sha1_list.nr; + if (nr == MAX_SHA1_ENTRIES) { + fsck_sha1_list(); + nr = 0; + } + sha1_list.entry[nr] = entry; + sha1_list.nr = ++nr; +} + +static int fsck_dir(int i, char *path) +{ + DIR *dir = opendir(path); + struct dirent *de; + + if (!dir) { + return error("missing sha1 directory '%s'", path); + } + + 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, de->d_ino); + continue; + } + fprintf(stderr, "bad sha1 file: %s/%s\n", path, de->d_name); + } + closedir(dir); + return 0; +} + +static int default_refs = 0; + +static int fsck_handle_ref(const char *refname, const unsigned char *sha1) +{ + struct object *obj; + + obj = lookup_object(sha1); + if (!obj) { + if (!standalone && 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); + if (!default_refs) + die("No default references"); +} + +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) +{ + int fd, count; + char hex[40]; + unsigned char sha1[20]; + static char path[PATH_MAX], link[PATH_MAX]; + const char *git_dir = gitenv(GIT_DIR_ENVIRONMENT) ? : DEFAULT_GIT_DIR_ENVIRONMENT; + + snprintf(path, sizeof(path), "%s/HEAD", git_dir); + if (readlink(path, link, sizeof(link)) < 0) + return error("HEAD is not a symlink"); + if (strncmp("refs/heads/", link, 11)) + return error("HEAD points to something strange (%s)", link); + fd = open(path, O_RDONLY); + if (fd < 0) + return error("HEAD: %s", strerror(errno)); + count = read(fd, hex, sizeof(hex)); + close(fd); + if (count < 0) + return error("HEAD: %s", strerror(errno)); + if (count < 40 || get_sha1_hex(hex, sha1)) + return error("HEAD: not a valid git pointer"); + return 0; +} + +int main(int argc, char **argv) +{ + int i, heads; + + 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, "--standalone")) { + standalone = 1; + continue; + } + if (!strcmp(arg, "--full")) { + check_full = 1; + continue; + } + if (*arg == '-') + usage("git-fsck-cache [--tags] [[--unreachable] [--cache] [--standalone | --full] <head-sha1>*]"); + } + + if (standalone && check_full) + die("Only one of --standalone or --full can be used."); + if (standalone) + unsetenv("GIT_ALTERNATE_OBJECT_DIRECTORIES"); + + fsck_head_link(); + fsck_object_dir(get_object_directory()); + if (check_full) { + int j; + struct packed_git *p; + prepare_alt_odb(); + for (j = 0; alt_odb[j].base; j++) { + char namebuf[PATH_MAX]; + int namelen = alt_odb[j].name - alt_odb[j].base; + memcpy(namebuf, alt_odb[j].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); + if (fsck_sha1(sha1) < 0) + fprintf(stderr, "bad sha1 entry '%s'\n", sha1_to_hex(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("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); + } + } + + check_connectivity(); + return 0; +} diff --git a/get-tar-commit-id.c b/get-tar-commit-id.c new file mode 100644 index 0000000000..a1a17e53d2 --- /dev/null +++ b/get-tar-commit-id.c @@ -0,0 +1,27 @@ +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#define HEADERSIZE 1024 + +int main(int argc, char **argv) +{ + char buffer[HEADERSIZE]; + ssize_t n; + + n = read(0, buffer, HEADERSIZE); + if (n < HEADERSIZE) { + fprintf(stderr, "read error\n"); + return 3; + } + if (buffer[156] != 'g') + return 1; + if (memcmp(&buffer[512], "52 comment=", 11)) + return 1; + n = write(1, &buffer[523], 41); + if (n < 41) { + fprintf(stderr, "write error\n"); + return 2; + } + return 0; +} @@ -0,0 +1,19 @@ +#!/bin/sh +cmd="$1" +shift +if which git-$cmd-script >& /dev/null +then + exec git-$cmd-script "$@" +fi + +if which git-$cmd >& /dev/null +then + exec git-$cmd "$@" +fi + +alternatives=($(echo $PATH | tr ':' '\n' | while read i; do ls $i/git-*-script 2> /dev/null; done)) + +echo Git command "'$cmd'" not found. Try one of +for i in "${alternatives[@]}"; do + echo $i | sed 's:^.*/git-: :' | sed 's:-script$::' +done | sort | uniq diff --git a/git-add-script b/git-add-script new file mode 100755 index 0000000000..61ec0404f0 --- /dev/null +++ b/git-add-script @@ -0,0 +1,2 @@ +#!/bin/sh +git-update-cache --add -- "$@" diff --git a/git-apply-patch-script b/git-apply-patch-script new file mode 100755 index 0000000000..6261fd85f2 --- /dev/null +++ b/git-apply-patch-script @@ -0,0 +1,144 @@ +#!/bin/sh +# Copyright (C) 2005 Junio C Hamano +# +# Applying diff between two trees to the work tree can be +# done with the following single command: +# +# GIT_EXTERNAL_DIFF=git-apply-patch-script git-diff-tree -p $tree1 $tree2 +# + +case "$#" in +1) + echo >&2 "cannot handle unmerged diff on path $1." + exit 1 ;; +8 | 9) + echo >&2 "cannot handle rename diff between $1 and $8 yet." + exit 1 ;; +esac +name="$1" tmp1="$2" hex1="$3" mode1="$4" tmp2="$5" hex2="$6" mode2="$7" + +type1=f +case "$mode1" in +*120???) type1=l ;; +*1007??) mode1=+x ;; +*1006??) mode1=-x ;; +.) type1=- ;; +esac + +type2=f +case "$mode2" in +*120???) type2=l ;; +*1007??) mode2=+x ;; +*1006??) mode2=-x ;; +.) type2=- ;; +esac + +case "$type1,$type2" in + +-,?) + dir=$(dirname "$name") + case "$dir" in '' | .) ;; *) mkdir -p "$dir" ;; esac || { + echo >&2 "cannot create leading path for $name." + exit 1 + } + if test -e "$name" + then + echo >&2 "path $name to be created already exists." + exit 1 + fi + case "$type2" in + f) + # creating a regular file + cat "$tmp2" >"$name" || { + echo >&2 "cannot create a regular file $name." + exit 1 + } + case "$mode2" in + +x) + echo >&2 "created a regular file $name with mode +x." + chmod "$mode2" "$name" + ;; + -x) + echo >&2 "created a regular file $name." + ;; + esac + ;; + l) + # creating a symlink + ln -s "$(cat "$tmp2")" "$name" || { + echo >&2 "cannot create a symbolic link $name." + exit 1 + } + echo >&2 "created a symbolic link $name." + ;; + *) + echo >&2 "do not know how to create $name of type $type2." + exit 1 + esac + git-update-cache --add -- "$name" ;; + +?,-) + rm -f "$name" || { + echo >&2 "cannot remove $name" + exit 1 + } + echo >&2 "deleted $name." + git-update-cache --remove -- "$name" ;; + +l,f|f,l) + echo >&2 "cannot change a regular file $name and a symbolic link $name." + exit 1 ;; + +l,l) + # symlink to symlink + current=$(readlink "$name") || { + echo >&2 "cannot read the target of the symbolic link $name." + exit 1 + } + original=$(cat "$tmp1") + next=$(cat "$tmp2") + test "$original" != "$current" || { + echo >&2 "cannot apply symbolic link target change ($original->$next) to $name which points to $current." + exit 1 + } + if test "$next" != "$current" + then + rm -f "$name" && ln -s "$next" "$name" || { + echo >&2 "cannot create symbolic link $name." + exit 1 + } + echo >&2 "changed symbolic target of $name." + git-update-cache -- "$name" + fi ;; + +f,f) + # changed + test -e "$name" || { + echo >&2 "regular file $name to be patched does not exist." + exit 1 + } + dir=$(dirname "$name") + case "$dir" in '' | .) ;; *) mkdir -p "$dir";; esac || { + echo >&2 "cannot create leading path for $name." + exit 1 + } + tmp=.git-apply-patch-$$ + trap "rm -f $tmp-*" 0 1 2 3 15 + + # Be careful, in case "$tmp2" is borrowed path from the work tree + # we are looking at... + diff -u -L "a/$name" -L "b/$name" "$tmp1" "$tmp2" >$tmp-patch + + # This will say "patching ..." so we do not say anything outselves. + patch -p1 <$tmp-patch || exit + rm -f $tmp-patch + case "$mode1,$mode2" in + "$mode2,$mode1") ;; + *) + chmod "$mode2" "$name" + echo >&2 "changed mode from $mode1 to $mode2." + ;; + esac + git-update-cache -- "$name" + +esac diff --git a/git-branch-script b/git-branch-script new file mode 100755 index 0000000000..ade0abf069 --- /dev/null +++ b/git-branch-script @@ -0,0 +1,11 @@ +#!/bin/sh + +. git-sh-setup-script || die "Not a git archive" + +branchname="$1" +rev=$(git-rev-parse --verify --default HEAD "$2"^0) || exit + +[ -z "$branchname" ] && die "git branch: I want a branch name" +[ -e "$GIT_DIR/refs/heads/$branchname" ] && die "$branchname already exists" + +echo $rev > "$GIT_DIR/refs/heads/$branchname" diff --git a/git-checkout-script b/git-checkout-script new file mode 100755 index 0000000000..7e70338c47 --- /dev/null +++ b/git-checkout-script @@ -0,0 +1,75 @@ +#!/bin/sh +. git-sh-setup-script || die "Not a git archive" + +old=$(git-rev-parse HEAD) +new= +force= +branch= +newbranch= +while [ "$#" != "0" ]; do + arg="$1" + shift + case "$arg" in + "-b") + newbranch="$1" + shift + [ -z "$newbranch" ] && + die "git checkout: -b needs a branch name" + [ -e "$GIT_DIR/refs/heads/$newbranch" ] && + die "git checkout: branch $newbranch already exists" + ;; + "-f") + force=1 + ;; + *) + rev=$(git-rev-parse --verify --revs-only "$arg^0") || exit + if [ -z "$rev" ]; then + echo "unknown flag $arg" + exit 1 + fi + if [ "$new" ]; then + echo "Multiple revisions?" + exit 1 + fi + new="$rev" + if [ -f "$GIT_DIR/refs/heads/$arg" ]; then + branch="$arg" + fi + ;; + esac + i=$(($i+1)) +done +[ -z "$new" ] && new=$old + +# +# If we don't have an old branch that we're switching to, +# and we don't have a new branch name for the target we +# are switching to, then we'd better just be checking out +# what we already had +# +[ -z "$branch$newbranch" ] && + [ "$new" != "$old" ] && + die "git checkout: you need to specify a new branch name" + +if [ "$force" ] +then + git-read-tree --reset $new && + git-checkout-cache -q -f -u -a +else + git-read-tree -m -u $old $new +fi + +# +# Switch the HEAD pointer to the new branch if it 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 + echo $new > "$GIT_DIR/refs/heads/$newbranch" + branch="$newbranch" + fi + [ "$branch" ] && ln -sf "refs/heads/$branch" "$GIT_DIR/HEAD" + rm -f "$GIT_DIR/MERGE_HEAD" +fi diff --git a/git-cherry b/git-cherry new file mode 100755 index 0000000000..e2a1a2db9f --- /dev/null +++ b/git-cherry @@ -0,0 +1,86 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano. +# + +usage="usage: $0 "'<upstream> [<head>] + + __*__*__*__*__> <upstream> + / + fork-point + \__+__+__+__+__+__+__+__> <head> + +Each commit between the fork-point and <head> is examined, and +compared against the change each commit between the fork-point and +<upstream> introduces. If the change does not seem to be in the +upstream, it is shown on the standard output. + +The output is intended to be used as: + + OLD_HEAD=$(git-rev-parse HEAD) + git-rev-parse linus >${GIT_DIR-.}/HEAD + git-cherry linus OLD_HEAD | + while read commit + do + GIT_EXTERNAL_DIFF=git-apply-patch-script git-diff-tree -p "$commit" && + git-commit-script -m "$commit" + done +' + +case "$#" in +1) linus=`git-rev-parse "$1"` && + junio=`git-rev-parse HEAD` || exit + ;; +2) linus=`git-rev-parse "$1"` && + junio=`git-rev-parse "$2"` || exit + ;; +*) echo >&2 "$usage"; exit 1 ;; +esac + +# Note that these list commits in reverse order; +# not that the order in inup matters... +inup=`git-rev-list ^$junio $linus` && +ours=`git-rev-list $junio ^$linus` || exit + +tmp=.cherry-tmp$$ +patch=$tmp-patch +mkdir $patch +trap "rm -rf $tmp-*" 0 1 2 3 15 + +_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" + +for c in $inup +do + git-diff-tree -p $c +done | git-patch-id | +while read id name +do + echo $name >>$patch/$id +done + +LF=' +' + +O= +for c in $ours +do + set x `git-diff-tree -p $c | git-patch-id` + if test "$2" != "" + then + if test -f "$patch/$2" + then + sign=- + else + sign=+ + fi + case "$O" in + '') O="$sign $c" ;; + *) O="$sign $c$LF$O" ;; + esac + fi +done +case "$O" in +'') ;; +*) echo "$O" ;; +esac diff --git a/git-clone-script b/git-clone-script new file mode 100755 index 0000000000..88e2c1f706 --- /dev/null +++ b/git-clone-script @@ -0,0 +1,103 @@ +#!/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. + +usage() { + echo >&2 "* git clone [-l] [-q] [-u <upload-pack>] <repo> <dir>" + exit 1 +} + +get_repo_base() { + (cd "$1" && (cd .git ; pwd)) 2> /dev/null +} + +quiet= +use_local=no +upload_pack= +while + case "$#,$1" in + 0,*) break ;; + *,-l|*,--l|*,--lo|*,--loc|*,--loca|*,--local) use_local=yes ;; + *,-q|*,--quiet) quiet=-q ;; + 1,-u|*,--upload-pack) usage ;; + *,-u|*,--upload-pack) + shift + upload_pack="--exec=$2" ;; + *,-*) usage ;; + *) break ;; + esac +do + shift +done + +# Turn the source into an absolute path if +# it is local +repo="$1" +local=no +if base=$(get_repo_base "$repo"); then + repo="$base" + local=yes +fi + +dir="$2" +mkdir "$dir" && +D=$( + (cd "$dir" && git-init-db && pwd) +) && +test -d "$D" || usage + +# We do local magic only when the user tells us to. +case "$local,$use_local" in +yes,yes) + ( cd "$repo/objects" ) || { + echo >&2 "-l flag seen but $repo is not local." + exit 1 + } + + # 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" "$D/.git/objects/sample" 2>/dev/null + then + l=l + fi && + rm -f "$D/.git/objects/sample" && + cd "$repo" && + find objects -type f -print | + cpio -puamd$l "$D/.git/" || exit 1 + + # Make a duplicate of refs and HEAD pointer + HEAD= + if test -f "$repo/HEAD" + then + HEAD=HEAD + fi + tar Ccf "$repo" - refs $HEAD | tar Cxf "$D/.git" - || exit 1 + exit 0 + ;; +esac + +case "$repo" in +rsync://*) + rsync $quiet -avz --ignore-existing "$repo/objects/" "$D/.git/objects/" && + rsync $quiet -avz --ignore-existing "$repo/refs/" "$D/.git/refs/" + ;; +http://*) + echo "Somebody should add http fetch" >&2 + exit 1 + ;; +*) + cd "$D" && case "$upload_pack" in + '') git-clone-pack $quiet "$repo" ;; + *) git-clone-pack $quiet "$upload_pack" "$repo" ;; + esac + ;; +esac diff --git a/git-commit-script b/git-commit-script new file mode 100755 index 0000000000..c6f8ee2b3d --- /dev/null +++ b/git-commit-script @@ -0,0 +1,104 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# + +. git-sh-setup-script || die "Not a git archive" + +usage () { + die 'git commit [-m existing-commit] [<path>...]' +} + +while case "$#" in 0) break ;; esac +do + case "$1" in + -m) shift + case "$#" in + 0) usage ;; + *) use_commit=`git-rev-parse "$1"` || + exit ;; + esac + ;; + *) break + ;; + esac + shift +done + +git-update-cache -q --refresh -- "$@" || exit 1 +PARENTS="-p HEAD" +if [ ! -r "$GIT_DIR/HEAD" ]; then + if [ -z "$(git-ls-files)" ]; then + echo Nothing to commit 1>&2 + exit 1 + fi + ( + echo "#" + echo "# Initial commit" + echo "#" + git-ls-files | sed 's/^/# New file: /' + echo "#" + ) > .editmsg + PARENTS="" +else + if [ -f "$GIT_DIR/MERGE_HEAD" ]; then + echo "#" + echo "# It looks like your 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 "#" + PARENTS="-p HEAD -p MERGE_HEAD" + elif test "$use_commit" != "" + then + pick_author_script=' + /^author /{ + 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 + } + ' + set_author_env=`git-cat-file commit "$use_commit" | + sed -ne "$pick_author_script"` + eval "$set_author_env" + export GIT_AUTHOR_NAME + export GIT_AUTHOR_EMAIL + export GIT_AUTHOR_DATE + git-cat-file commit "$use_commit" | + sed -e '1,/^$/d' + fi >.editmsg + git-status-script >>.editmsg +fi +if [ "$?" != "0" ] +then + cat .editmsg + rm .editmsg + exit 1 +fi +case "$use_commit" in +'') + ${VISUAL:-${EDITOR:-vi}} .editmsg + ;; +esac +grep -v '^#' < .editmsg | git-stripspace > .cmitmsg +[ -s .cmitmsg ] && + tree=$(git-write-tree) && + commit=$(cat .cmitmsg | git-commit-tree $tree $PARENTS) && + echo $commit > "$GIT_DIR/HEAD" && + rm -f -- "$GIT_DIR/MERGE_HEAD" +ret="$?" +rm -f .cmitmsg .editmsg +exit "$ret" diff --git a/git-core.spec.in b/git-core.spec.in new file mode 100644 index 0000000000..5d1ffa37ed --- /dev/null +++ b/git-core.spec.in @@ -0,0 +1,48 @@ +# Pass --without docs to rpmbuild if you don't want the documetnation +Name: git-core +Version: @@VERSION@@ +Release: 1 +Vendor: Linus Torvalds <torvalds@osdl.org> +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, openssl-devel, curl-devel %{!?_without_docs:, xmlto, asciidoc > 6.0.3} +BuildRoot: %{_tmppath}/%{name}-%{version}-root +Prereq: sh-utils, diffutils, rsync, rcs, mktemp >= 1.5 + +%description +GIT comes in two layers. The bottom layer is merely an extremely fast +and flexible filesystem-based database designed to store directory trees +with regard to their history. The top layer is a SCM-like tool which +enables human beings to work with the database in a manner to a degree +similar to other SCM tools (like CVS, BitKeeper or Monotone). + +%prep +%setup -q + +%build + +make all %{!?_without_docs: doc} + +%install +rm -rf $RPM_BUILD_ROOT +make dest=$RPM_BUILD_ROOT prefix=%{_prefix} mandir=%{_mandir} install %{!?_without_docs: install-doc} + +%clean +rm -rf $RPM_BUILD_ROOT + +%files +%defattr(-,root,root) +%{_bindir}/* +%doc README COPYING Documentation/*.txt +%{!?_without_docs: %doc Documentation/*.html } +%{!?_without_docs: %{_mandir}/man1/*.1.gz} +%{!?_without_docs: %{_mandir}/man7/*.7.gz} + +%changelog +* 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 diff --git a/git-cvsimport-script b/git-cvsimport-script new file mode 100755 index 0000000000..8f57396c2d --- /dev/null +++ b/git-cvsimport-script @@ -0,0 +1,742 @@ +#!/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); +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); + +$SIG{'PIPE'}="IGNORE"; +$ENV{'TZ'}="UTC"; + +our($opt_h,$opt_o,$opt_v,$opt_d,$opt_p,$opt_C,$opt_z,$opt_i); + +sub usage() { + print STDERR <<END; +Usage: ${\basename $0} # fetch/update GIT from CVS + [ -o branch-for-HEAD ] [ -h ] [ -v ] [ -d CVSROOT ] + [ -p opts-for-cvsps ] [ -C GIT_repository ] [ -z fuzz ] + [ -i ] [ CVS_module ] +END + exit(1); +} + +getopts("hivo:d:p:C:z:") 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"; +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(); +} + +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($user,$pass,$serv,$port) = ($1,$2,$3,$4); + $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 = 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(); + + my $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 F 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; + # $self->{'socketo'}->write("Argument -ko\n") or return undef; + # -ko: Linus' version doesn't use it + $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=0; + while($cnt) { + my $buf; + my $num = $self->{'socketi'}->read($buf,$cnt); + die "Server: Filesize $cnt: $num: $!\n" if not defined $num or $num<=0; + print $fh $buf; + $res += $num; + $cnt -= $num; + } + } 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=""; + while($cnt) { + my $buf; + my $num = $self->{'socketi'}->read($buf,$cnt); + die "S: Mbinary $cnt: $num: $!\n" if not defined $num or $num<=0; + print $fh $buf; + $res += $num; + $cnt -= $num; + } + } 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"; + } else { + die "Unknown: $line\n"; + } + } + } +} +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) { + # retry + $self->conn(); + $self->_file($fn,$rev) + or die "No file command send\n"; + $res = $self->_line($fh); + die "No input: $fn $rev\n" unless defined $res; + } + close ($fh); + + return ($name, $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; +} + +-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 $forward_master = 0; +my %branch_date; + +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; +unless(-d $git_dir) { + system("git-init-db"); + 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"; + + $last_branch = basename(readlink("$git_dir/HEAD")); + unless($last_branch) { + warn "Cannot read the last branch name: $! -- assuming 'master'\n"; + $last_branch = "master"; + } + $orig_branch = $last_branch; + if (-f "$git_dir/CVS2GIT_HEAD") { + die <<EOM; +CVS2GIT_HEAD exists. +Make sure your working directory corresponds to HEAD and remove CVS2GIT_HEAD. +You may need to run + + git-read-tree -m -u CVS2GIT_HEAD HEAD +EOM + } + system('cp', "$git_dir/HEAD", "$git_dir/CVS2GIT_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_branch); + die "read-tree failed: $?\n" if $?; + + # Get the last import timestamps + opendir(D,"$git_dir/refs/heads"); + while(defined(my $head = readdir(D))) { + next if $head =~ /^\./; + open(F,"$git_dir/refs/heads/$head") + or die "Bad head branch: $head: $!\n"; + chomp(my $ftag = <F>); + close(F); + open(F,"git-cat-file commit $ftag |"); + while(<F>) { + next unless /^author\s.*\s(\d+)\s[-+]\d{4}$/; + $branch_date{$head} = $1; + last; + } + close(F); + } + closedir(D); +} + +-d $git_dir + or die "Could not create git subdir ($git_dir).\n"; + +my $pid = open(CVS,"-|"); +die "Cannot fork: $!\n" unless defined $pid; +unless($pid) { + my @opt; + @opt = split(/,/,$opt_p) if defined $opt_p; + unshift @opt, '-z', $opt_z if defined $opt_z; + exec("cvsps",@opt,"-u","-A","--cvs-direct",'--root',$opt_d,$cvs_tree); + die "Could not start cvsps: $!\n"; +} + + +## 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; + +my($patchset,$date,$author,$branch,$ancestor,$tag,$logmsg); +my(@old,@new); +my $commit = sub { + my $pid; + while(@old) { + my @o2; + if(@old > 55) { + @o2 = splice(@old,0,50); + } else { + @o2 = @old; + @old = (); + } + system("git-update-cache","--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-cache","--add", + (map { ('--cacheinfo', @$_) } @n2)); + die "Cannot add files: $?\n" if $?; + } + + $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 $parent = ""; + if(open(C,"$git_dir/refs/heads/$last_branch")) { + chomp($parent = <C>); + close(C); + length($parent) == 40 + or die "Cannot get parent id ($parent): $!\n"; + print "Parent ID $parent\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(); + dup2($pw->fileno(),0); + dup2($pr->fileno(),1); + $pr->close(); + $pw->close(); + + my @par = (); + @par = ("-p",$parent) if $parent; + exec("env", + "GIT_AUTHOR_NAME=$author", + "GIT_AUTHOR_EMAIL=$author", + "GIT_AUTHOR_DATE=".strftime("+0000 %Y-%m-%d %H:%M:%S",gmtime($date)), + "GIT_COMMITTER_NAME=$author", + "GIT_COMMITTER_EMAIL=$author", + "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(); + + # compatibility with git2cvs + substr($logmsg,32767) = "" if length($logmsg) > 32767; + $logmsg =~ s/[\s\n]+\z//; + + print $pw "$logmsg\n" + or die "Error writing to git-commit-tree: $!\n"; + $pw->close(); + + print "Committed patch $patchset ($branch)\n" if $opt_v; + chomp(my $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 $?; + + open(C,">$git_dir/refs/heads/$branch") + or die "Cannot open branch $branch for update: $!\n"; + print C "$cid\n" + or die "Cannot write branch $branch for update: $!\n"; + close(C) + or die "Cannot write branch $branch for update: $!\n"; + + if($tag) { + open(C,">$git_dir/refs/tags/$tag") + or die "Cannot create tag $tag: $!\n"; + print C "$cid\n" + or die "Cannot write tag $branch: $!\n"; + close(C) + or die "Cannot write tag $branch: $!\n"; + print "Created tag '$tag' on '$branch'\n" if $opt_v; + } +}; + +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+$//; + $author = $_; + $state = 4; + } elsif($state == 4 and s/^Branch:\s+//) { + s/\s+$//; + $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($ancestor) { + 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"; + $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"; + $state=11; + next; + } + print H "$id\n" + or die "Could not write branch $branch: $!"; + close(H) + or die "Could not write branch $branch: $!"; + } + if(($ancestor || $branch) ne $last_branch) { + print "Switching from $last_branch to $branch\n" if $opt_v; + system("git-read-tree", $branch); + die "read-tree failed: $?\n" if $?; + } + $last_branch = $branch if $branch ne $last_branch; + $state = 9; + } elsif($state == 8) { + $logmsg .= "$_\n"; + } elsif($state == 9 and /^\s+(\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#^/+##; + my ($tmpname, $size) = $cvs->file($fn,$rev); + print "".($init ? "New" : "Update")." $fn: $size bytes.\n" if $opt_v; + open my $F, '-|', "git-hash-object -w $tmpname" + or die "Cannot create object: $!\n"; + my $sha = <$F>; + chomp $sha; + close $F; + 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*$/) { + my $fn = $1; + $fn =~ s#^/+##; + push(@old,$fn); + } elsif($state == 9 and /^\s*$/) { + $state = 10; + } elsif(($state == 9 or $state == 10) and /^-+$/) { + &$commit(); + $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; + +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; + 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', 'CVS2GIT_HEAD', 'HEAD'); + die "read-tree failed: $?\n" if $?; + } +} else { + $orig_branch = "master"; + print "DONE; creating $orig_branch branch\n" if $opt_v; + system("cp","$git_dir/refs/heads/$opt_o","$git_dir/refs/heads/master") + unless -f "$git_dir/refs/heads/master"; + unlink("$git_dir/HEAD"); + symlink("refs/heads/$orig_branch","$git_dir/HEAD"); + unless ($opt_i) { + system('git checkout'); + die "checkout failed: $?\n" if $?; + } +} +unlink("$git_dir/CVS2GIT_HEAD"); diff --git a/git-diff-script b/git-diff-script new file mode 100755 index 0000000000..03ed555e77 --- /dev/null +++ b/git-diff-script @@ -0,0 +1,19 @@ +#!/bin/sh +. git-sh-setup-script || die "Not a git archive" + +rev=($(git-rev-parse --revs-only "$@")) +flags=($(git-rev-parse --no-revs --flags "$@")) +files=($(git-rev-parse --no-revs --no-flags "$@")) +case "${#rev[*]}" in +0) + git-diff-files -M -p "$@";; +1) + git-diff-cache -M -p "$@";; +2) + begin=$(echo "${rev[1]}" | tr -d '^') + end="${rev[0]}" + git-diff-tree -M -p $flags $begin $end $files;; +*) + echo "I don't understand" + exit 1;; +esac diff --git a/git-external-diff-script b/git-external-diff-script new file mode 100755 index 0000000000..137280ac72 --- /dev/null +++ b/git-external-diff-script @@ -0,0 +1,67 @@ +#!/bin/sh +# Copyright (C) 2005 Junio C Hamano +# +# This script is designed to emulate what the built-in diff driver +# does when set as GIT_EXTERNAL_SCRIPT. + +case "$#" in +1) + echo "* Unmerged path $1" + exit 0 ;; +*) + name1="$1" tmp1="$2" hex1="$3" mode1="$4" tmp2="$5" hex2="$6" mode2="$7" + case "$#" in + 7) + name2="$name1" ;; + 9) + name2="$8" xfrm_msg="$9" ;; + esac ;; +esac + +show_create () { + name_="$1" tmp_="$2" hex_="$3" mode_="$4" + echo "diff --git a/$name_ b/$name_" + echo "new file mode $mode_" + diff ${GIT_DIFF_OPTS-'-pu'} -L /dev/null -L "b/$name_" /dev/null "$tmp_" +} + +show_delete () { + name_="$1" tmp_="$2" hex_="$3" mode_="$4" + echo "diff --git a/$name_ b/$name_" + echo "deleted file mode $mode_" + diff ${GIT_DIFF_OPTS-'-pu'} -L "a/$name_" -L /dev/null "$tmp_" /dev/null +} + +case "$mode1" in +120*) type1=l ;; +100*) type1=f ;; +.) show_create "$name2" "$tmp2" "$hex2" "$mode2" + exit 0 ;; +esac +case "$mode2" in +120*) type2=l ;; +100*) type2=f ;; +.) show_delete "$name1" "$tmp1" "$hex1" "$mode1" + exit 0 ;; +esac + +if test "$type1" != "$type2" +then + show_delete "$name1" "$tmp1" "$hex1" "$mode1" + show_create "$name2" "$tmp2" "$hex2" "$mode2" + exit 0 +fi + +echo diff --git "a/$name1" "b/$name2" +if test "$mode1" != "$mode2" +then + echo "old mode $mode1" + echo "new mode $mode2" + if test "$xfrm_msg" != "" + then + echo "$xfrm_msg" + fi +fi +diff ${GIT_DIFF_OPTS-'-pu'} -L "a/$name1" -L "b/$name2" "$tmp1" "$tmp2" +exit 0 + diff --git a/git-fetch-script b/git-fetch-script new file mode 100755 index 0000000000..b44cf3f5a5 --- /dev/null +++ b/git-fetch-script @@ -0,0 +1,41 @@ +#!/bin/sh +# +. git-sh-setup-script || die "Not a git archive" +. git-parse-remote "$@" +merge_repo="$_remote_repo" +merge_head="$_remote_head" +merge_store="$_remote_store" + +TMP_HEAD="$GIT_DIR/TMP_HEAD" + +case "$merge_repo" in +http://*) + head=$(wget -q -O - "$merge_repo/$merge_head") || exit 1 + echo Fetching "$merge_head" using http + git-http-pull -v -a "$head" "$merge_repo/" + ;; +rsync://*) + rsync -L "$merge_repo/$merge_head" "$TMP_HEAD" || exit 1 + head=$(git-rev-parse TMP_HEAD) + rm -f "$TMP_HEAD" + rsync -avz --ignore-existing "$merge_repo/objects/" "$GIT_OBJECT_DIRECTORY/" + ;; +*) + head=$(git-fetch-pack "$merge_repo" "$merge_head") + ;; +esac || exit 1 + +git-rev-parse --verify "$head" > /dev/null || exit 1 + +case "$merge_store" in +'') + ;; +*) + echo "$head" > "$GIT_DIR/$merge_store" +esac && + +# FETCH_HEAD is fed to git-resolve-script which will eventually be +# passed to git-commit-tree as one of the parents. Make sure we do +# not give a tag object ID. + +git-rev-parse "$head^0" >"$GIT_DIR/FETCH_HEAD" diff --git a/git-format-patch-script b/git-format-patch-script new file mode 100755 index 0000000000..9d26c9c46a --- /dev/null +++ b/git-format-patch-script @@ -0,0 +1,127 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +usage () { + echo >&2 "usage: $0"' [-n] [-o dir] [-<diff options>...] upstream [ our-head ] + +Prepare each commit with its patch since our-head forked from upstream, +one file per patch, for e-mail submission. Each output file is +numbered sequentially from 1, and uses the first line of the commit +message (massaged for pathname safety) as the filename. + +When -o is specified, output files are created in that directory; otherwise in +the current working directory. + +When -n is specified, instead of "[PATCH] Subject", the first line is formatted +as "[PATCH N/M] Subject", unless you have only one patch. +' + exit 1 +} + +diff_opts= +IFS=' +' +LF=' +' +outdir=./ + +while case "$#" in 0) break;; esac +do + case "$1" in + -n|--n|--nu|--num|--numb|--numbe|--number|--numbere|--numbered) + numbered=t ;; + -o=*|--o=*|--ou=*|--out=*|--outp=*|--outpu=*|--output=*|--output-=*|\ + --output-d=*|--output-di=*|--output-dir=*|--output-dire=*|\ + --output-direc=*|--output-direct=*|--output-directo=*|\ + --output-director=*|--output-directory=*) + outdir=`expr "$1" : '-[^=]*=\(.*\)'` ;; + -o|--o|--ou|--out|--outp|--outpu|--output|--output-|--output-d|\ + --output-di|--output-dir|--output-dire|--output-direc|--output-direct|\ + --output-directo|--output-director|--output-directory) + case "$#" in 1) usage ;; esac; shift + outdir="$1" ;; + -*) diff_opts="$diff_opts$LF$1" ;; + *) break ;; + esac + shift +done + +case "$#" in +2) linus="$1" junio="$2" ;; +1) linus="$1" junio=HEAD ;; +*) usage ;; +esac +junio=`git-rev-parse --verify "$junio"` +linus=`git-rev-parse --verify "$linus"` + +case "$outdir" in +*/) ;; +*) outdir="$outdir/" ;; +esac +test -d "$outdir" || mkdir -p "$outdir" || exit + +tmp=.tmp-series$$ +trap 'rm -f $tmp-*' 0 1 2 3 15 + +series=$tmp-series + +titleScript=' + /./d + /^$/n + s/^\[PATCH[^]]*\] *// + s/[^-a-z.A-Z_0-9]/-/g + s/\.\.\.*/\./g + s/\.*$// + s/--*/-/g + s/^-// + s/-$// + s/$/./ + p + q +' + +_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" +stripCommitHead='/^'"$_x40"' (from '"$_x40"')$/d' + +git-rev-list --merge-order "$junio" "^$linus" >$series +total=`wc -l <$series` +i=$total +while read commit +do + title=`git-cat-file commit "$commit" | + git-stripspace | + sed -ne "$titleScript"` + case "$numbered" in + '') num= ;; + *) + case $total in + 1) num= ;; + *) num=' '`printf "%d/%d" $i $total` ;; + esac + esac + file=`printf '%04d-%stxt' $i "$title"` + i=`expr "$i" - 1` + echo "$file" + { + mailScript=' + /./d + /^$/n + s|^|[PATCH'"$num"'] | + : body + p + n + b body' + + git-cat-file commit "$commit" | + git-stripspace | + sed -ne "$mailScript" + echo '---' + echo + git-diff-tree -p $diff_opts "$commit" | git-apply --stat --summary + echo + git-diff-tree -p $diff_opts "$commit" | sed -e "$stripCommitHead" + } >"$outdir$file" +done <$series diff --git a/git-log-script b/git-log-script new file mode 100755 index 0000000000..049d02ea14 --- /dev/null +++ b/git-log-script @@ -0,0 +1,3 @@ +#!/bin/sh +. git-sh-setup-script || die "Not a git archive" +git-rev-list --pretty $(git-rev-parse --default HEAD "$@") | LESS=-S ${PAGER:-less} diff --git a/git-merge-one-file-script b/git-merge-one-file-script new file mode 100755 index 0000000000..9802f67032 --- /dev/null +++ b/git-merge-one-file-script @@ -0,0 +1,87 @@ +#!/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 - orignal 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.") + echo "Removing $4" + if test -f "$4" + then + rm -f -- "$4" + fi && + exec git-update-cache --remove -- "$4" + ;; + +# +# Added in one. +# +".$2." | "..$3" ) + echo "Adding $4" + git-update-cache --add --cacheinfo "$6$7" "$2$3" "$4" && + exec git-checkout-cache -u -f -- "$4" + ;; + +# +# Added in both (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-cache --add --cacheinfo "$6" "$2" "$4" && + exec git-checkout-cache -u -f -- "$4" + ;; + +# +# Modified in both, but differently. +# +"$1$2$3") + echo "Auto-merging $4." + orig=`git-unpack-file $1` + src2=`git-unpack-file $3` + + # We reset the index to the first branch, making + # git-diff-file useful + git-update-cache --add --cacheinfo "$6" "$2" "$4" + git-checkout-cache -u -f -- "$4" && + merge "$4" "$orig" "$src2" + ret=$? + rm -f -- "$orig" "$src2" + + if [ "$6" != "$7" ]; then + echo "ERROR: Permissions conflict: $5->$6,$7." + ret=1 + fi + + if [ $ret -ne 0 ]; then + echo "ERROR: Merge conflict in $4." + exit 1 + fi + exec git-update-cache -- "$4" + ;; + +*) + echo "ERROR: $4: Not handling case $1 -> $2 -> $3" + ;; +esac +exit 1 diff --git a/git-parse-remote b/git-parse-remote new file mode 100755 index 0000000000..bfe7a906d9 --- /dev/null +++ b/git-parse-remote @@ -0,0 +1,79 @@ +: To be included in git-pull and git-fetch scripts. + +# A remote repository can be specified on the command line +# in one of the following formats: +# +# <repo> +# <repo> <head> +# <repo> tag <tag> +# +# where <repo> could be one of: +# +# a URL (including absolute or local pathname) +# a short-hand +# a short-hand followed by a trailing path +# +# A short-hand <name> has a corresponding file $GIT_DIR/branches/<name>, +# whose contents is a URL, possibly followed by a URL fragment #<head> +# to name the default branch on the remote side to fetch from. + +_remote_repo= _remote_store= _remote_head= _remote_name= + +case "$1" in +*:* | /* | ../* | ./* ) + _remote_repo="$1" + ;; +* ) + # otherwise, it is a short hand. + case "$1" in + */*) + # a short-hand followed by a trailing path + _token=$(expr "$1" : '\([^/]*\)/') + _rest=$(expr "$1" : '[^/]*\(/.*\)$') + ;; + *) + _token="$1" + _rest= + _remote_store="refs/heads/$_token" + ;; + esac + test -f "$GIT_DIR/branches/$_token" || + die "No such remote branch: $_token" + + _remote_repo=$(cat "$GIT_DIR/branches/$_token")"$_rest" + ;; +esac + +case "$_remote_repo" in +*"#"*) + _remote_head=`expr "$_remote_repo" : '.*#\(.*\)$'` + _remote_repo=`expr "$_remote_repo" : '\(.*\)#'` + ;; +esac + +_remote_name=$(echo "$_remote_repo" | sed 's|\.git/*$||') + +case "$2" in +tag) + _remote_name="tag '$3' of $_remote_name" + _remote_head="refs/tags/$3" + _remote_store="$_remote_head" + ;; +?*) + # command line specified a head explicitly; do not + # store the fetched head as a branch head. + _remote_name="head '$2' of $_remote_name" + _remote_head="refs/heads/$2" + _remote_store='' + ;; +'') + case "$_remote_head" in + '') + _remote_head=HEAD ;; + *) + _remote_head="refs/heads/$_remote_head" + _remote_name="head '$_remote_head' of $_remote_name" + ;; + esac + ;; +esac diff --git a/git-prune-script b/git-prune-script new file mode 100755 index 0000000000..2c678f26a4 --- /dev/null +++ b/git-prune-script @@ -0,0 +1,25 @@ +#!/bin/sh + +. git-sh-setup-script || die "Not a git archive" + +dryrun= +while case "$#" in 0) break ;; esac +do + case "$1" in + -n) dryrun=echo ;; + --) break ;; + -*) echo >&2 "usage: git-prune-script [ -n ] [ heads... ]"; exit 1 ;; + *) break ;; + esac + shift; +done + +git-fsck-cache --full --cache --unreachable "$@" | +sed -ne '/unreachable /{ + s/unreachable [^ ][^ ]* // + s|\(..\)|\1/|p +}' | { + cd "$GIT_OBJECT_DIRECTORY" || exit + xargs -r $dryrun rm -f +} + diff --git a/git-pull-script b/git-pull-script new file mode 100755 index 0000000000..fc463260bd --- /dev/null +++ b/git-pull-script @@ -0,0 +1,12 @@ +#!/bin/sh +# +. git-sh-setup-script || die "Not a git archive" +. git-parse-remote "$@" +merge_name="$_remote_name" + +git-fetch-script "$@" || exit 1 + +git-resolve-script \ + "$(cat "$GIT_DIR"/HEAD)" \ + "$(cat "$GIT_DIR"/FETCH_HEAD)" \ + "Merge $merge_name" diff --git a/git-push-script b/git-push-script new file mode 100755 index 0000000000..80e922bb5d --- /dev/null +++ b/git-push-script @@ -0,0 +1,3 @@ +#!/bin/sh +. git-sh-setup-script || die "Not a git archive" +git-send-pack "$@" diff --git a/git-rebase-script b/git-rebase-script new file mode 100755 index 0000000000..10ab700d04 --- /dev/null +++ b/git-rebase-script @@ -0,0 +1,49 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano. +# + +usage="usage: $0 "'<upstream> [<head>] + +Uses output from git-cherry to rebase local commits to the new head of +upstream tree.' + +: ${GIT_DIR=.git} + +case "$#" in +1) linus=`git-rev-parse "$1"` && + junio=`git-rev-parse HEAD` || exit + ;; +2) linus=`git-rev-parse "$1"` && + junio=`git-rev-parse "$2"` || exit + ;; +*) echo >&2 "$usage"; exit 1 ;; +esac + +git-read-tree -m -u $junio $linus && +echo "$linus" >"$GIT_DIR/HEAD" || exit + +tmp=.rebase-tmp$$ +fail=$tmp-fail +trap "rm -rf $tmp-*" 0 1 2 3 15 + +>$fail + +git-cherry $linus $junio | +while read sign commit +do + case "$sign" in + -) continue ;; + esac + S=`cat "$GIT_DIR/HEAD"` && + GIT_EXTERNAL_DIFF=git-apply-patch-script git-diff-tree -p $commit && + git-commit-script -m "$commit" || { + echo $commit >>$fail + git-read-tree --reset -u $S + } +done +if test -s $fail +then + echo Some commits could not be rebased, check by hand: + cat $fail +fi diff --git a/git-relink-script b/git-relink-script new file mode 100755 index 0000000000..f6b4f6a2f8 --- /dev/null +++ b/git-relink-script @@ -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-repack-script b/git-repack-script new file mode 100755 index 0000000000..15b7fd6a15 --- /dev/null +++ b/git-repack-script @@ -0,0 +1,14 @@ +#!/bin/sh +. git-sh-setup-script || die "Not a git archive" + +rm -f .tmp-pack-* +packname=$(git-rev-list --unpacked --objects $(git-rev-parse --all) | + git-pack-objects --non-empty --incremental .tmp-pack) || + exit 1 +if [ -z "$packname" ]; then + echo Nothing new to pack + exit 0 +fi +mkdir -p "$GIT_OBJECT_DIRECTORY/pack" && +mv .tmp-pack-$packname.pack "$GIT_OBJECT_DIRECTORY/pack/pack-$packname.pack" && +mv .tmp-pack-$packname.idx "$GIT_OBJECT_DIRECTORY/pack/pack-$packname.idx" diff --git a/git-reset-script b/git-reset-script new file mode 100755 index 0000000000..0c02aa6423 --- /dev/null +++ b/git-reset-script @@ -0,0 +1,5 @@ +#!/bin/sh +. git-sh-setup-script || die "Not a git archive" +git-read-tree --reset HEAD +git-update-cache --refresh +rm -f "$GIT_DIR/MERGE_HEAD" diff --git a/git-resolve-script b/git-resolve-script new file mode 100755 index 0000000000..fffcb07df3 --- /dev/null +++ b/git-resolve-script @@ -0,0 +1,64 @@ +#!/bin/sh +# +# Copyright (c) 2005 Linus Torvalds +# +# Resolve two trees. +# +. git-sh-setup-script || die "Not a git archive" + +head=$(git-rev-parse --revs-only "$1") +merge=$(git-rev-parse --revs-only "$2") +merge_msg="$3" + +dropheads() { + rm -f -- "$GIT_DIR/MERGE_HEAD" \ + "$GIT_DIR/LAST_MERGE" || exit 1 +} + +# +# 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 + die "git-resolve-script <head> <remote> <merge-message>" +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 + +if [ "$common" == "$merge" ]; then + echo "Already up-to-date. Yeeah!" + dropheads + exit 0 +fi +if [ "$common" == "$head" ]; then + echo "Updating from $head to $merge." + git-read-tree -u -m $head $merge || exit 1 + echo $merge > "$GIT_DIR"/HEAD + git-diff-tree -p $head $merge | git-apply --stat + dropheads + exit 0 +fi +echo "Trying to merge $merge into $head" +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-cache -o git-merge-one-file-script -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" +echo $result_commit > "$GIT_DIR"/HEAD +git-diff-tree -p $head $result_commit | git-apply --stat +dropheads diff --git a/git-sh-setup-script b/git-sh-setup-script new file mode 100755 index 0000000000..660c524ce8 --- /dev/null +++ b/git-sh-setup-script @@ -0,0 +1,17 @@ +#!/bin/sh +# +# Set up GIT_DIR and GIT_OBJECT_DIRECTORY +# and return true if everything looks ok +# +: ${GIT_DIR=.git} +: ${GIT_OBJECT_DIRECTORY="$GIT_DIR/objects"} + +die() { + echo "$@" >&2 + exit 1 +} + +[ -d "$GIT_DIR" ] && +[ -d "$GIT_DIR/refs" ] +[ -d "$GIT_OBJECT_DIRECTORY" ] && +[ -d "$GIT_OBJECT_DIRECTORY/00" ] diff --git a/git-shortlog b/git-shortlog new file mode 100755 index 0000000000..a147e7b474 --- /dev/null +++ b/git-shortlog @@ -0,0 +1,177 @@ +#!/usr/bin/perl -w + +use strict; + +# +# Even with git, we don't always have name translations. +# So have an email->real name table to translate the +# (hopefully few) missing names +# +my %mailmap = ( + 'R.Marek@sh.cvut.cz' => 'Rudolf Marek', + 'Ralf.Wildenhues@gmx.de' => 'Ralf Wildenhues', + 'aherrman@de.ibm.com' => 'Andreas Herrmann', + 'akpm@osdl.org' => 'Andrew Morton', + 'andrew.vasquez@qlogic.com' => 'Andrew Vasquez', + 'aquynh@gmail.com' => 'Nguyen Anh Quynh', + 'axboe@suse.de' => 'Jens Axboe', + 'blaisorblade@yahoo.it' => 'Paolo \'Blaisorblade\' Giarrusso', + 'bunk@stusta.de' => 'Adrian Bunk', + 'domen@coderock.org' => 'Domen Puncer', + 'dougg@torque.net' => 'Douglas Gilbert', + 'dwmw2@shinybook.infradead.org' => 'David Woodhouse', + 'ecashin@coraid.com' => 'Ed L Cashin', + 'felix@derklecks.de' => 'Felix Moeller', + 'fzago@systemfabricworks.com' => 'Frank Zago', + 'gregkh@suse.de' => 'Greg Kroah-Hartman', + 'hch@lst.de' => 'Christoph Hellwig', + 'htejun@gmail.com' => 'Tejun Heo', + 'jejb@mulgrave.(none)' => 'James Bottomley', + 'jejb@titanic.il.steeleye.com' => 'James Bottomley', + 'jgarzik@pretzel.yyz.us' => 'Jeff Garzik', + 'johnpol@2ka.mipt.ru' => 'Evgeniy Polyakov', + 'kay.sievers@vrfy.org' => 'Kay Sievers', + 'minyard@acm.org' => 'Corey Minyard', + 'mshah@teja.com' => 'Mitesh shah', + 'pj@ludd.ltu.se' => 'Peter A Jonsson', + 'rmps@joel.ist.utl.pt' => 'Rui Saraiva', + 'santtu.hyrkko@gmail.com' => 'Santtu Hyrkkö', + 'simon@thekelleys.org.uk' => 'Simon Kelley', + 'ssant@in.ibm.com' => 'Sachin P Sant', + 'terra@gnome.org' => 'Morten Welinder', + 'tony.luck@intel.com' => 'Tony Luck', + 'welinder@anemone.rentec.com' => 'Morten Welinder', + 'welinder@darter.rentec.com' => 'Morten Welinder', + 'welinder@troll.com' => 'Morten Welinder', +); + +my (%map); +my $pstate = 1; +my $n_records = 0; +my $n_output = 0; + + +sub shortlog_entry($$) { + my ($name, $desc) = @_; + my $key = $name; + + $desc =~ s#/pub/scm/linux/kernel/git/#/.../#g; + $desc =~ s#\[PATCH\] ##g; + + # store description in array, in email->{desc list} map + if (exists $map{$key}) { + # grab ref + my $obj = $map{$key}; + + # add desc to array + push(@$obj, $desc); + } else { + # create new array, containing 1 item + my @arr = ($desc); + + # store ref to array + $map{$key} = \@arr; + } +} + +# sort comparison function +sub by_name($$) { + my ($a, $b) = @_; + + uc($a) cmp uc($b); +} + +sub shortlog_output { + my ($obj, $key, $desc); + + foreach $key (sort by_name keys %map) { + # output author + printf "%s:\n", $key; + + # output author's 1-line summaries + $obj = $map{$key}; + foreach $desc (@$obj) { + print " $desc\n"; + $n_output++; + } + + # blank line separating author from next author + print "\n"; + } +} + +sub changelog_input { + my ($author, $desc); + + while (<>) { + # get author and email + if ($pstate == 1) { + my ($email); + + next unless /^Author: (.*)<(.*)>.*$/; + + $n_records++; + + $author = $1; + $email = $2; + $desc = undef; + + # trim trailing whitespace. + # why doesn't chomp work? + while ($author && ($author =~ /\s$/)) { + chop $author; + } + + # cset author fixups + if (exists $mailmap{$email}) { + $author = $mailmap{$email}; + } elsif (exists $mailmap{$author}) { + $author = $mailmap{$author}; + } elsif ((!$author) || ($author eq "")) { + $author = $email; + } + + $pstate++; + } + + # skip to blank line + elsif ($pstate == 2) { + next unless /^\s*$/; + $pstate++; + } + + # skip to non-blank line + elsif ($pstate == 3) { + next unless /^\s*(\S.*)$/; + + # skip lines that are obviously not + # a 1-line cset description + next if /^\s*From: /; + + chomp; + $desc = $1; + + &shortlog_entry($author, $desc); + + $pstate = 1; + } + + else { + die "invalid parse state $pstate"; + } + } +} + +sub finalize { + #print "\n$n_records records parsed.\n"; + + if ($n_records != $n_output) { + die "parse error: input records != output records\n"; + } +} + +&changelog_input; +&shortlog_output; +&finalize; +exit(0); + diff --git a/git-status-script b/git-status-script new file mode 100755 index 0000000000..7d47fdea79 --- /dev/null +++ b/git-status-script @@ -0,0 +1,38 @@ +#!/bin/sh +report () { + header="# +# $1: +# ($2) +# +" + trailer="" + while read oldmode mode oldsha sha status name newname + do + echo -n "$header" + header="" + trailer="# +" + case "$status" in + M ) echo "# modified: $name";; + D*) echo "# deleted: $name";; + T ) echo "# typechange: $name";; + C*) echo "# copied: $name -> $newname";; + R*) echo "# renamed: $name -> $newname";; + N*) echo "# new file: $name";; + U ) echo "# unmerged: $name";; + esac + done + echo -n "$trailer" + [ "$header" ] +} + +git-update-cache --refresh >& /dev/null +git-diff-cache -M --cached HEAD | sed 's/^://' | report "Updated but not checked in" "will commit" +committable="$?" +git-diff-files | sed 's/^://' | report "Changed but not updated" "use git-update-cache to mark for commit" +if [ "$committable" == "0" ] +then + echo "nothing to commit" + exit 1 +fi +exit 0 diff --git a/git-tag-script b/git-tag-script new file mode 100755 index 0000000000..4124f5494a --- /dev/null +++ b/git-tag-script @@ -0,0 +1,26 @@ +#!/bin/sh +# Copyright (c) 2005 Linus Torvalds + +. git-sh-setup-script || die "Not a git archive" +name="$1" +[ "$name" ] || die "I need a tag-name" + +object=${2:-$(cat "$GIT_DIR"/HEAD)} +type=$(git-cat-file -t $object) || exit 1 +tagger=$(git-var GIT_COMMITTER_IDENT) || exit 1 + +( echo "#" + echo "# Write a tag message" + echo "#" ) > .editmsg +${VISUAL:-${EDITOR:-vi}} .editmsg || exit + +grep -v '^#' < .editmsg | git-stripspace > .tagmsg + +[ -s .tagmsg ] || exit + +( echo -e "object $object\ntype $type\ntag $name\ntagger $tagger\n"; cat .tagmsg ) > .tmp-tag +rm -f .tmp-tag.asc .tagmsg +gpg -bsa .tmp-tag && cat .tmp-tag.asc >> .tmp-tag +mkdir -p "$GIT_DIR/refs/tags" +git-mktag < .tmp-tag > "$GIT_DIR/refs/tags/$name" +#rm .tmp-tag .tmp-tag.sig diff --git a/git-verify-tag-script b/git-verify-tag-script new file mode 100755 index 0000000000..c4992118ec --- /dev/null +++ b/git-verify-tag-script @@ -0,0 +1,9 @@ +#!/bin/sh +GIT_DIR=${GIT_DIR:-.git} + +tag=$1 +[ -f "$GIT_DIR/refs/tags/$tag" ] && tag=$(cat "$GIT_DIR/refs/tags/$tag") + +git-cat-file tag $tag > .tmp-vtag || exit 1 +cat .tmp-vtag | sed '/-----BEGIN PGP/Q' | gpg --verify .tmp-vtag - +rm -f .tmp-vtag diff --git a/git-whatchanged b/git-whatchanged new file mode 100755 index 0000000000..6fbd115601 --- /dev/null +++ b/git-whatchanged @@ -0,0 +1,4 @@ +#!/bin/sh +git-rev-list $(git-rev-parse --default HEAD --revs-only "$@") | + git-diff-tree --stdin --pretty -r $(git-rev-parse --no-revs "$@") | + LESS="$LESS -S" ${PAGER:-less} diff --git a/gitenv.c b/gitenv.c new file mode 100644 index 0000000000..ab9396f969 --- /dev/null +++ b/gitenv.c @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" + +/* + * This array must be sorted by its canonical name, because + * we do look-up by binary search. + */ +static struct backward_compatible_env { + const char *canonical; + const char *old; +} bc_name[] = { + { "GIT_ALTERNATE_OBJECT_DIRECTORIES", "SHA1_FILE_DIRECTORIES" }, + { "GIT_AUTHOR_DATE", "AUTHOR_DATE" }, + { "GIT_AUTHOR_EMAIL", "AUTHOR_EMAIL" }, + { "GIT_AUTHOR_NAME", "AUTHOR_NAME" }, + { "GIT_COMMITTER_EMAIL", "COMMIT_AUTHOR_EMAIL" }, + { "GIT_COMMITTER_NAME", "COMMIT_AUTHOR_NAME" }, + { "GIT_OBJECT_DIRECTORY", "SHA1_FILE_DIRECTORY" }, +}; + +static void warn_old_environment(int pos) +{ + int i; + static int warned = 0; + if (warned) + return; + + warned = 1; + fprintf(stderr, + "warning: Attempting to use %s\n", + bc_name[pos].old); + fprintf(stderr, + "warning: GIT environment variables have been renamed.\n" + "warning: Please adjust your scripts and environment.\n"); + for (i = 0; i < sizeof(bc_name) / sizeof(bc_name[0]); i++) { + /* warning is needed only when old name is there and + * new name is not. + */ + if (!getenv(bc_name[i].canonical) && getenv(bc_name[i].old)) + fprintf(stderr, "warning: old %s => new %s\n", + bc_name[i].old, bc_name[i].canonical); + } +} + +char *gitenv_bc(const char *e) +{ + int first, last; + char *val = getenv(e); + if (val) + die("gitenv_bc called on existing %s; fix the caller.", e); + + first = 0; + last = sizeof(bc_name) / sizeof(bc_name[0]); + while (last > first) { + int next = (last + first) >> 1; + int cmp = strcmp(e, bc_name[next].canonical); + if (!cmp) { + val = getenv(bc_name[next].old); + /* If the user has only old name, warn. + * otherwise stay silent. + */ + if (val) + warn_old_environment(next); + return val; + } + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + return NULL; +} @@ -37,7 +37,7 @@ proc getcommits {rargs} { set parsed_args $rargs } if [catch { - set commfd [open "|git-rev-list --header --merge-order $parsed_args" r] + set commfd [open "|git-rev-list --header --topo-order $parsed_args" r] } err] { puts stderr "Error executing git-rev-list: $err" exit 1 diff --git a/hash-object.c b/hash-object.c new file mode 100644 index 0000000000..0821e543f5 --- /dev/null +++ b/hash-object.c @@ -0,0 +1,45 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + * Copyright (C) Junio C Hamano, 2005 + */ +#include "cache.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 const char *hash_object_usage = +"git-hash-object [-t <type>] [-w] <file>..."; + +int main(int argc, char **argv) +{ + int i; + const char *type = "blob"; + int write_object = 0; + + for (i = 1 ; i < argc; i++) { + if (!strcmp(argv[i], "-t")) { + if (argc <= ++i) + die(hash_object_usage); + type = argv[i]; + } + else if (!strcmp(argv[i], "-w")) + write_object = 1; + else + hash_object(argv[i], type, write_object); + } + return 0; +} diff --git a/http-pull.c b/http-pull.c new file mode 100644 index 0000000000..1f9d60b9b1 --- /dev/null +++ b/http-pull.c @@ -0,0 +1,183 @@ +#include "cache.h" +#include "commit.h" + +#include "pull.h" + +#include <curl/curl.h> +#include <curl/easy.h> + +static CURL *curl; + +static char *base; + +static SHA_CTX c; +static z_stream stream; + +static int local; +static int zret; + +struct buffer +{ + size_t posn; + size_t size; + void *buffer; +}; + +static size_t fwrite_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(buffer->buffer + buffer->posn, ptr, size); + buffer->posn += size; + return size; +} + +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; + do { + ssize_t retval = write(local, ptr + posn, size - posn); + if (retval < 0) + return posn; + posn += retval; + } while (posn < size); + + stream.avail_in = size; + stream.next_in = ptr; + do { + stream.next_out = expn; + stream.avail_out = sizeof(expn); + zret = inflate(&stream, Z_SYNC_FLUSH); + SHA1_Update(&c, expn, sizeof(expn) - stream.avail_out); + } while (stream.avail_in && zret == Z_OK); + return size; +} + +int fetch(unsigned char *sha1) +{ + char *hex = sha1_to_hex(sha1); + char *filename = sha1_file_name(sha1); + unsigned char real_sha1[20]; + char *url; + char *posn; + + local = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0666); + + if (local < 0) + return error("Couldn't open %s\n", filename); + + memset(&stream, 0, sizeof(stream)); + + inflateInit(&stream); + + SHA1_Init(&c); + + curl_easy_setopt(curl, CURLOPT_FILE, NULL); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite_sha1_file); + + url = xmalloc(strlen(base) + 50); + strcpy(url, base); + posn = url + strlen(base); + strcpy(posn, "objects/"); + posn += 8; + memcpy(posn, hex, 2); + posn += 2; + *(posn++) = '/'; + strcpy(posn, hex + 2); + + curl_easy_setopt(curl, CURLOPT_URL, url); + + if (curl_easy_perform(curl)) + return error("Couldn't get %s for %s\n", url, hex); + + close(local); + inflateEnd(&stream); + SHA1_Final(real_sha1, &c); + if (zret != Z_STREAM_END) { + unlink(filename); + return error("File %s (%s) corrupt\n", hex, url); + } + if (memcmp(sha1, real_sha1, 20)) { + unlink(filename); + return error("File %s has bad hash\n", hex); + } + + pull_say("got %s\n", hex); + return 0; +} + +int fetch_ref(char *ref, unsigned char *sha1) +{ + char *url, *posn; + char hex[42]; + struct buffer buffer; + buffer.size = 41; + buffer.posn = 0; + buffer.buffer = hex; + hex[41] = '\0'; + + curl_easy_setopt(curl, CURLOPT_FILE, &buffer); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite_buffer); + + url = xmalloc(strlen(base) + 6 + strlen(ref)); + strcpy(url, base); + posn = url + strlen(base); + strcpy(posn, "refs/"); + posn += 5; + strcpy(posn, ref); + + curl_easy_setopt(curl, CURLOPT_URL, url); + + if (curl_easy_perform(curl)) + return error("Couldn't get %s for %s\n", url, ref); + + hex[40] = '\0'; + get_sha1_hex(hex, sha1); + 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] == '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++; + } + arg++; + } + if (argc < arg + 2) { + usage("git-http-pull [-c] [-t] [-a] [-d] [-v] [--recover] [-w ref] commit-id url"); + return 1; + } + commit_id = argv[arg]; + url = argv[arg + 1]; + + curl_global_init(CURL_GLOBAL_ALL); + + curl = curl_easy_init(); + + base = url; + + if (pull(commit_id)) + return 1; + + curl_global_cleanup(); + return 0; +} diff --git a/ident.c b/ident.c new file mode 100644 index 0000000000..9ef636ee87 --- /dev/null +++ b/ident.c @@ -0,0 +1,148 @@ +/* + * ident.c + * + * create git identifier lines of the form "name <email> date" + * + * Copyright (C) 2005 Linus Torvalds + */ +#include "cache.h" + +#include <pwd.h> +#include <time.h> +#include <ctype.h> + +static char real_email[1000]; +static char real_name[1000]; +static char real_date[50]; + +int setup_ident(void) +{ + int len; + struct passwd *pw = getpwuid(getuid()); + + if (!pw) + die("You don't exist. Go away!"); + + /* Get the name ("gecos") */ + len = strlen(pw->pw_gecos); + if (len >= sizeof(real_name)) + die("Your parents must have hated you!"); + memcpy(real_name, pw->pw_gecos, len+1); + + /* Make up a fake email address (name + '@' + hostname [+ '.' + domainname]) */ + len = strlen(pw->pw_name); + if (len > sizeof(real_email)/2) + die("Your sysadmin must hate you!"); + memcpy(real_email, pw->pw_name, len); + real_email[len++] = '@'; + gethostname(real_email + len, sizeof(real_email) - len); + if (!strchr(real_email+len, '.')) { + len = strlen(real_email); + real_email[len++] = '.'; + getdomainname(real_email+len, sizeof(real_email)-len); + } + + /* And set the default date */ + datestamp(real_date, sizeof(real_date)); + return 0; +} + +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 const char crud_array[256] = { + [0 ... 31] = 1, + [' '] = 1, + ['.'] = 1, [','] = 1, + [':'] = 1, [';'] = 1, + ['<'] = 1, ['>'] = 1, + ['"'] = 1, ['\''] = 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 delimeters 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; +} + +char *get_ident(const char *name, const char *email, const char *date_str) +{ + static char buffer[1000]; + char date[50]; + int i; + + if (!name) + name = real_name; + if (!email) + email = real_email; + strcpy(date, real_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; +} + +char *git_author_info(void) +{ + return get_ident(gitenv("GIT_AUTHOR_NAME"), gitenv("GIT_AUTHOR_EMAIL"), gitenv("GIT_AUTHOR_DATE")); +} + +char *git_committer_info(void) +{ + return get_ident(gitenv("GIT_COMMITTER_NAME"), gitenv("GIT_COMMITTER_EMAIL"), gitenv("GIT_COMMITTER_DATE")); +} diff --git a/index.c b/index.c new file mode 100644 index 0000000000..87fc7b0387 --- /dev/null +++ b/index.c @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2005, Junio C Hamano + */ +#include <signal.h> +#include "cache.h" + +static struct cache_file *cache_file_list; + +static void remove_lock_file(void) +{ + while (cache_file_list) { + if (cache_file_list->lockfile[0]) + unlink(cache_file_list->lockfile); + cache_file_list = cache_file_list->next; + } +} + +static void remove_lock_file_on_signal(int signo) +{ + remove_lock_file(); +} + +int hold_index_file_for_update(struct cache_file *cf, const char *path) +{ + sprintf(cf->lockfile, "%s.lock", path); + cf->next = cache_file_list; + cache_file_list = cf; + if (!cf->next) { + signal(SIGINT, remove_lock_file_on_signal); + atexit(remove_lock_file); + } + return open(cf->lockfile, O_RDWR | O_CREAT | O_EXCL, 0600); +} + +int commit_index_file(struct cache_file *cf) +{ + char indexfile[PATH_MAX]; + int i; + strcpy(indexfile, cf->lockfile); + i = strlen(indexfile) - 5; /* .lock */ + indexfile[i] = 0; + i = rename(cf->lockfile, indexfile); + cf->lockfile[0] = 0; + return i; +} + +void rollback_index_file(struct cache_file *cf) +{ + if (cf->lockfile[0]) + unlink(cf->lockfile); + cf->lockfile[0] = 0; +} + diff --git a/init-db.c b/init-db.c new file mode 100644 index 0000000000..c78c495114 --- /dev/null +++ b/init-db.c @@ -0,0 +1,93 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" + +static void safe_create_dir(const char *dir) +{ + if (mkdir(dir, 0777) < 0) { + if (errno != EEXIST) { + perror(dir); + exit(1); + } + } +} + +static void create_default_files(const char *git_dir) +{ + unsigned len = strlen(git_dir); + static char path[PATH_MAX]; + + 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); + strcpy(path + len, "refs/heads"); + safe_create_dir(path); + strcpy(path + len, "refs/tags"); + safe_create_dir(path); + + /* + * Create the default symlink from ".git/HEAD" to the "master" + * branch + */ + strcpy(path + len, "HEAD"); + if (symlink("refs/heads/master", path) < 0) { + if (errno != EEXIST) { + perror(path); + exit(1); + } + } +} + +/* + * 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 main(int argc, char **argv) +{ + const char *git_dir; + const char *sha1_dir; + char *path; + int len, i; + + /* + * Set up the default .git directory contents + */ + git_dir = gitenv(GIT_DIR_ENVIRONMENT); + if (!git_dir) { + git_dir = DEFAULT_GIT_DIR_ENVIRONMENT; + fprintf(stderr, "defaulting to local storage area\n"); + } + safe_create_dir(git_dir); + create_default_files(git_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); + for (i = 0; i < 256; i++) { + sprintf(path+len, "/%02x", i); + safe_create_dir(path); + } + strcpy(path+len, "/pack"); + safe_create_dir(path); + return 0; +} diff --git a/local-pull.c b/local-pull.c new file mode 100644 index 0000000000..2f06fbee8b --- /dev/null +++ b/local-pull.c @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2005 Junio C Hamano + */ +#include "cache.h" +#include "commit.h" +#include "pull.h" + +static int use_link = 0; +static int use_symlink = 0; +static int use_filecopy = 1; + +static char *path; /* "Remote" git repository */ + +int fetch(unsigned char *sha1) +{ + static int object_name_start = -1; + static char filename[PATH_MAX]; + char *hex = sha1_to_hex(sha1); + const 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); + if (use_link) { + if (!link(filename, dest_filename)) { + pull_say("link %s\n", hex); + return 0; + } + /* If we got ENOENT there is no point continuing. */ + if (errno == ENOENT) { + fprintf(stderr, "does not exist %s\n", filename); + return -1; + } + } + if (use_symlink && !symlink(filename, dest_filename)) { + pull_say("symlink %s\n", hex); + return 0; + } + if (use_filecopy) { + int ifd, ofd, status; + struct stat st; + void *map; + ifd = open(filename, O_RDONLY); + if (ifd < 0 || fstat(ifd, &st) < 0) { + close(ifd); + fprintf(stderr, "cannot open %s\n", filename); + return -1; + } + map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, ifd, 0); + close(ifd); + if (-1 == (int)(long)map) { + fprintf(stderr, "cannot mmap %s\n", filename); + return -1; + } + ofd = open(dest_filename, O_WRONLY | O_CREAT | O_EXCL, 0666); + status = ((ofd < 0) || + (write(ofd, map, st.st_size) != st.st_size)); + munmap(map, st.st_size); + close(ofd); + if (status) + fprintf(stderr, "cannot write %s\n", dest_filename); + else + pull_say("copy %s\n", hex); + return status; + } + fprintf(stderr, "failed to copy %s with given copy methods.\n", hex); + return -1; +} + +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(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-pull [-c] [-t] [-a] [-d] [-v] [-w filename] [--recover] [-l] [-s] [-n] 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, char **argv) +{ + char *commit_id; + 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] == '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 + usage(local_pull_usage); + arg++; + } + if (argc < arg + 2) + usage(local_pull_usage); + commit_id = argv[arg]; + path = argv[arg + 1]; + + if (pull(commit_id)) + return 1; + + return 0; +} diff --git a/ls-files.c b/ls-files.c new file mode 100644 index 0000000000..c6c32d94b0 --- /dev/null +++ b/ls-files.c @@ -0,0 +1,350 @@ +/* + * 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 <dirent.h> +#include <fnmatch.h> + +#include "cache.h" + +static int show_deleted = 0; +static int show_cached = 0; +static int show_others = 0; +static int show_ignored = 0; +static int show_stage = 0; +static int show_unmerged = 0; +static int show_killed = 0; +static int line_terminator = '\n'; + +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 int nr_excludes; +static const char **excludes; +static int excludes_alloc; + +static void add_exclude(const char *string) +{ + if (nr_excludes == excludes_alloc) { + excludes_alloc = alloc_nr(excludes_alloc); + excludes = realloc(excludes, excludes_alloc*sizeof(char *)); + } + excludes[nr_excludes++] = string; +} + +static void add_excludes_from_file(const char *fname) +{ + int fd, i; + long size; + char *buf, *entry; + + fd = open(fname, O_RDONLY); + if (fd < 0) + goto err; + size = lseek(fd, 0, SEEK_END); + if (size < 0) + goto err; + lseek(fd, 0, SEEK_SET); + if (size == 0) { + close(fd); + return; + } + buf = xmalloc(size); + if (read(fd, buf, size) != size) + goto err; + close(fd); + + entry = buf; + for (i = 0; i < size; i++) { + if (buf[i] == '\n') { + if (entry != buf + i) { + buf[i] = 0; + add_exclude(entry); + } + entry = buf + i + 1; + } + } + return; + +err: perror(fname); + exit(1); +} + +static int excluded(const char *pathname) +{ + int i; + if (nr_excludes) { + const char *basename = strrchr(pathname, '/'); + basename = (basename) ? basename+1 : pathname; + for (i = 0; i < nr_excludes; i++) + if (fnmatch(excludes[i], basename, 0) == 0) + return 1; + } + return 0; +} + +struct nond_on_fs { + int len; + char name[0]; +}; + +static struct nond_on_fs **dir; +static int nr_dir; +static int dir_alloc; + +static void add_name(const char *pathname, int len) +{ + struct nond_on_fs *ent; + + if (cache_name_pos(pathname, len) >= 0) + return; + + if (nr_dir == dir_alloc) { + dir_alloc = alloc_nr(dir_alloc); + dir = xrealloc(dir, dir_alloc*sizeof(ent)); + } + ent = xmalloc(sizeof(*ent) + len + 1); + ent->len = len; + memcpy(ent->name, pathname, len); + dir[nr_dir++] = ent; +} + +/* + * 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 currently ignore all names starting with a dot. + * That likely will not change. + */ +static void read_directory(const char *path, const char *base, int baselen) +{ + DIR *dir = opendir(path); + + if (dir) { + struct dirent *de; + char fullname[MAXPATHLEN + 1]; + memcpy(fullname, base, baselen); + + while ((de = readdir(dir)) != 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; + if (excluded(de->d_name) != show_ignored) + continue; + len = strlen(de->d_name); + memcpy(fullname + baselen, de->d_name, len+1); + + 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); + read_directory(fullname, fullname, + baselen + len + 1); + continue; + case DT_REG: + case DT_LNK: + break; + } + add_name(fullname, baselen + len); + } + closedir(dir); + } +} + +static int cmp_name(const void *p1, const void *p2) +{ + const struct nond_on_fs *e1 = *(const struct nond_on_fs **)p1; + const struct nond_on_fs *e2 = *(const struct nond_on_fs **)p2; + + return cache_name_compare(e1->name, e1->len, + e2->name, e2->len); +} + +static void show_killed_files(void) +{ + int i; + for (i = 0; i < nr_dir; i++) { + struct nond_on_fs *ent = dir[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) + printf("%s%.*s%c", tag_killed, + dir[i]->len, dir[i]->name, + line_terminator); + } +} + +static void show_files(void) +{ + int i; + + /* For cached/deleted files we don't need to even do the readdir */ + if (show_others || show_killed) { + read_directory(".", "", 0); + qsort(dir, nr_dir, sizeof(struct nond_on_fs *), cmp_name); + if (show_others) + for (i = 0; i < nr_dir; i++) + printf("%s%.*s%c", tag_other, + dir[i]->len, dir[i]->name, + line_terminator); + if (show_killed) + show_killed_files(); + } + if (show_cached | show_stage) { + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (excluded(ce->name) != show_ignored) + continue; + if (show_unmerged && !ce_stage(ce)) + continue; + if (!show_stage) + printf("%s%s%c", + ce_stage(ce) ? tag_unmerged : + tag_cached, + ce->name, line_terminator); + else + printf("%s%06o %s %d\t%s%c", + ce_stage(ce) ? tag_unmerged : + tag_cached, + ntohl(ce->ce_mode), + sha1_to_hex(ce->sha1), + ce_stage(ce), + ce->name, line_terminator); + } + } + if (show_deleted) { + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + struct stat st; + if (excluded(ce->name) != show_ignored) + continue; + if (!lstat(ce->name, &st)) + continue; + printf("%s%s%c", tag_removed, ce->name, + line_terminator); + } + } +} + +static const char *ls_files_usage = + "git-ls-files [-z] [-t] (--[cached|deleted|others|stage|unmerged|killed])* " + "[ --ignored [--exclude=<pattern>] [--exclude-from=<file>) ]"; + +int main(int argc, char **argv) +{ + int i; + + for (i = 1; i < argc; i++) { + char *arg = argv[i]; + + if (!strcmp(arg, "-z")) { + line_terminator = 0; + } else if (!strcmp(arg, "-t")) { + tag_cached = "H "; + tag_unmerged = "M "; + tag_removed = "R "; + tag_other = "? "; + tag_killed = "K "; + } else if (!strcmp(arg, "-c") || !strcmp(arg, "--cached")) { + show_cached = 1; + } else if (!strcmp(arg, "-d") || !strcmp(arg, "--deleted")) { + show_deleted = 1; + } else if (!strcmp(arg, "-o") || !strcmp(arg, "--others")) { + show_others = 1; + } else if (!strcmp(arg, "-i") || !strcmp(arg, "--ignored")) { + show_ignored = 1; + } else if (!strcmp(arg, "-s") || !strcmp(arg, "--stage")) { + show_stage = 1; + } else if (!strcmp(arg, "-k") || !strcmp(arg, "--killed")) { + show_killed = 1; + } else 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; + } else if (!strcmp(arg, "-x") && i+1 < argc) { + add_exclude(argv[++i]); + } else if (!strncmp(arg, "--exclude=", 10)) { + add_exclude(arg+10); + } else if (!strcmp(arg, "-X") && i+1 < argc) { + add_excludes_from_file(argv[++i]); + } else if (!strncmp(arg, "--exclude-from=", 15)) { + add_excludes_from_file(arg+15); + } else + usage(ls_files_usage); + } + + if (show_ignored && !nr_excludes) { + 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_cached = 1; + + read_cache(); + show_files(); + return 0; +} diff --git a/ls-tree.c b/ls-tree.c new file mode 100644 index 0000000000..8d524b89a2 --- /dev/null +++ b/ls-tree.c @@ -0,0 +1,247 @@ +/* + * 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(const char *path) +{ + const char *next, *slash; + int len; + struct tree_entry_list *elem = &root_entry; + + /* Find tree element, descending from root, that + * corresponds to the named path, lazily expanding + * the tree if possible. + */ + + while (path) { + /* The fact we still have path means that the caller + * wants us to make sure that elem at this point is a + * directory, and possibly descend into it. Even what + * is left is just trailing slashes, we loop back to + * here, and this call to prepare_children() will + * catch elem not being a tree. Nice. + */ + if (prepare_children(elem)) + return NULL; + + slash = strchr(path, '/'); + if (!slash) { + len = strlen(path); + next = NULL; + } + else { + next = slash + 1; + len = slash - path; + } + if (len) { + /* (len == 0) if the original path was "drivers/char/" + * and we have run already two rounds, having elem + * pointing at the drivers/char directory. + */ + elem = elem->item.tree->entries; + while (elem) { + if ((strlen(elem->name) == len) && + !strncmp(elem->name, path, len)) { + /* found */ + break; + } + elem = elem->next; + } + if (!elem) + return NULL; + } + path = next; + } + + return elem; +} + +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) +{ + int err = 0; + struct tree_entry_list *e = find_entry(path); + 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++) + err = err | list_one(path[i]); + 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/merge-base.c b/merge-base.c new file mode 100644 index 0000000000..12ebb95fba --- /dev/null +++ b/merge-base.c @@ -0,0 +1,72 @@ +#include <stdlib.h> +#include "cache.h" +#include "commit.h" + +static struct commit *process_list(struct commit_list **list_p, int this_mark, + int other_mark) +{ + struct commit *item = (*list_p)->item; + + if (item->object.flags & other_mark) { + return item; + } else { + pop_most_recent_commit(list_p, this_mark); + } + return NULL; +} + +static struct commit *common_ancestor(struct commit *rev1, struct commit *rev2) +{ + struct commit_list *rev1list = NULL; + struct commit_list *rev2list = NULL; + + commit_list_insert(rev1, &rev1list); + rev1->object.flags |= 0x1; + commit_list_insert(rev2, &rev2list); + rev2->object.flags |= 0x2; + + parse_commit(rev1); + parse_commit(rev2); + + while (rev1list || rev2list) { + struct commit *ret; + if (!rev1list) { + // process 2 + ret = process_list(&rev2list, 0x2, 0x1); + } else if (!rev2list) { + // process 1 + ret = process_list(&rev1list, 0x1, 0x2); + } else if (rev1list->item->date < rev2list->item->date) { + // process 2 + ret = process_list(&rev2list, 0x2, 0x1); + } else { + // process 1 + ret = process_list(&rev1list, 0x1, 0x2); + } + if (ret) { + free_commit_list(rev1list); + free_commit_list(rev2list); + return ret; + } + } + return NULL; +} + +int main(int argc, char **argv) +{ + struct commit *rev1, *rev2, *ret; + unsigned char rev1key[20], rev2key[20]; + + if (argc != 3 || + get_sha1(argv[1], rev1key) || + get_sha1(argv[2], rev2key)) { + usage("git-merge-base <commit-id> <commit-id>"); + } + rev1 = lookup_commit_reference(rev1key); + rev2 = lookup_commit_reference(rev2key); + ret = common_ancestor(rev1, rev2); + if (!ret) + return 1; + printf("%s\n", sha1_to_hex(ret->object.sha1)); + return 0; +} diff --git a/merge-cache.c b/merge-cache.c new file mode 100644 index 0000000000..37c72d26ca --- /dev/null +++ b/merge-cache.c @@ -0,0 +1,128 @@ +#include <sys/types.h> +#include <sys/wait.h> + +#include "cache.h" + +static const char *pgm = NULL; +static const char *arguments[8]; +static int one_shot; +static int err; + +static void run_program(void) +{ + int pid = fork(), 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 + die("merge program failed"); + } +} + +static int merge_entry(int pos, const char *path) +{ + int found; + + if (pos >= active_nr) + die("git-merge-cache: %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-cache: %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; + + if (argc < 3) + usage("git-merge-cache [-o] <merge-program> (-a | <filename>*)"); + + read_cache(); + + i = 1; + if (!strcmp(argv[1], "-o")) { + one_shot = 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-cache: unknown option %s", arg); + } + merge_file(arg); + } + if (err) + die("merge program failed"); + return 0; +} diff --git a/mktag.c b/mktag.c new file mode 100644 index 0000000000..585677eb83 --- /dev/null +++ b/mktag.c @@ -0,0 +1,136 @@ +#include "cache.h" + +/* + * A signature file has a very simple fixed format: three lines + * of "object <sha1>" + "type <typename>" + "tag <tagname>", + * followed by some free-form signature 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; +} + +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 || size > MAXSIZE-1) + return -1; + buffer[size] = 0; + + /* Verify object line */ + object = buffer; + if (memcmp(object, "object ", 7)) + return -1; + if (get_sha1_hex(object + 7, sha1)) + return -1; + + /* Verify type line */ + type_line = object + 48; + if (memcmp(type_line - 1, "\ntype ", 6)) + return -1; + + /* Verify tag-line */ + tag_line = strchr(type_line, '\n'); + if (!tag_line) + return -1; + tag_line++; + if (memcmp(tag_line, "tag ", 4) || tag_line[4] == '\n') + return -1; + + /* Get the actual type */ + typelen = tag_line - type_line - strlen("type \n"); + if (typelen >= sizeof(type)) + return -1; + memcpy(type, type_line+5, typelen); + type[typelen] = 0; + + /* Verify that the object matches */ + if (get_sha1_hex(object + 7, sha1)) + return -1; + if (verify_object(sha1, type)) + return -1; + + /* 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 -1; + } + + /* Verify the tagger line */ + tagger_line = tag_line; + + if (memcmp(tagger_line, "tagger", 6) || (tagger_line[6] == '\n')) + return -1; + + /* The actual stuff afterwards we don't care about.. */ + return 0; +} + +int main(int argc, char **argv) +{ + unsigned long size; + char buffer[MAXSIZE]; + unsigned char result_sha1[20]; + + if (argc != 1) + usage("cat <signaturefile> | git-mktag"); + + // Read the signature + size = 0; + for (;;) { + int ret = read(0, buffer + size, MAXSIZE - size); + if (!ret) + break; + if (ret < 0) { + if (errno == EAGAIN) + continue; + break; + } + size += ret; + } + + // 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", result_sha1) < 0) + die("unable to write tag file"); + printf("%s\n", sha1_to_hex(result_sha1)); + return 0; +} diff --git a/mozilla-sha1/sha1.c b/mozilla-sha1/sha1.c new file mode 100644 index 0000000000..7f6fc05e06 --- /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, void *_dataIn, int len) { + 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..f5decbf43b --- /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, void *dataIn, int len); +void SHA1_Final(unsigned char hashout[20], SHA_CTX *ctx); diff --git a/object.c b/object.c new file mode 100644 index 0000000000..c410e97c8f --- /dev/null +++ b/object.c @@ -0,0 +1,152 @@ +#include "object.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "cache.h" +#include "tag.h" + +struct object **objs; +int nr_objs; +static int obj_allocs; + +static int find_object(const unsigned char *sha1) +{ + int first = 0, last = nr_objs; + + while (first < last) { + int next = (first + last) / 2; + struct object *obj = objs[next]; + int cmp; + + cmp = memcmp(sha1, obj->sha1, 20); + if (!cmp) + return next; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + return -first-1; +} + +struct object *lookup_object(const unsigned char *sha1) +{ + int pos = find_object(sha1); + if (pos >= 0) + return objs[pos]; + return NULL; +} + +void created_object(const unsigned char *sha1, struct object *obj) +{ + int pos = find_object(sha1); + + obj->parsed = 0; + memcpy(obj->sha1, sha1, 20); + obj->type = NULL; + obj->refs = NULL; + obj->used = 0; + + if (pos >= 0) + die("Inserting %s twice\n", sha1_to_hex(sha1)); + pos = -pos-1; + + if (obj_allocs == nr_objs) { + obj_allocs = alloc_nr(obj_allocs); + objs = xrealloc(objs, obj_allocs * sizeof(struct object *)); + } + + /* Insert it into the right place */ + memmove(objs + pos + 1, objs + pos, (nr_objs - pos) * + sizeof(struct object *)); + + objs[pos] = obj; + nr_objs++; +} + +void add_ref(struct object *refer, struct object *target) +{ + struct object_list **pp = &refer->refs; + struct object_list *p; + + while ((p = *pp) != NULL) { + if (p->item == target) + return; + pp = &p->next; + } + + target->used = 1; + p = xmalloc(sizeof(*p)); + p->item = target; + p->next = NULL; + *pp = p; +} + +void mark_reachable(struct object *obj, unsigned int mask) +{ + struct object_list *p = obj->refs; + + /* If we've been here already, don't bother */ + if (obj->flags & mask) + return; + obj->flags |= mask; + while (p) { + mark_reachable(p->item, mask); + p = p->next; + } +} + +struct object *lookup_object_type(const unsigned char *sha1, const char *type) +{ + 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; + } +} + +struct object *parse_object(const unsigned char *sha1) +{ + unsigned long size; + char type[20]; + 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)); + if (!strcmp(type, "blob")) { + struct blob *blob = lookup_blob(sha1); + parse_blob_buffer(blob, buffer, size); + obj = &blob->object; + } else if (!strcmp(type, "tree")) { + struct tree *tree = lookup_tree(sha1); + parse_tree_buffer(tree, buffer, size); + obj = &tree->object; + } else if (!strcmp(type, "commit")) { + struct commit *commit = lookup_commit(sha1); + parse_commit_buffer(commit, buffer, size); + if (!commit->buffer) { + commit->buffer = buffer; + buffer = NULL; + } + obj = &commit->object; + } else if (!strcmp(type, "tag")) { + struct tag *tag = lookup_tag(sha1); + parse_tag_buffer(tag, buffer, size); + obj = &tag->object; + } else { + obj = NULL; + } + free(buffer); + return obj; + } + return NULL; +} diff --git a/object.h b/object.h new file mode 100644 index 0000000000..d82c7c454b --- /dev/null +++ b/object.h @@ -0,0 +1,38 @@ +#ifndef OBJECT_H +#define OBJECT_H + +struct object_list { + struct object *item; + struct object_list *next; + const char *name; +}; + +struct object { + unsigned parsed : 1; + unsigned used : 1; + unsigned int flags; + unsigned char sha1[20]; + const char *type; + struct object_list *refs; + void *util; +}; + +extern int nr_objs; +extern struct object **objs; + +/** 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); + +void add_ref(struct object *refer, struct object *target); + +void mark_reachable(struct object *obj, unsigned int mask); + +#endif /* OBJECT_H */ diff --git a/pack-check.c b/pack-check.c new file mode 100644 index 0000000000..054b0131e9 --- /dev/null +++ b/pack-check.c @@ -0,0 +1,143 @@ +#include "cache.h" +#include "pack.h" + +static int verify_packfile(struct packed_git *p) +{ + unsigned long index_size = p->index_size; + void *index_base = p->index_base; + SHA_CTX ctx; + unsigned char sha1[20]; + unsigned long pack_size = p->pack_size; + void *pack_base; + struct pack_header *hdr; + int nr_objects, err, i; + + /* Header consistency check */ + hdr = p->pack_base; + if (hdr->hdr_signature != htonl(PACK_SIGNATURE)) + return error("Packfile signature mismatch", p->pack_name); + if (hdr->hdr_version != htonl(PACK_VERSION)) + return error("Packfile version %d different from ours %d", + ntohl(hdr->hdr_version), PACK_VERSION); + nr_objects = ntohl(hdr->hdr_entries); + if (num_packed_objects(p) != nr_objects) + return error("Packfile claims to have %d objects, " + "while idx size expects %d", nr_objects, + num_packed_objects(p)); + + SHA1_Init(&ctx); + pack_base = p->pack_base; + SHA1_Update(&ctx, pack_base, pack_size - 20); + SHA1_Final(sha1, &ctx); + if (memcmp(sha1, index_base + index_size - 40, 20)) + return error("Packfile %s SHA1 mismatch with idx", + p->pack_name); + if (memcmp(sha1, pack_base + pack_size - 20, 20)) + return error("Packfile %s SHA1 mismatch with itself", + p->pack_name); + + /* 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. + */ + for (i = err = 0; i < nr_objects; i++) { + unsigned char sha1[20]; + struct pack_entry e; + void *data; + char type[20]; + unsigned long size; + + if (nth_packed_object_sha1(p, i, sha1)) + die("internal error pack-check nth-packed-object"); + if (!find_pack_entry_one(sha1, &e, p)) + die("internal error pack-check find-pack-entry-one"); + data = unpack_entry_gently(&e, 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; +} + + +static void show_pack_info(struct packed_git *p) +{ + struct pack_header *hdr; + int nr_objects, i; + + hdr = p->pack_base; + nr_objects = ntohl(hdr->hdr_entries); + + for (i = 0; i < nr_objects; i++) { + unsigned char sha1[20], base_sha1[20]; + struct pack_entry e; + char type[20]; + unsigned long size; + unsigned long store_size; + int delta_chain_length; + + if (nth_packed_object_sha1(p, i, sha1)) + die("internal error pack-check nth-packed-object"); + if (!find_pack_entry_one(sha1, &e, p)) + die("internal error pack-check find-pack-entry-one"); + + packed_object_info_detail(&e, type, &size, &store_size, + &delta_chain_length, + base_sha1); + printf("%s ", sha1_to_hex(sha1)); + if (!delta_chain_length) + printf("%-6s %lu %u\n", type, size, e.offset); + else + printf("%-6s %lu %u %d %s\n", type, size, e.offset, + delta_chain_length, sha1_to_hex(base_sha1)); + } + +} + +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 (memcmp(sha1, index_base + index_size - 20, 20)) + ret = error("Packfile index for %s SHA1 mismatch", + p->pack_name); + + if (!ret) { + /* Verify pack file */ + use_packed_git(p); + ret = verify_packfile(p); + unuse_packed_git(p); + } + + if (verbose) { + if (ret) + printf("%s: bad\n", p->pack_name); + else { + use_packed_git(p); + show_pack_info(p); + unuse_packed_git(p); + printf("%s: ok\n", p->pack_name); + } + } + + return ret; +} diff --git a/pack-objects.c b/pack-objects.c new file mode 100644 index 0000000000..ed24a33300 --- /dev/null +++ b/pack-objects.c @@ -0,0 +1,471 @@ +#include <ctype.h> +#include "cache.h" +#include "object.h" +#include "delta.h" +#include "pack.h" +#include "csum-file.h" + +static const char pack_usage[] = "git-pack-objects [--incremental] [--window=N] [--depth=N] {--stdout | base-name} < object-list"; + +struct object_entry { + unsigned char sha1[20]; + unsigned long size; + unsigned long offset; + unsigned int depth; + unsigned int hash; + enum object_type type; + unsigned long delta_size; + struct object_entry *delta; +}; + +static unsigned char object_list_sha1[20]; +static int non_empty = 0; +static int incremental = 0; +static struct object_entry **sorted_by_sha, **sorted_by_type; +static struct object_entry *objects = NULL; +static int nr_objects = 0, nr_alloc = 0; +static const char *base_name; +static unsigned char pack_file_sha1[20]; + +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_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 unsigned long write_object(struct sha1file *f, struct object_entry *entry) +{ + unsigned long size; + char type[10]; + void *buf = read_sha1_file(entry->sha1, type, &size); + unsigned char header[10]; + unsigned hdrlen, datalen; + enum object_type obj_type; + + 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); + + /* + * The object header is a byte of 'type' followed by zero or + * more bytes of length. For deltas, the 20 bytes of delta sha1 + * follows that. + */ + obj_type = entry->type; + if (entry->delta) { + buf = delta_against(buf, size, entry); + size = entry->delta_size; + obj_type = OBJ_DELTA; + } + hdrlen = encode_header(obj_type, size, header); + sha1write(f, header, hdrlen); + if (entry->delta) { + sha1write(f, entry->delta, 20); + hdrlen += 20; + } + datalen = sha1write_compressed(f, buf, size); + free(buf); + return hdrlen + datalen; +} + +static unsigned long write_one(struct sha1file *f, + struct object_entry *e, + unsigned long offset) +{ + if (e->offset) + /* offset starts from header size and cannot be zero + * if it is written already. + */ + return offset; + e->offset = offset; + offset += write_object(f, e); + /* if we are delitified, write out its base object. */ + if (e->delta) + offset = write_one(f, e->delta, offset); + return offset; +} + +static void write_pack_file(void) +{ + int i; + struct sha1file *f; + unsigned long offset; + unsigned long mb; + struct pack_header hdr; + + if (!base_name) + f = sha1fd(1, "<stdout>"); + else + f = sha1create("%s-%s.%s", base_name, sha1_to_hex(object_list_sha1), "pack"); + hdr.hdr_signature = htonl(PACK_SIGNATURE); + hdr.hdr_version = htonl(PACK_VERSION); + hdr.hdr_entries = htonl(nr_objects); + sha1write(f, &hdr, sizeof(hdr)); + offset = sizeof(hdr); + for (i = 0; i < nr_objects; i++) + offset = write_one(f, objects + i, offset); + + sha1close(f, pack_file_sha1, 1); + mb = offset >> 20; + offset &= 0xfffff; +} + +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_objects; + unsigned int 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 * sizeof(int)); + + /* + * Write the actual SHA1 entries.. + */ + list = sorted_by_sha; + for (i = 0; i < nr_objects; i++) { + struct object_entry *entry = *list++; + unsigned int 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 add_object_entry(unsigned char *sha1, unsigned int hash) +{ + unsigned int idx = nr_objects; + struct object_entry *entry; + + if (incremental && has_sha1_pack(sha1)) + return 0; + + if (idx >= nr_alloc) { + unsigned int needed = (idx + 1024) * 3 / 2; + objects = xrealloc(objects, needed * sizeof(*entry)); + nr_alloc = needed; + } + entry = objects + idx; + memset(entry, 0, sizeof(*entry)); + memcpy(entry->sha1, sha1, 20); + entry->hash = hash; + nr_objects = idx+1; + return 1; +} + +static void check_object(struct object_entry *entry) +{ + char type[20]; + + if (!sha1_object_info(entry->sha1, type, &entry->size)) { + if (!strcmp(type, "commit")) { + entry->type = OBJ_COMMIT; + } else if (!strcmp(type, "tree")) { + entry->type = OBJ_TREE; + } else if (!strcmp(type, "blob")) { + entry->type = OBJ_BLOB; + } else if (!strcmp(type, "tag")) { + entry->type = OBJ_TAG; + } else + die("unable to pack object %s of type %s", + sha1_to_hex(entry->sha1), type); + } + else + die("unable to get type of object %s", + sha1_to_hex(entry->sha1)); +} + +static void get_object_details(void) +{ + int i; + struct object_entry *entry = objects; + + for (i = 0; i < nr_objects; i++) + check_object(entry++); +} + +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 memcmp(a->sha1, b->sha1, 20); +} + +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->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; +}; + +/* + * 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 *cur, struct unpacked *old, unsigned max_depth) +{ + struct object_entry *cur_entry = cur->entry; + struct object_entry *old_entry = old->entry; + unsigned long size, oldsize, delta_size, sizediff; + long max_size; + void *delta_buf; + + /* Don't bother doing diffs between different types */ + if (cur_entry->type != old_entry->type) + return -1; + + size = cur_entry->size; + if (size < 50) + return -1; + oldsize = old_entry->size; + sizediff = oldsize > size ? oldsize - size : size - oldsize; + if (sizediff > size / 8) + return -1; + if (old_entry->depth >= max_depth) + return 0; + + /* + * NOTE! + * + * We always delta from the bigger to the smaller, since that's + * more space-efficient (deletes don't have to say _what_ they + * delete). + */ + max_size = size / 2 - 20; + if (cur_entry->delta) + max_size = cur_entry->delta_size-1; + if (sizediff >= max_size) + return -1; + delta_buf = diff_delta(old->data, oldsize, + cur->data, size, &delta_size, max_size); + if (!delta_buf) + return 0; + cur_entry->delta = old_entry; + cur_entry->delta_size = delta_size; + cur_entry->depth = old_entry->depth + 1; + free(delta_buf); + return 0; +} + +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); + + memset(array, 0, array_size); + i = nr_objects; + idx = 0; + while (--i >= 0) { + struct object_entry *entry = list[i]; + struct unpacked *n = array + idx; + unsigned long size; + char type[10]; + int j; + + free(n->data); + n->entry = entry; + n->data = read_sha1_file(entry->sha1, type, &size); + if (size != entry->size) + die("object %s inconsistent object length (%lu vs %lu)", sha1_to_hex(entry->sha1), size, entry->size); + 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; + } + idx++; + if (idx >= window) + idx = 0; + } +} + +int main(int argc, char **argv) +{ + SHA_CTX ctx; + char line[PATH_MAX + 20]; + int window = 10, depth = 10, pack_to_stdout = 0; + int i; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg == '-') { + if (!strcmp("--non-empty", arg)) { + non_empty = 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("--stdout", arg)) { + pack_to_stdout = 1; + continue; + } + usage(pack_usage); + } + if (base_name) + usage(pack_usage); + base_name = arg; + } + + if (pack_to_stdout != !base_name) + usage(pack_usage); + + SHA1_Init(&ctx); + while (fgets(line, sizeof(line), stdin) != NULL) { + unsigned int hash; + char *p; + unsigned char sha1[20]; + + if (get_sha1_hex(line, sha1)) + die("expected sha1, got garbage"); + hash = 0; + p = line+40; + while (*p) { + unsigned char c = *p++; + if (isspace(c)) + continue; + hash = hash * 11 + c; + } + if (add_object_entry(sha1, hash)) + SHA1_Update(&ctx, sha1, 20); + } + SHA1_Final(object_list_sha1, &ctx); + if (non_empty && !nr_objects) + return 0; + get_object_details(); + + fprintf(stderr, "Packing %d objects\n", nr_objects); + + sorted_by_sha = create_sorted_list(sha1_sort); + sorted_by_type = create_sorted_list(type_size_sort); + if (window && depth) + find_deltas(sorted_by_type, window+1, depth); + + write_pack_file(); + if (!pack_to_stdout) { + write_index_file(); + puts(sha1_to_hex(object_list_sha1)); + } + return 0; +} diff --git a/pack.h b/pack.h new file mode 100644 index 0000000000..657deaa3f4 --- /dev/null +++ b/pack.h @@ -0,0 +1,32 @@ +#ifndef PACK_H +#define PACK_H + +/* + * The packed object type is stored in 3 bits. + * The type value 0 is a reserved prefix if ever there is more than 7 + * object types, or any future format extensions. + */ +enum object_type { + OBJ_EXT = 0, + OBJ_COMMIT = 1, + OBJ_TREE = 2, + OBJ_BLOB = 3, + OBJ_TAG = 4, + /* 5/6 for future expansion */ + OBJ_DELTA = 7, +}; + +/* + * Packed object header + */ +#define PACK_SIGNATURE 0x5041434b /* "PACK" */ +#define PACK_VERSION 2 +struct pack_header { + unsigned int hdr_signature; + unsigned int hdr_version; + unsigned int hdr_entries; +}; + +extern int verify_pack(struct packed_git *, int); + +#endif diff --git a/patch-delta.c b/patch-delta.c new file mode 100644 index 0000000000..26281ea123 --- /dev/null +++ b/patch-delta.c @@ -0,0 +1,72 @@ +/* + * 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 <stdlib.h> +#include <string.h> +#include "delta.h" + +void *patch_delta(void *src_buf, unsigned long src_size, + 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 = delta_buf + delta_size; + + /* make sure the orig file size matches what we expect */ + size = get_delta_hdr_size(&data); + if (size != src_size) + return NULL; + + /* now the result size */ + size = get_delta_hdr_size(&data); + dst_buf = malloc(size); + if (!dst_buf) + return NULL; + + out = dst_buf; + while (data < top) { + cmd = *data++; + if (cmd & 0x80) { + unsigned long cp_off = 0, cp_size = 0; + const unsigned char *buf; + 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 (cp_size == 0) cp_size = 0x10000; + buf = (cmd & 0x40) ? dst_buf : src_buf; + memcpy(out, buf + cp_off, cp_size); + out += cp_size; + } else { + memcpy(out, data, cmd); + out += cmd; + data += cmd; + } + } + + /* sanity check */ + if (data != top || out - dst_buf != size) { + free(dst_buf); + return NULL; + } + + *dst_size = size; + return dst_buf; +} diff --git a/patch-id.c b/patch-id.c new file mode 100644 index 0000000000..0bd640cc48 --- /dev/null +++ b/patch-id.c @@ -0,0 +1,79 @@ +#include <ctype.h> +#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; + + if (!get_sha1_hex(p, n)) { + flush_current_id(patchlen, sha1, &ctx); + memcpy(sha1, n, 20); + patchlen = 0; + continue; + } + + /* Ignore commit comments */ + if (!patchlen && memcmp(line, "diff ", 5)) + 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[] = "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.c b/path.c new file mode 100644 index 0000000000..d217ef0b7f --- /dev/null +++ b/path.c @@ -0,0 +1,60 @@ +/* + * 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 pathname[PATH_MAX]; +static char bad_path[] = "/bad-path/"; + +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; + + 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 = gitenv(GIT_DIR_ENVIRONMENT) ? : DEFAULT_GIT_DIR_ENVIRONMENT; + 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); +} diff --git a/pkt-line.c b/pkt-line.c new file mode 100644 index 0000000000..69473046bf --- /dev/null +++ b/pkt-line.c @@ -0,0 +1,117 @@ +#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. + */ +static void safe_write(int fd, const void *buf, unsigned n) +{ + while (n) { + int ret = write(fd, buf, n); + if (ret > 0) { + buf += ret; + n -= ret; + continue; + } + if (!ret) + die("write error (disk full?)"); + if (errno == EAGAIN || errno == EINTR) + continue; + die("write error (%s)", strerror(errno)); + } +} + +/* + * 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 = read(fd, buffer + n, size - n); + if (ret < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + die("read error (%s)", strerror(errno)); + } + if (!ret) + die("unexpected EOF"); + 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..b0b4f6d495 --- /dev/null +++ b/pkt-line.h @@ -0,0 +1,12 @@ +#ifndef PKTLINE_H +#define PKTLINE_H + +/* + * Silly packetized line writing interface + */ +void packet_flush(int fd); +void packet_write(int fd, const char *fmt, ...); + +int packet_read_line(int fd, char *buffer, unsigned size); + +#endif diff --git a/ppc/sha1.c b/ppc/sha1.c new file mode 100644 index 0000000000..5ba4fc5259 --- /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 += 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..e85611a4ef --- /dev/null +++ b/ppc/sha1ppc.S @@ -0,0 +1,185 @@ +/* + * SHA-1 implementation for PowerPC. + * + * Copyright (C) 2005 Paul Mackerras <paulus@samba.org> + */ +#define FS 80 + +/* + * We roll the registers for T, A, B, C, D, E around on each + * iteration; T on iteration t is A on iteration t+1, and so on. + * We use registers 7 - 12 for this. + */ +#define RT(t) ((((t)+5)%6)+7) +#define RA(t) ((((t)+4)%6)+7) +#define RB(t) ((((t)+3)%6)+7) +#define RC(t) ((((t)+2)%6)+7) +#define RD(t) ((((t)+1)%6)+7) +#define RE(t) ((((t)+0)%6)+7) + +/* We use registers 16 - 31 for the W values */ +#define W(t) (((t)%16)+16) + +#define STEPD0(t) \ + and %r6,RB(t),RC(t); \ + andc %r0,RD(t),RB(t); \ + rotlwi RT(t),RA(t),5; \ + rotlwi RB(t),RB(t),30; \ + or %r6,%r6,%r0; \ + add %r0,RE(t),%r15; \ + add RT(t),RT(t),%r6; \ + add %r0,%r0,W(t); \ + add RT(t),RT(t),%r0 + +#define STEPD1(t) \ + xor %r6,RB(t),RC(t); \ + rotlwi RT(t),RA(t),5; \ + rotlwi RB(t),RB(t),30; \ + xor %r6,%r6,RD(t); \ + add %r0,RE(t),%r15; \ + add RT(t),RT(t),%r6; \ + add %r0,%r0,W(t); \ + add RT(t),RT(t),%r0 + +#define STEPD2(t) \ + and %r6,RB(t),RC(t); \ + and %r0,RB(t),RD(t); \ + rotlwi RT(t),RA(t),5; \ + rotlwi RB(t),RB(t),30; \ + or %r6,%r6,%r0; \ + and %r0,RC(t),RD(t); \ + or %r6,%r6,%r0; \ + add %r0,RE(t),%r15; \ + add RT(t),RT(t),%r6; \ + add %r0,%r0,W(t); \ + add RT(t),RT(t),%r0 + +#define LOADW(t) \ + lwz W(t),(t)*4(%r4) + +#define UPDATEW(t) \ + xor %r0,W((t)-3),W((t)-8); \ + xor W(t),W((t)-16),W((t)-14); \ + xor W(t),W(t),%r0; \ + rotlwi W(t),W(t),1 + +#define STEP0LD4(t) \ + STEPD0(t); LOADW((t)+4); \ + STEPD0((t)+1); LOADW((t)+5); \ + STEPD0((t)+2); LOADW((t)+6); \ + STEPD0((t)+3); LOADW((t)+7) + +#define STEPUP4(t, fn) \ + STEP##fn(t); UPDATEW((t)+4); \ + STEP##fn((t)+1); UPDATEW((t)+5); \ + STEP##fn((t)+2); UPDATEW((t)+6); \ + STEP##fn((t)+3); UPDATEW((t)+7) + +#define STEPUP20(t, fn) \ + STEPUP4(t, fn); \ + STEPUP4((t)+4, fn); \ + STEPUP4((t)+8, fn); \ + STEPUP4((t)+12, fn); \ + STEPUP4((t)+16, fn) + + .globl sha1_core +sha1_core: + stwu %r1,-FS(%r1) + stw %r15,FS-68(%r1) + stw %r16,FS-64(%r1) + stw %r17,FS-60(%r1) + stw %r18,FS-56(%r1) + stw %r19,FS-52(%r1) + stw %r20,FS-48(%r1) + stw %r21,FS-44(%r1) + stw %r22,FS-40(%r1) + stw %r23,FS-36(%r1) + stw %r24,FS-32(%r1) + stw %r25,FS-28(%r1) + stw %r26,FS-24(%r1) + stw %r27,FS-20(%r1) + stw %r28,FS-16(%r1) + stw %r29,FS-12(%r1) + stw %r30,FS-8(%r1) + stw %r31,FS-4(%r1) + + /* Load up A - E */ + lwz RA(0),0(%r3) /* A */ + lwz RB(0),4(%r3) /* B */ + lwz RC(0),8(%r3) /* C */ + lwz RD(0),12(%r3) /* D */ + lwz RE(0),16(%r3) /* E */ + + mtctr %r5 + +1: LOADW(0) + LOADW(1) + LOADW(2) + LOADW(3) + + lis %r15,0x5a82 /* K0-19 */ + ori %r15,%r15,0x7999 + STEP0LD4(0) + STEP0LD4(4) + STEP0LD4(8) + STEPUP4(12, D0) + STEPUP4(16, D0) + + lis %r15,0x6ed9 /* K20-39 */ + ori %r15,%r15,0xeba1 + STEPUP20(20, D1) + + lis %r15,0x8f1b /* K40-59 */ + ori %r15,%r15,0xbcdc + STEPUP20(40, D2) + + lis %r15,0xca62 /* K60-79 */ + ori %r15,%r15,0xc1d6 + STEPUP4(60, D1) + STEPUP4(64, D1) + STEPUP4(68, D1) + STEPUP4(72, D1) + STEPD1(76) + STEPD1(77) + STEPD1(78) + STEPD1(79) + + lwz %r20,16(%r3) + lwz %r19,12(%r3) + lwz %r18,8(%r3) + lwz %r17,4(%r3) + lwz %r16,0(%r3) + add %r20,RE(80),%r20 + add RD(0),RD(80),%r19 + add RC(0),RC(80),%r18 + add RB(0),RB(80),%r17 + add RA(0),RA(80),%r16 + mr RE(0),%r20 + stw RA(0),0(%r3) + stw RB(0),4(%r3) + stw RC(0),8(%r3) + stw RD(0),12(%r3) + stw RE(0),16(%r3) + + addi %r4,%r4,64 + bdnz 1b + + lwz %r15,FS-68(%r1) + lwz %r16,FS-64(%r1) + lwz %r17,FS-60(%r1) + lwz %r18,FS-56(%r1) + lwz %r19,FS-52(%r1) + lwz %r20,FS-48(%r1) + lwz %r21,FS-44(%r1) + lwz %r22,FS-40(%r1) + lwz %r23,FS-36(%r1) + lwz %r24,FS-32(%r1) + lwz %r25,FS-28(%r1) + lwz %r26,FS-24(%r1) + lwz %r27,FS-20(%r1) + lwz %r28,FS-16(%r1) + lwz %r29,FS-12(%r1) + lwz %r30,FS-8(%r1) + lwz %r31,FS-4(%r1) + addi %r1,%r1,FS + blr diff --git a/prune-packed.c b/prune-packed.c new file mode 100644 index 0000000000..41ee2a73bc --- /dev/null +++ b/prune-packed.c @@ -0,0 +1,66 @@ +#include "cache.h" + +static const char prune_packed_usage[] = "git-prune-packed: usage: git-prune-packed"; + +static void prune_dir(int i, DIR *dir, char *pathname, int len) +{ + 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)) + continue; + memcpy(pathname + len, de->d_name, 38); + if (unlink(pathname) < 0) + error("unable to unlink %s", pathname); + } +} + +static void prune_packed_objects(void) +{ + 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 (!d) + die("unable to open %s", pathname); + prune_dir(i, d, pathname, len + 3); + closedir(d); + } +} + +int main(int argc, char **argv) +{ + int i; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg == '-') { + /* Handle flags here .. */ + usage(prune_packed_usage); + } + /* Handle arguments here .. */ + usage(prune_packed_usage); + } + prune_packed_objects(); + return 0; +} diff --git a/pull.c b/pull.c new file mode 100644 index 0000000000..ed3078e3b2 --- /dev/null +++ b/pull.c @@ -0,0 +1,178 @@ +#include "pull.h" + +#include "cache.h" +#include "commit.h" +#include "tree.h" +#include "tag.h" +#include "blob.h" +#include "refs.h" + +const char *write_ref = NULL; + +const unsigned char *current_ref = NULL; + +int get_tree = 0; +int get_history = 0; +int get_all = 0; +int get_verbosely = 0; +static unsigned char current_commit_sha1[20]; + +static const char commitS[] = "commit"; +static const char treeS[] = "tree"; +static const char blobS[] = "blob"; + +void pull_say(const char *fmt, const char *hex) { + if (get_verbosely) + fprintf(stderr, fmt, hex); +} + +static void report_missing(const char *what, const unsigned char *missing) +{ + char missing_hex[41]; + + strcpy(missing_hex, sha1_to_hex(missing));; + fprintf(stderr, + "Cannot obtain needed %s %s\nwhile processing commit %s.\n", + what, missing_hex, sha1_to_hex(current_commit_sha1)); +} + +static int make_sure_we_have_it(const char *what, unsigned char *sha1) +{ + int status = 0; + + if (!has_sha1_file(sha1)) { + status = fetch(sha1); + if (status && what) + report_missing(what, sha1); + } + return status; +} + +static int process_unknown(unsigned char *sha1); + +static int process_tree(unsigned char *sha1) +{ + struct tree *tree = lookup_tree(sha1); + struct tree_entry_list *entries; + + if (parse_tree(tree)) + return -1; + + for (entries = tree->entries; entries; entries = entries->next) { + const char *what = entries->directory ? treeS : blobS; + if (make_sure_we_have_it(what, entries->item.tree->object.sha1)) + return -1; + if (entries->directory) { + if (process_tree(entries->item.tree->object.sha1)) + return -1; + } + } + return 0; +} + +static int process_commit(unsigned char *sha1) +{ + struct commit *obj = lookup_commit(sha1); + + if (make_sure_we_have_it(commitS, sha1)) + return -1; + + if (parse_commit(obj)) + return -1; + + if (get_tree) { + if (make_sure_we_have_it(treeS, obj->tree->object.sha1)) + return -1; + if (process_tree(obj->tree->object.sha1)) + return -1; + if (!get_all) + get_tree = 0; + } + if (get_history) { + struct commit_list *parents = obj->parents; + for (; parents; parents = parents->next) { + if (has_sha1_file(parents->item->object.sha1)) + continue; + if (make_sure_we_have_it(NULL, + parents->item->object.sha1)) { + /* The server might not have it, and + * we don't mind. + */ + continue; + } + if (process_commit(parents->item->object.sha1)) + return -1; + memcpy(current_commit_sha1, sha1, 20); + } + } + return 0; +} + +static int process_tag(unsigned char *sha1) +{ + struct tag *obj = lookup_tag(sha1); + + if (parse_tag(obj)) + return -1; + return process_unknown(obj->tagged->sha1); +} + +static int process_unknown(unsigned char *sha1) +{ + struct object *obj; + if (make_sure_we_have_it("object", sha1)) + return -1; + obj = parse_object(sha1); + if (!obj) + return error("Unable to parse object %s", sha1_to_hex(sha1)); + if (obj->type == commit_type) + return process_commit(sha1); + if (obj->type == tree_type) + return process_tree(sha1); + if (obj->type == blob_type) + return 0; + if (obj->type == tag_type) + return process_tag(sha1); + return error("Unable to determine requirement of type %s for %s", + obj->type, sha1_to_hex(sha1)); +} + +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; +} + + +int pull(char *target) +{ + unsigned char sha1[20]; + int fd = -1; + + if (write_ref && current_ref) { + fd = lock_ref_sha1(write_ref, current_ref); + if (fd < 0) + return -1; + } + + if (interpret_target(target, sha1)) + return error("Could not interpret %s as something to pull", + target); + if (process_unknown(sha1)) + return -1; + + if (write_ref) { + if (current_ref) { + write_ref_sha1(write_ref, fd, sha1); + } else { + write_ref_sha1_unlocked(write_ref, sha1); + } + } + return 0; +} diff --git a/pull.h b/pull.h new file mode 100644 index 0000000000..e173ae3337 --- /dev/null +++ b/pull.h @@ -0,0 +1,41 @@ +#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 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); + +/* If set, the ref filename to write the target value to. */ +extern const char *write_ref; + +/* If set, the hash that the current value of write_ref must be. */ +extern const unsigned char *current_ref; + +/* 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; + +/* Report what we got under get_verbosely */ +extern void pull_say(const char *, const char *); + +extern int pull(char *target); + +#endif /* PULL_H */ diff --git a/quote.c b/quote.c new file mode 100644 index 0000000000..5e6fda311c --- /dev/null +++ b/quote.c @@ -0,0 +1,41 @@ +#include "cache.h" +#include "quote.h" + +/* Help to copy the thing properly quoted for the shell safety. + * any single quote is replaced with '\'', and the caller is + * expected to enclose the result within a single quote pair. + * + * E.g. + * original sq_quote result + * name ==> name ==> 'name' + * a b ==> a b ==> 'a b' + * a'b ==> a'\''b ==> 'a'\''b' + */ +char *sq_quote(const char *src) +{ + static char *buf = NULL; + int cnt, c; + const char *cp; + char *bp; + + /* count bytes needed to store the quoted string. */ + for (cnt = 3, cp = src; *cp; cnt++, cp++) + if (*cp == '\'') + cnt += 3; + + buf = xmalloc(cnt); + bp = buf; + *bp++ = '\''; + while ((c = *src++)) { + if (c != '\'') + *bp++ = c; + else { + bp = strcpy(bp, "'\\''"); + bp += 4; + } + } + *bp++ = '\''; + *bp = 0; + return buf; +} + diff --git a/quote.h b/quote.h new file mode 100644 index 0000000000..c8cfb3a124 --- /dev/null +++ b/quote.h @@ -0,0 +1,26 @@ +#ifndef QUOTE_H +#define QUOTE_H + + +/* Help to copy the thing properly quoted for the shell safety. + * any single quote 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. + */ + +char *sq_quote(const char *src); + +#endif diff --git a/read-cache.c b/read-cache.c new file mode 100644 index 0000000000..f448ab17e2 --- /dev/null +++ b/read-cache.c @@ -0,0 +1,497 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" + +struct cache_entry **active_cache = NULL; +unsigned int active_nr = 0, active_alloc = 0, active_cache_changed = 0; + +/* + * 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); +} + +int ce_match_stat(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 (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 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; + 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(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 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; + } + return 0; +} + +/* + * 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 (!skip_df_check && check_file_directory_conflict(ce, pos, ok_to_replace)) { + if (!ok_to_replace) + return -1; + 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; +} + +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 (memcmp(sha1, (void *)hdr + size - 20, 20)) + return error("bad index file sha1 signature"); + return 0; +} + +int read_cache(void) +{ + int fd, i; + struct stat st; + unsigned long size, offset; + void *map; + struct cache_header *hdr; + + errno = EBUSY; + if (active_cache) + return error("more than one cachefile"); + errno = ENOENT; + fd = open(get_index_file(), O_RDONLY); + if (fd < 0) + return (errno == ENOENT) ? 0 : error("open failed"); + + size = 0; // avoid gcc warning + map = (void *)-1; + if (!fstat(fd, &st)) { + size = st.st_size; + errno = EINVAL; + if (size >= sizeof(struct cache_header) + 20) + map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); + } + close(fd); + if (-1 == (int)(long)map) + return error("mmap failed"); + + hdr = map; + if (verify_hdr(hdr, size) < 0) + goto unmap; + + active_nr = ntohl(hdr->hdr_entries); + active_alloc = alloc_nr(active_nr); + active_cache = calloc(active_alloc, sizeof(struct cache_entry *)); + + offset = sizeof(*hdr); + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = map + offset; + offset = offset + ce_size(ce); + active_cache[i] = ce; + } + return active_nr; + +unmap: + munmap(map, size); + errno = EINVAL; + return error("verify header failed"); +} + +#define WRITE_BUFFER_SIZE 8192 +static unsigned char write_buffer[WRITE_BUFFER_SIZE]; +static unsigned long write_buffer_len; + +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) { + SHA1_Update(context, write_buffer, WRITE_BUFFER_SIZE); + if (write(fd, write_buffer, WRITE_BUFFER_SIZE) != WRITE_BUFFER_SIZE) + return -1; + buffered = 0; + } + write_buffer_len = buffered; + len -= partial; + data += partial; + } + return 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); + } + + /* Append the SHA1 signature at the end */ + SHA1_Final(write_buffer + left, context); + left += 20; + if (write(fd, write_buffer, left) != left) + return -1; + return 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 (ce_write(&c, newfd, ce, ce_size(ce)) < 0) + return -1; + } + return ce_flush(&c, newfd); +} diff --git a/read-tree.c b/read-tree.c new file mode 100644 index 0000000000..65f2d9a5dc --- /dev/null +++ b/read-tree.c @@ -0,0 +1,603 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" + +static int stage = 0; +static int update = 0; + +static int unpack_tree(unsigned char *sha1) +{ + void *buffer; + unsigned long size; + int ret; + + buffer = read_object_with_reference(sha1, "tree", &size, NULL); + if (!buffer) + return -1; + ret = read_tree(buffer, size, stage, NULL); + free(buffer); + return ret; +} + +static int path_matches(struct cache_entry *a, struct cache_entry *b) +{ + int len = ce_namelen(a); + return ce_namelen(b) == len && + !memcmp(a->name, b->name, len); +} + +static int same(struct cache_entry *a, struct cache_entry *b) +{ + return a->ce_mode == b->ce_mode && + !memcmp(a->sha1, b->sha1, 20); +} + + +/* + * This removes all trivial merges that don't change the tree + * and collapses them to state 0. + */ +static struct cache_entry *merge_entries(struct cache_entry *a, + struct cache_entry *b, + struct cache_entry *c) +{ + /* + * Ok, all three entries describe the same + * filename, but maybe the contents or file + * mode have changed? + * + * The trivial cases end up being the ones where two + * out of three files are the same: + * - both destinations the same, trivially take either + * - one of the destination versions hasn't changed, + * take the other. + * + * The "all entries exactly the same" case falls out as + * a special case of any of the "two same" cases. + * + * Here "a" is "original", and "b" and "c" are the two + * trees we are merging. + */ + if (a && b && c) { + if (same(b,c)) + return c; + if (same(a,b)) + return c; + if (same(a,c)) + return b; + } + return NULL; +} + +/* + * 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 stat st; + + if (!lstat(ce->name, &st)) { + unsigned changed = ce_match_stat(ce, &st); + if (!changed) + return; + errno = 0; + } + if (errno == ENOENT) + return; + die("Entry '%s' not uptodate. Cannot merge.", ce->name); +} + +/* + * If the old tree contained a CE that isn't even in the + * result, that's always a problem, regardless of whether + * it's up-to-date or not (ie it can be a file that we + * have updated but not committed yet). + */ +static void reject_merge(struct cache_entry *ce) +{ + die("Entry '%s' would be overwritten by merge. Cannot merge.", ce->name); +} + +static int merged_entry_internal(struct cache_entry *merge, struct cache_entry *old, struct cache_entry **dst, int allow_dirty) +{ + 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 if (!allow_dirty) { + verify_uptodate(old); + } + } + merge->ce_flags &= ~htons(CE_STAGEMASK); + *dst++ = merge; + return 1; +} + +static int merged_entry_allow_dirty(struct cache_entry *merge, struct cache_entry *old, struct cache_entry **dst) +{ + return merged_entry_internal(merge, old, dst, 1); +} + +static int merged_entry(struct cache_entry *merge, struct cache_entry *old, struct cache_entry **dst) +{ + return merged_entry_internal(merge, old, dst, 0); +} + +static int deleted_entry(struct cache_entry *ce, struct cache_entry *old, struct cache_entry **dst) +{ + if (old) + verify_uptodate(old); + ce->ce_mode = 0; + *dst++ = ce; + return 1; +} + +static int causes_df_conflict(struct cache_entry *ce, int stage, + struct cache_entry **dst_, + struct cache_entry **next_, + int tail) +{ + /* This is called during the merge operation and walking + * the active_cache[] array is messy, because it is in the + * middle of overlapping copy operation. The invariants + * are: + * (1) active_cache points at the first (zeroth) entry. + * (2) up to dst pointer are resolved entries. + * (3) from the next pointer (head-inclusive) to the tail + * of the active_cache array have the remaining paths + * to be processed. There can be a gap between dst + * and next. Note that next is called "src" in the + * merge_cache() function, and tail is the original + * end of active_cache array when merge_cache() started. + * (4) the path corresponding to *ce is not found in (2) + * or (3). It is in the gap. + * + * active_cache -----......+++++++++++++. + * ^dst ^next ^tail + */ + int i, next, dst; + const char *path = ce->name; + int namelen = ce_namelen(ce); + + next = next_ - active_cache; + dst = dst_ - active_cache; + + for (i = 0; i < tail; i++) { + int entlen, len; + const char *one, *two; + if (dst <= i && i < next) + continue; + ce = active_cache[i]; + if (ce_stage(ce) != stage) + continue; + /* If ce->name is a prefix of path, then path is a file + * that hangs underneath ce->name, which is bad. + * If path is a prefix of ce->name, then it is the + * other way around which also is bad. + */ + entlen = ce_namelen(ce); + if (namelen == entlen) + continue; + if (namelen < entlen) { + len = namelen; + one = path; + two = ce->name; + } else { + len = entlen; + one = ce->name; + two = path; + } + if (memcmp(one, two, len)) + continue; + if (two[len] == '/') + return 1; + } + return 0; +} + +static int threeway_merge(struct cache_entry *stages[4], + struct cache_entry **dst, + struct cache_entry **next, int tail) +{ + struct cache_entry *old = stages[0]; + struct cache_entry *a = stages[1], *b = stages[2], *c = stages[3]; + struct cache_entry *merge; + int count; + + /* #5ALT */ + if (!a && b && c && same(b, c)) { + if (old && !same(b, old)) + return -1; + return merged_entry_allow_dirty(b, old, dst); + } + /* #2ALT and #3ALT */ + if (!a && (!!b != !!c)) { + /* + * The reason we need to worry about directory/file + * conflicts only in #2ALT and #3ALT case is this: + * + * (1) For all other cases that read-tree internally + * resolves a path, we always have such a path in + * *both* stage2 and stage3 when we begin. + * Traditionally, the behaviour has been even + * stricter and we did not resolve a path without + * initially being in all of stage1, 2, and 3. + * + * (2) When read-tree finishes, all resolved paths (i.e. + * the paths that are in stage0) must have come from + * either stage2 or stage3. It is not possible to + * have a stage0 path as a result of a merge if + * neither stage2 nor stage3 had that path. + * + * (3) It is guaranteed that just after reading the + * stages, each stage cannot have directory/file + * conflicts on its own, because they are populated + * by reading hierarchy of a tree. Combined with + * (1) and (2) above, this means that no matter what + * combination of paths we take from stage2 and + * stage3 as a result of a merge, they cannot cause + * a directory/file conflict situation (otherwise + * the "guilty" path would have already had such a + * conflict in the original stage, either stage2 + * or stage3). Although its stage2 is synthesized + * by overlaying the current index on top of "our + * head" tree, --emu23 case also has this guarantee, + * by calling add_cache_entry() to create such stage2 + * entries. + * + * (4) Only #2ALT and #3ALT lack the guarantee (1). + * They resolve paths that exist only in stage2 + * or stage3. The stage2 tree may have a file DF + * while stage3 tree may have a file DF/DF. If + * #2ALT and #3ALT rules happen to apply to both + * of them, we would end up having DF (coming from + * stage2) and DF/DF (from stage3) in the result. + * When we attempt to resolve a path that exists + * only in stage2, we need to make sure there is + * no path that would conflict with it in stage3 + * and vice versa. + */ + if (c) { /* #2ALT */ + if (!causes_df_conflict(c, 2, dst, next, tail) && + (!old || same(c, old))) + return merged_entry_allow_dirty(c, old, dst); + } + else { /* #3ALT */ + if (!causes_df_conflict(b, 3, dst, next, tail) && + (!old || same(b, old))) + return merged_entry_allow_dirty(b, old, dst); + } + /* otherwise we will apply the original rule */ + } + /* #14ALT */ + if (a && b && c && same(a, b) && !same(a, c)) { + if (old && same(old, c)) + return merged_entry_allow_dirty(c, old, dst); + /* otherwise the regular rule applies */ + } + /* + * If we have an entry in the index cache ("old"), then we want + * to make sure that it matches any entries in stage 2 ("first + * branch", aka "b"). + */ + if (old) { + if (!b || !same(old, b)) + return -1; + } + merge = merge_entries(a, b, c); + if (merge) + return merged_entry(merge, old, dst); + if (old) + verify_uptodate(old); + count = 0; + if (a) { *dst++ = a; count++; } + if (b) { *dst++ = b; count++; } + if (c) { *dst++ = c; count++; } + 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>. + * + */ +static int twoway_merge(struct cache_entry **src, struct cache_entry **dst, + struct cache_entry **next, int tail) +{ + struct cache_entry *current = src[0]; + struct cache_entry *oldtree = src[1], *newtree = src[2]; + + if (src[3]) + return -1; + + 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))) { + *dst++ = current; + return 1; + } + else if (oldtree && !newtree && same(current, oldtree)) { + /* 10 or 11 */ + return deleted_entry(oldtree, current, dst); + } + else if (oldtree && newtree && + same(current, oldtree) && !same(current, newtree)) { + /* 20 or 21 */ + return merged_entry(newtree, current, dst); + } + else + /* all other failures */ + return -1; + } + else if (newtree) + return merged_entry(newtree, current, dst); + else + return deleted_entry(oldtree, current, dst); +} + +/* + * Two-way merge emulated with three-way merge. + * + * This treats "read-tree -m H M" by transforming it internally + * into "read-tree -m H I+H M", where I+H is a tree that would + * contain the contents of the current index file, overlayed on + * top of H. Unlike the traditional two-way merge, this leaves + * the stages in the resulting index file and lets the user resolve + * the merge conflicts using standard tools for three-way merge. + * + * This function is just to set-up such an arrangement, and the + * actual merge uses threeway_merge() function. + */ +static void setup_emu23(void) +{ + /* stage0 contains I, stage1 H, stage2 M. + * move stage2 to stage3, and create stage2 entries + * by scanning stage0 and stage1 entries. + */ + int i, namelen, size; + struct cache_entry *ce, *stage2; + + for (i = 0; i < active_nr; i++) { + ce = active_cache[i]; + if (ce_stage(ce) != 2) + continue; + /* hoist them up to stage 3 */ + namelen = ce_namelen(ce); + ce->ce_flags = create_ce_flags(namelen, 3); + } + + for (i = 0; i < active_nr; i++) { + ce = active_cache[i]; + if (ce_stage(ce) > 1) + continue; + namelen = ce_namelen(ce); + size = cache_entry_size(namelen); + stage2 = xmalloc(size); + memcpy(stage2, ce, size); + stage2->ce_flags = create_ce_flags(namelen, 2); + if (add_cache_entry(stage2, ADD_CACHE_OK_TO_ADD) < 0) + die("cannot merge index and our head tree"); + + /* We are done with this name, so skip to next name */ + while (i < active_nr && + ce_namelen(active_cache[i]) == namelen && + !memcmp(active_cache[i]->name, ce->name, namelen)) + i++; + i--; /* compensate for the loop control */ + } +} + +/* + * One-way merge. + * + * The rule is: + * - take the stat information from stage0, take the data from stage1 + */ +static int oneway_merge(struct cache_entry **src, struct cache_entry **dst, + struct cache_entry **next, int tail) +{ + struct cache_entry *old = src[0]; + struct cache_entry *a = src[1]; + + if (src[2] || src[3]) + return -1; + + if (!a) + return 0; + if (old && same(old, a)) { + *dst++ = old; + return 1; + } + return merged_entry(a, NULL, dst); +} + +static void check_updates(struct cache_entry **src, int nr) +{ + static struct checkout state = { + .base_dir = "", + .force = 1, + .quiet = 1, + .refresh_cache = 1, + }; + unsigned short mask = htons(CE_UPDATE); + while (nr--) { + struct cache_entry *ce = *src++; + if (!ce->ce_mode) { + if (update) + unlink(ce->name); + continue; + } + if (ce->ce_flags & mask) { + ce->ce_flags &= ~mask; + if (update) + checkout_entry(ce, &state); + } + } +} + +typedef int (*merge_fn_t)(struct cache_entry **, struct cache_entry **, struct cache_entry **, int); + +static void merge_cache(struct cache_entry **src, int nr, merge_fn_t fn) +{ + struct cache_entry **dst = src; + int tail = nr; + + while (nr) { + int entries; + struct cache_entry *name, *ce, *stages[4] = { NULL, }; + + name = ce = *src; + for (;;) { + int stage = ce_stage(ce); + stages[stage] = ce; + ce = *++src; + active_nr--; + if (!--nr) + break; + if (!path_matches(ce, name)) + break; + } + + entries = fn(stages, dst, src, tail); + if (entries < 0) + reject_merge(name); + dst += entries; + active_nr += entries; + } + check_updates(active_cache, active_nr); +} + +static int read_cache_unmerged(void) +{ + int i, deleted; + struct cache_entry **dst; + + read_cache(); + dst = active_cache; + deleted = 0; + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + if (ce_stage(ce)) { + deleted++; + continue; + } + if (deleted) + *dst = ce; + dst++; + } + active_nr -= deleted; + return deleted; +} + +static char *read_tree_usage = "git-read-tree (<sha> | -m [-u] <sha1> [<sha2> [<sha3>]])"; + +static struct cache_file cache_file; + +int main(int argc, char **argv) +{ + int i, newfd, merge, reset, emu23; + unsigned char sha1[20]; + + newfd = hold_index_file_for_update(&cache_file, get_index_file()); + if (newfd < 0) + die("unable to create new cachefile"); + + merge = 0; + reset = 0; + emu23 = 0; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + /* "-u" means "update", meaning that a merge will update the working directory */ + if (!strcmp(arg, "-u")) { + update = 1; + continue; + } + + /* This differs from "-m" in that we'll silently ignore unmerged entries */ + if (!strcmp(arg, "--reset")) { + if (stage || merge || emu23) + usage(read_tree_usage); + reset = 1; + merge = 1; + stage = 1; + read_cache_unmerged(); + continue; + } + + /* "-m" stands for "merge", meaning we start in stage 1 */ + if (!strcmp(arg, "-m")) { + if (stage || merge || emu23) + usage(read_tree_usage); + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + stage = 1; + merge = 1; + continue; + } + + /* "-emu23" uses 3-way merge logic to perform fast-forward */ + if (!strcmp(arg, "--emu23")) { + if (stage || merge || emu23) + usage(read_tree_usage); + if (read_cache_unmerged()) + die("you need to resolve your current index first"); + merge = emu23 = stage = 1; + continue; + } + + if (get_sha1(arg, sha1) < 0) + usage(read_tree_usage); + if (stage > 3) + usage(read_tree_usage); + if (unpack_tree(sha1) < 0) + die("failed to unpack tree object %s", arg); + stage++; + } + if (update && !merge) + usage(read_tree_usage); + if (merge) { + static const merge_fn_t merge_function[] = { + [1] = oneway_merge, + [2] = twoway_merge, + [3] = threeway_merge, + }; + merge_fn_t fn; + + if (stage < 2 || stage > 4) + die("just how do you expect me to merge %d trees?", stage-1); + if (emu23 && stage != 3) + die("--emu23 takes only two trees"); + fn = merge_function[stage-1]; + if (stage == 3 && emu23) { + setup_emu23(); + fn = merge_function[3]; + } + merge_cache(active_cache, active_nr, fn); + } + if (write_cache(newfd, active_cache, active_nr) || + commit_index_file(&cache_file)) + die("unable to write new index file"); + return 0; +} diff --git a/receive-pack.c b/receive-pack.c new file mode 100644 index 0000000000..f39703f7af --- /dev/null +++ b/receive-pack.c @@ -0,0 +1,221 @@ +#include "cache.h" +#include "refs.h" +#include "pkt-line.h" +#include <sys/wait.h> + +static const char receive_pack_usage[] = "git-receive-pack <git-dir>"; + +static const char *unpacker = "git-unpack-objects"; + +static int show_ref(const char *path, const unsigned char *sha1) +{ + packet_write(1, "%s %s\n", sha1_to_hex(sha1), path); + return 0; +} + +static void write_head_info(void) +{ + for_each_ref(show_ref); +} + +struct command { + struct command *next; + unsigned char old_sha1[20]; + unsigned char new_sha1[20]; + char ref_name[0]; +}; + +static struct command *commands = NULL; + +static int is_all_zeroes(const char *hex) +{ + int i; + for (i = 0; i < 40; i++) + if (*hex++ != '0') + return 0; + return 1; +} + +static int verify_old_ref(const char *name, char *hex_contents) +{ + int fd, ret; + char buffer[60]; + + if (is_all_zeroes(hex_contents)) + return 0; + fd = open(name, O_RDONLY); + if (fd < 0) + return -1; + ret = read(fd, buffer, 40); + close(fd); + if (ret != 40) + return -1; + if (memcmp(buffer, hex_contents, 40)) + return -1; + return 0; +} + +static void update(const char *name, unsigned char *old_sha1, unsigned char *new_sha1) +{ + char new_hex[60], *old_hex, *lock_name; + int newfd, namelen, written; + + namelen = strlen(name); + lock_name = xmalloc(namelen + 10); + memcpy(lock_name, name, namelen); + memcpy(lock_name + namelen, ".lock", 6); + + strcpy(new_hex, sha1_to_hex(new_sha1)); + old_hex = sha1_to_hex(old_sha1); + if (!has_sha1_file(new_sha1)) + die("unpack should have generated %s, but I can't find it!", new_hex); + + newfd = open(lock_name, O_CREAT | O_EXCL | O_WRONLY, 0666); + if (newfd < 0) + die("unable to create %s (%s)", lock_name, strerror(errno)); + + /* Write the ref with an ending '\n' */ + new_hex[40] = '\n'; + new_hex[41] = 0; + written = write(newfd, new_hex, 41); + /* Remove the '\n' again */ + new_hex[40] = 0; + + close(newfd); + if (written != 41) { + unlink(lock_name); + die("unable to write %s", lock_name); + } + if (verify_old_ref(name, old_hex) < 0) { + unlink(lock_name); + die("%s changed during push", name); + } + if (rename(lock_name, name) < 0) { + unlink(lock_name); + die("unable to replace %s", name); + } + fprintf(stderr, "%s: %s -> %s\n", name, old_hex, new_hex); +} + + +/* + * 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->ref_name, cmd->old_sha1, cmd->new_sha1); + cmd = cmd->next; + } +} + +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; + int len; + + 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); + cmd = xmalloc(sizeof(struct command) + len - 80); + memcpy(cmd->old_sha1, old_sha1, 20); + memcpy(cmd->new_sha1, new_sha1, 20); + memcpy(cmd->ref_name, line + 82, len - 81); + cmd->next = NULL; + *p = cmd; + p = &cmd->next; + } +} + +static void unpack(void) +{ + pid_t pid = fork(); + + if (pid < 0) + die("unpack fork failed"); + if (!pid) { + execlp(unpacker, unpacker, NULL); + die("unpack execute failed"); + } + + for (;;) { + int status, code; + int retval = waitpid(pid, &status, 0); + + if (retval < 0) { + if (errno == EINTR) + continue; + die("waitpid failed (%s)", strerror(retval)); + } + if (retval != pid) + die("waitpid is confused"); + if (WIFSIGNALED(status)) + die("%s died of signal %d", unpacker, WTERMSIG(status)); + if (!WIFEXITED(status)) + die("%s died out of really strange complications", unpacker); + code = WEXITSTATUS(status); + if (code) + die("%s exited with error code %d", unpacker, code); + return; + } +} + +int main(int argc, char **argv) +{ + int i; + const char *dir = NULL; + + argv++; + for (i = 1; i < argc; i++) { + const 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); + + /* chdir to the directory. If that fails, try appending ".git" */ + if (chdir(dir) < 0) { + if (chdir(mkpath("%s.git", dir)) < 0) + die("unable to cd to %s", dir); + } + + /* If we have a ".git" directory, chdir to it */ + chdir(".git"); + setenv("GIT_DIR", ".", 1); + + if (access("objects", X_OK) < 0 || access("refs/heads", X_OK) < 0) + die("%s doesn't appear to be a git directory", dir); + write_head_info(); + + /* EOF */ + packet_flush(1); + + read_head_info(); + if (commands) { + unpack(); + execute_commands(); + } + return 0; +} diff --git a/refs.c b/refs.c new file mode 100644 index 0000000000..774f1630fe --- /dev/null +++ b/refs.c @@ -0,0 +1,251 @@ +#include "refs.h" +#include "cache.h" + +#include <errno.h> + +static int read_ref(const char *refname, unsigned char *sha1) +{ + int ret = -1; + int fd = open(git_path(refname), O_RDONLY); + + if (fd >= 0) { + char buffer[60]; + if (read(fd, buffer, sizeof(buffer)) >= 40) + ret = get_sha1_hex(buffer, sha1); + close(fd); + } + return ret; +} + +static int do_for_each_ref(const char *base, int (*fn)(const char *path, const unsigned char *sha1)) +{ + int retval = 0; + DIR *dir = opendir(git_path(base)); + + if (dir) { + struct dirent *de; + int baselen = strlen(base); + char *path = xmalloc(baselen + 257); + + if (!strncmp(base, "./", 2)) { + base += 2; + baselen -= 2; + } + memcpy(path, base, baselen); + if (baselen && base[baselen-1] != '/') + path[baselen++] = '/'; + + while ((de = readdir(dir)) != NULL) { + unsigned char sha1[20]; + struct stat st; + int namelen; + + if (de->d_name[0] == '.') + continue; + namelen = strlen(de->d_name); + if (namelen > 255) + continue; + memcpy(path + baselen, de->d_name, namelen+1); + if (lstat(git_path(path), &st) < 0) + continue; + if (S_ISDIR(st.st_mode)) { + retval = do_for_each_ref(path, fn); + if (retval) + break; + continue; + } + if (read_ref(path, sha1) < 0) + continue; + if (!has_sha1_file(sha1)) + continue; + retval = fn(path, sha1); + if (retval) + break; + } + free(path); + closedir(dir); + } + return retval; +} + +int head_ref(int (*fn)(const char *path, const unsigned char *sha1)) +{ + unsigned char sha1[20]; + if (!read_ref("HEAD", sha1)) + return fn("HEAD", sha1); + return 0; +} + +int for_each_ref(int (*fn)(const char *path, const unsigned char *sha1)) +{ + return do_for_each_ref("refs", fn); +} + +static char *ref_file_name(const char *ref) +{ + char *base = get_refs_directory(); + int baselen = strlen(base); + int reflen = strlen(ref); + char *ret = xmalloc(baselen + 2 + reflen); + sprintf(ret, "%s/%s", base, ref); + return ret; +} + +static char *ref_lock_file_name(const char *ref) +{ + char *base = get_refs_directory(); + int baselen = strlen(base); + int reflen = strlen(ref); + char *ret = xmalloc(baselen + 7 + reflen); + sprintf(ret, "%s/%s.lock", base, ref); + return ret; +} + +static int read_ref_file(const char *filename, unsigned char *sha1) { + int fd = open(filename, O_RDONLY); + char hex[41]; + if (fd < 0) { + return error("Couldn't open %s\n", filename); + } + if ((read(fd, hex, 41) < 41) || + (hex[40] != '\n') || + get_sha1_hex(hex, sha1)) { + error("Couldn't read a hash from %s\n", filename); + close(fd); + return -1; + } + close(fd); + return 0; +} + +int get_ref_sha1(const char *ref, unsigned char *sha1) +{ + char *filename; + int retval; + if (check_ref_format(ref)) + return -1; + filename = ref_file_name(ref); + retval = read_ref_file(filename, sha1); + free(filename); + return retval; +} + +static int lock_ref_file(const char *filename, const char *lock_filename, + const unsigned char *old_sha1) +{ + int fd = open(lock_filename, O_WRONLY | O_CREAT | O_EXCL, 0666); + unsigned char current_sha1[20]; + int retval; + if (fd < 0) { + return error("Couldn't open lock file for %s: %s", + filename, strerror(errno)); + } + retval = read_ref_file(filename, current_sha1); + if (old_sha1) { + if (retval) { + close(fd); + unlink(lock_filename); + return error("Could not read the current value of %s", + filename); + } + if (memcmp(current_sha1, old_sha1, 20)) { + close(fd); + unlink(lock_filename); + error("The current value of %s is %s", + filename, sha1_to_hex(current_sha1)); + return error("Expected %s", + sha1_to_hex(old_sha1)); + } + } else { + if (!retval) { + close(fd); + unlink(lock_filename); + return error("Unexpectedly found a value of %s for %s", + sha1_to_hex(current_sha1), filename); + } + } + return fd; +} + +int lock_ref_sha1(const char *ref, const unsigned char *old_sha1) +{ + char *filename; + char *lock_filename; + int retval; + if (check_ref_format(ref)) + return -1; + filename = ref_file_name(ref); + lock_filename = ref_lock_file_name(ref); + retval = lock_ref_file(filename, lock_filename, old_sha1); + free(filename); + free(lock_filename); + return retval; +} + +static int write_ref_file(const char *filename, + const char *lock_filename, int fd, + const unsigned char *sha1) +{ + char *hex = sha1_to_hex(sha1); + char term = '\n'; + if (write(fd, hex, 40) < 40 || + write(fd, &term, 1) < 1) { + error("Couldn't write %s\n", filename); + close(fd); + return -1; + } + close(fd); + rename(lock_filename, filename); + return 0; +} + +int write_ref_sha1(const char *ref, int fd, const unsigned char *sha1) +{ + char *filename; + char *lock_filename; + int retval; + if (fd < 0) + return -1; + if (check_ref_format(ref)) + return -1; + filename = ref_file_name(ref); + lock_filename = ref_lock_file_name(ref); + retval = write_ref_file(filename, lock_filename, fd, sha1); + free(filename); + free(lock_filename); + return retval; +} + +int check_ref_format(const char *ref) +{ + char *middle; + if (ref[0] == '.' || ref[0] == '/') + return -1; + middle = strchr(ref, '/'); + if (!middle || !middle[1]) + return -1; + if (strchr(middle + 1, '/')) + return -1; + return 0; +} + +int write_ref_sha1_unlocked(const char *ref, const unsigned char *sha1) +{ + char *filename; + char *lock_filename; + int fd; + int retval; + if (check_ref_format(ref)) + return -1; + filename = ref_file_name(ref); + lock_filename = ref_lock_file_name(ref); + fd = open(lock_filename, O_WRONLY | O_CREAT | O_EXCL, 0666); + if (fd < 0) { + error("Writing %s", lock_filename); + perror("Open"); + } + retval = write_ref_file(filename, lock_filename, fd, sha1); + free(filename); + free(lock_filename); + return retval; +} diff --git a/refs.h b/refs.h new file mode 100644 index 0000000000..2625596701 --- /dev/null +++ b/refs.h @@ -0,0 +1,28 @@ +#ifndef REFS_H +#define REFS_H + +/* + * Calls the specified function for each ref file until it returns nonzero, + * and returns the value + */ +extern int head_ref(int (*fn)(const char *path, const unsigned char *sha1)); +extern int for_each_ref(int (*fn)(const char *path, const unsigned char *sha1)); + +/** Reads the refs file specified into sha1 **/ +extern int get_ref_sha1(const char *ref, unsigned char *sha1); + +/** Locks ref and returns the fd to give to write_ref_sha1() if the ref + * has the given value currently; otherwise, returns -1. + **/ +extern int lock_ref_sha1(const char *ref, const unsigned char *old_sha1); + +/** Writes sha1 into the refs file specified, locked with the given fd. **/ +extern int write_ref_sha1(const char *ref, int fd, const unsigned char *sha1); + +/** Writes sha1 into the refs file specified. **/ +extern int write_ref_sha1_unlocked(const char *ref, const unsigned char *sha1); + +/** Returns 0 if target has the right format for a ref. **/ +extern int check_ref_format(const char *target); + +#endif /* REFS_H */ diff --git a/rev-list.c b/rev-list.c new file mode 100644 index 0000000000..fdb531caf6 --- /dev/null +++ b/rev-list.c @@ -0,0 +1,522 @@ +#include "cache.h" +#include "tag.h" +#include "commit.h" +#include "tree.h" +#include "blob.h" +#include "epoch.h" + +#define SEEN (1u << 0) +#define INTERESTING (1u << 1) +#define COUNTED (1u << 2) +#define SHOWN (1u << 3) + +static const char rev_list_usage[] = + "usage: git-rev-list [OPTION] commit-id <commit-id>\n" + " --max-count=nr\n" + " --max-age=epoch\n" + " --min-age=epoch\n" + " --bisect\n" + " --objects\n" + " --unpacked\n" + " --header\n" + " --pretty\n" + " --merge-order [ --show-breaks ]"; + +static int unpacked = 0; +static int bisect_list = 0; +static int tag_objects = 0; +static int tree_objects = 0; +static int blob_objects = 0; +static int verbose_header = 0; +static int show_parents = 0; +static int hdr_termination = 0; +static const char *prefix = ""; +static unsigned long max_age = -1; +static unsigned long min_age = -1; +static int max_count = -1; +static enum cmit_fmt commit_format = CMIT_FMT_RAW; +static int merge_order = 0; +static int show_breaks = 0; +static int stop_traversal = 0; +static int topo_order = 0; + +static void show_commit(struct commit *commit) +{ + commit->object.flags |= SHOWN; + if (show_breaks) { + prefix = "| "; + if (commit->object.flags & DISCONTINUITY) { + prefix = "^ "; + } else if (commit->object.flags & BOUNDARY) { + prefix = "= "; + } + } + printf("%s%s", prefix, sha1_to_hex(commit->object.sha1)); + if (show_parents) { + struct commit_list *parents = commit->parents; + while (parents) { + printf(" %s", sha1_to_hex(parents->item->object.sha1)); + parents = parents->next; + } + } + putchar('\n'); + if (verbose_header) { + static char pretty_header[16384]; + pretty_print_commit(commit_format, commit->buffer, ~0, pretty_header, sizeof(pretty_header)); + printf("%s%c", pretty_header, hdr_termination); + } + fflush(stdout); +} + +static int filter_commit(struct commit * commit) +{ + if (stop_traversal && (commit->object.flags & BOUNDARY)) + return STOP; + if (commit->object.flags & (UNINTERESTING|SHOWN)) + return CONTINUE; + if (min_age != -1 && (commit->date > min_age)) + return CONTINUE; + if (max_age != -1 && (commit->date < max_age)) { + stop_traversal=1; + return merge_order?CONTINUE:STOP; + } + if (max_count != -1 && !max_count--) + return STOP; + return DO; +} + +static int process_commit(struct commit * commit) +{ + int action=filter_commit(commit); + + if (action == STOP) { + return STOP; + } + + if (action == CONTINUE) { + return CONTINUE; + } + + show_commit(commit); + + return CONTINUE; +} + +static struct object_list **add_object(struct object *obj, struct object_list **p, const char *name) +{ + struct object_list *entry = xmalloc(sizeof(*entry)); + entry->item = obj; + entry->next = *p; + entry->name = name; + *p = entry; + return &entry->next; +} + +static struct object_list **process_blob(struct blob *blob, struct object_list **p, const char *name) +{ + struct object *obj = &blob->object; + + if (!blob_objects) + return p; + if (obj->flags & (UNINTERESTING | SEEN)) + return p; + obj->flags |= SEEN; + return add_object(obj, p, name); +} + +static struct object_list **process_tree(struct tree *tree, struct object_list **p, const char *name) +{ + struct object *obj = &tree->object; + struct tree_entry_list *entry; + + if (!tree_objects) + return p; + if (obj->flags & (UNINTERESTING | SEEN)) + return p; + if (parse_tree(tree) < 0) + die("bad tree object %s", sha1_to_hex(obj->sha1)); + obj->flags |= SEEN; + p = add_object(obj, p, name); + for (entry = tree->entries ; entry ; entry = entry->next) { + if (entry->directory) + p = process_tree(entry->item.tree, p, entry->name); + else + p = process_blob(entry->item.blob, p, entry->name); + } + return p; +} + +static struct object_list *pending_objects = NULL; + +static void show_commit_list(struct commit_list *list) +{ + struct object_list *objects = NULL, **p = &objects, *pending; + while (list) { + struct commit *commit = pop_most_recent_commit(&list, SEEN); + + p = process_tree(commit->tree, p, ""); + if (process_commit(commit) == STOP) + break; + } + for (pending = pending_objects; pending; pending = pending->next) { + struct object *obj = pending->item; + const char *name = pending->name; + if (obj->flags & (UNINTERESTING | SEEN)) + continue; + if (obj->type == tag_type) { + obj->flags |= SEEN; + p = add_object(obj, p, name); + continue; + } + if (obj->type == tree_type) { + p = process_tree((struct tree *)obj, p, name); + continue; + } + if (obj->type == blob_type) { + p = process_blob((struct blob *)obj, p, name); + continue; + } + die("unknown pending object %s (%s)", sha1_to_hex(obj->sha1), name); + } + while (objects) { + printf("%s %s\n", sha1_to_hex(objects->item->sha1), objects->name); + objects = objects->next; + } +} + +static void mark_blob_uninteresting(struct blob *blob) +{ + if (!blob_objects) + return; + if (blob->object.flags & UNINTERESTING) + return; + blob->object.flags |= UNINTERESTING; +} + +static void mark_tree_uninteresting(struct tree *tree) +{ + struct object *obj = &tree->object; + struct tree_entry_list *entry; + + if (!tree_objects) + return; + if (obj->flags & UNINTERESTING) + return; + obj->flags |= UNINTERESTING; + if (!has_sha1_file(obj->sha1)) + return; + if (parse_tree(tree) < 0) + die("bad tree %s", sha1_to_hex(obj->sha1)); + entry = tree->entries; + while (entry) { + if (entry->directory) + mark_tree_uninteresting(entry->item.tree); + else + mark_blob_uninteresting(entry->item.blob); + entry = entry->next; + } +} + +static void mark_parents_uninteresting(struct commit *commit) +{ + struct commit_list *parents = commit->parents; + + if (tree_objects) + mark_tree_uninteresting(commit->tree); + while (parents) { + struct commit *commit = parents->item; + commit->object.flags |= UNINTERESTING; + + /* + * A missing commit is ok iff its parent is marked + * uninteresting. + * + * We just mark such a thing parsed, so that when + * it is popped next time around, we won't be trying + * to parse it and get an error. + */ + if (!has_sha1_file(commit->object.sha1)) + commit->object.parsed = 1; + parents = parents->next; + } +} + +static int everybody_uninteresting(struct commit_list *list) +{ + while (list) { + struct commit *commit = list->item; + list = list->next; + if (commit->object.flags & UNINTERESTING) + continue; + return 0; + } + return 1; +} + +/* + * 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; + 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) { + nr++; + p = p->next; + } + closest = 0; + best = list; + + p = list; + while (p) { + int distance = count_distance(p); + clear_distance(list); + if (nr - distance < distance) + distance = nr - distance; + if (distance > closest) { + best = p; + closest = distance; + } + p = p->next; + } + if (best) + best->next = NULL; + return best; +} + +static struct commit_list *limit_list(struct commit_list *list) +{ + struct commit_list *newlist = NULL; + struct commit_list **p = &newlist; + while (list) { + struct commit *commit = pop_most_recent_commit(&list, SEEN); + struct object *obj = &commit->object; + + if (unpacked && has_sha1_pack(obj->sha1)) + obj->flags |= UNINTERESTING; + if (obj->flags & UNINTERESTING) { + mark_parents_uninteresting(commit); + if (everybody_uninteresting(list)) + break; + continue; + } + p = &commit_list_insert(commit, p)->next; + } + if (bisect_list) + newlist = find_bisection(newlist); + return newlist; +} + +static void add_pending_object(struct object *obj, const char *name) +{ + add_object(obj, &pending_objects, name); +} + +static struct commit *get_commit_reference(const char *name, unsigned int flags) +{ + unsigned char sha1[20]; + struct object *object; + + if (get_sha1(name, sha1)) + usage(rev_list_usage); + object = parse_object(sha1); + if (!object) + die("bad object %s", name); + + /* + * Tag object? Look what it points to.. + */ + while (object->type == tag_type) { + struct tag *tag = (struct tag *) object; + object->flags |= flags; + if (tag_objects && !(object->flags & UNINTERESTING)) + add_pending_object(object, tag->tag); + object = parse_object(tag->tagged->sha1); + } + + /* + * Commit object? Just return it, we'll do all the complex + * reachability crud. + */ + if (object->type == commit_type) { + struct commit *commit = (struct commit *)object; + object->flags |= flags; + if (parse_commit(commit) < 0) + die("unable to parse commit %s", name); + if (flags & UNINTERESTING) + mark_parents_uninteresting(commit); + return commit; + } + + /* + * Tree object? Either mark it uniniteresting, or add it + * to the list of objects to look at later.. + */ + if (object->type == tree_type) { + struct tree *tree = (struct tree *)object; + if (!tree_objects) + return NULL; + if (flags & UNINTERESTING) { + mark_tree_uninteresting(tree); + return NULL; + } + add_pending_object(object, ""); + return NULL; + } + + /* + * Blob object? You know the drill by now.. + */ + if (object->type == blob_type) { + struct blob *blob = (struct blob *)object; + if (!blob_objects) + return NULL; + if (flags & UNINTERESTING) { + mark_blob_uninteresting(blob); + return NULL; + } + add_pending_object(object, ""); + return NULL; + } + die("%s is unknown object", name); +} + +int main(int argc, char **argv) +{ + struct commit_list *list = NULL; + int i, limited = 0; + + for (i = 1 ; i < argc; i++) { + int flags; + char *arg = argv[i]; + struct commit *commit; + + if (!strncmp(arg, "--max-count=", 12)) { + max_count = atoi(arg + 12); + continue; + } + if (!strncmp(arg, "--max-age=", 10)) { + max_age = atoi(arg + 10); + continue; + } + if (!strncmp(arg, "--min-age=", 10)) { + min_age = atoi(arg + 10); + continue; + } + if (!strcmp(arg, "--header")) { + verbose_header = 1; + continue; + } + if (!strncmp(arg, "--pretty", 8)) { + commit_format = get_commit_format(arg+8); + verbose_header = 1; + hdr_termination = '\n'; + prefix = "commit "; + continue; + } + if (!strcmp(arg, "--parents")) { + show_parents = 1; + continue; + } + if (!strcmp(arg, "--bisect")) { + bisect_list = 1; + continue; + } + if (!strcmp(arg, "--objects")) { + tag_objects = 1; + tree_objects = 1; + blob_objects = 1; + continue; + } + if (!strcmp(arg, "--unpacked")) { + unpacked = 1; + limited = 1; + continue; + } + if (!strcmp(arg, "--merge-order")) { + merge_order = 1; + continue; + } + if (!strcmp(arg, "--show-breaks")) { + show_breaks = 1; + continue; + } + if (!strcmp(arg, "--topo-order")) { + topo_order = 1; + limited = 1; + continue; + } + + flags = 0; + if (*arg == '^') { + flags = UNINTERESTING; + arg++; + limited = 1; + } + if (show_breaks && !merge_order) + usage(rev_list_usage); + commit = get_commit_reference(arg, flags); + if (!commit) + continue; + if (commit->object.flags & SEEN) + continue; + commit->object.flags |= SEEN; + commit_list_insert(commit, &list); + } + + if (!merge_order) { + sort_by_date(&list); + if (limited) + list = limit_list(list); + if (topo_order) + sort_in_topological_order(&list); + show_commit_list(list); + } else { + if (sort_list_in_merge_order(list, &process_commit)) { + die("merge order sort failed\n"); + } + } + + return 0; +} diff --git a/rev-parse.c b/rev-parse.c new file mode 100644 index 0000000000..43af88b5a8 --- /dev/null +++ b/rev-parse.c @@ -0,0 +1,384 @@ +/* + * rev-parse.c + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "commit.h" +#include "refs.h" + +static char *def = NULL; +static int no_revs = 0; +static int single_rev = 0; +static int revs_only = 0; +static int do_rev_argument = 1; +static int output_revs = 0; +static int flags_only = 0; +static int no_flags = 0; + +#define NORMAL 0 +#define REVERSED 1 +static int show_type = NORMAL; + +static int get_extended_sha1(char *name, unsigned char *sha1); + +/* + * 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[] = { + "--max-count=", + "--max-age=", + "--min-age=", + "--merge-order", + NULL + }; + const char **p = rev_args; + + for (;;) { + const char *str = *p++; + int len; + if (!str) + return 0; + len = strlen(str); + if (!strncmp(arg, str, len)) + return 1; + } +} + +static void show_rev(int type, const unsigned char *sha1) +{ + if (no_revs) + return; + output_revs++; + printf("%s%s\n", type == show_type ? "" : "^", sha1_to_hex(sha1)); +} + +static void show_rev_arg(char *rev) +{ + if (no_revs) + return; + puts(rev); +} + +static void show_norev(char *norev) +{ + if (flags_only) + return; + if (revs_only) + return; + puts(norev); +} + +static void show_arg(char *arg) +{ + if (no_flags) + return; + if (do_rev_argument && is_rev_argument(arg)) + show_rev_arg(arg); + else + show_norev(arg); +} + +static int get_parent(char *name, unsigned char *result, int idx) +{ + unsigned char sha1[20]; + int ret = get_extended_sha1(name, 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) { + memcpy(result, commit->object.sha1, 20); + return 0; + } + p = commit->parents; + while (p) { + if (!--idx) { + memcpy(result, p->item->object.sha1, 20); + return 0; + } + p = p->next; + } + return -1; +} + +static int find_short_object_filename(int len, const char *name, unsigned char *sha1) +{ + static char dirname[PATH_MAX]; + char hex[40]; + DIR *dir; + int found; + + snprintf(dirname, sizeof(dirname), "%s/%.2s", get_object_directory(), name); + dir = opendir(dirname); + sprintf(hex, "%.2s", name); + found = 0; + if (dir) { + struct dirent *de; + while ((de = readdir(dir)) != NULL) { + if (strlen(de->d_name) != 38) + continue; + if (memcmp(de->d_name, name + 2, len-2)) + continue; + memcpy(hex + 2, de->d_name, 38); + if (++found > 1) + break; + } + closedir(dir); + } + if (found == 1) + return get_sha1_hex(hex, sha1) == 0; + return 0; +} + +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; + + prepare_packed_git(); + for (p = packed_git; p; 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 = memcmp(match, now, 20); + 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)) { + memcpy(sha1, now, 20); + return 1; + } + } + } + } + return 0; +} + +static int get_short_sha1(char *name, unsigned char *sha1) +{ + int i; + char canonical[40]; + unsigned char res[20]; + + memset(res, 0, 20); + memset(canonical, 'x', 40); + for (i = 0;;i++) { + unsigned char c = name[i]; + unsigned char val; + if (!c || i > 40) + break; + 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; + } + if (i < 4) + return -1; + if (find_short_object_filename(i, canonical, sha1)) + return 0; + if (find_short_packed_object(i, res, sha1)) + return 0; + return -1; +} + +/* + * This is like "get_sha1()", except it allows "sha1 expressions", + * notably "xyz^" for "parent of xyz" + */ +static int get_extended_sha1(char *name, unsigned char *sha1) +{ + int parent, ret; + int len = strlen(name); + + parent = 1; + if (len > 2 && name[len-1] >= '0' && name[len-1] <= '9') { + parent = name[len-1] - '0'; + len--; + } + if (len > 1 && name[len-1] == '^') { + name[len-1] = 0; + ret = get_parent(name, sha1, parent); + name[len-1] = '^'; + if (!ret) + return 0; + } + ret = get_sha1(name, sha1); + if (!ret) + return 0; + return get_short_sha1(name, sha1); +} + +static void show_default(void) +{ + char *s = def; + + if (s) { + unsigned char sha1[20]; + + def = NULL; + if (!get_extended_sha1(s, sha1)) { + show_rev(NORMAL, sha1); + return; + } + show_arg(s); + } +} + +static int show_reference(const char *refname, const unsigned char *sha1) +{ + show_rev(NORMAL, sha1); + return 0; +} + +int main(int argc, char **argv) +{ + int i, as_is = 0; + unsigned char sha1[20]; + + for (i = 1; i < argc; i++) { + char *arg = argv[i]; + char *dotdot; + + if (as_is) { + show_norev(arg); + continue; + } + if (*arg == '-') { + if (!strcmp(arg, "--")) { + show_default(); + if (revs_only) + break; + as_is = 1; + } + if (!strcmp(arg, "--default")) { + def = argv[i+1]; + i++; + continue; + } + if (!strcmp(arg, "--revs-only")) { + revs_only = 1; + continue; + } + if (!strcmp(arg, "--no-revs")) { + no_revs = 1; + continue; + } + if (!strcmp(arg, "--flags")) { + flags_only = 1; + continue; + } + if (!strcmp(arg, "--no-flags")) { + no_flags = 1; + continue; + } + if (!strcmp(arg, "--verify")) { + revs_only = 1; + do_rev_argument = 0; + single_rev = 1; + continue; + } + if (!strcmp(arg, "--not")) { + show_type ^= REVERSED; + continue; + } + if (!strcmp(arg, "--all")) { + for_each_ref(show_reference); + continue; + } + show_arg(arg); + continue; + } + dotdot = strstr(arg, ".."); + if (dotdot) { + unsigned char end[20]; + char *n = dotdot+2; + *dotdot = 0; + if (!get_extended_sha1(arg, sha1)) { + if (!*n) + n = "HEAD"; + if (!get_extended_sha1(n, end)) { + if (no_revs) + continue; + def = NULL; + show_rev(NORMAL, end); + show_rev(REVERSED, sha1); + continue; + } + } + *dotdot = '.'; + } + if (!get_extended_sha1(arg, sha1)) { + if (no_revs) + continue; + def = NULL; + show_rev(NORMAL, sha1); + continue; + } + if (*arg == '^' && !get_extended_sha1(arg+1, sha1)) { + if (no_revs) + continue; + def = NULL; + show_rev(REVERSED, sha1); + continue; + } + show_default(); + show_norev(arg); + } + show_default(); + if (single_rev && output_revs != 1) { + fprintf(stderr, "Needed a single revision\n"); + exit(1); + } + return 0; +} diff --git a/rev-tree.c b/rev-tree.c new file mode 100644 index 0000000000..7f92819850 --- /dev/null +++ b/rev-tree.c @@ -0,0 +1,140 @@ +#include "cache.h" +#include "commit.h" + +/* + * revision.h leaves the low 16 bits of the "flags" field of the + * revision data structure unused. We use it for a "reachable from + * this commit <N>" bitmask. + */ +#define MAX_COMMITS 16 +#define REACHABLE (1U << 16) + +#define cmit_flags(cmit) ((cmit)->object.flags & ~REACHABLE) + +static int show_edges = 0; +static int basemask = 0; + +static void read_cache_file(const char *path) +{ + die("no revtree cache file yet"); +} + +/* + * Some revisions are less interesting than others. + * + * For example, if we use a cache-file, that one may contain + * revisions that were never used. They are never interesting. + * + * And sometimes we're only interested in "edge" commits, ie + * places where the marking changes between parent and child. + */ +static int interesting(struct commit *rev) +{ + unsigned mask = cmit_flags(rev); + + if (!mask) + return 0; + if (show_edges) { + struct commit_list *p = rev->parents; + while (p) { + if (mask != cmit_flags(p->item)) + return 1; + p = p->next; + } + return 0; + } + if (mask & basemask) + return 0; + + return 1; +} + +/* + * Usage: git-rev-tree [--edges] [--cache <cache-file>] <commit-id> [<commit-id2>] + * + * The cache-file can be quite important for big trees. This is an + * expensive operation if you have to walk the whole chain of + * parents in a tree with a long revision history. + */ +int main(int argc, char **argv) +{ + int i; + int nr = 0; + unsigned char sha1[MAX_COMMITS][20]; + struct commit_list *list = NULL; + + /* + * First - pick up all the revisions we can (both from + * caches and from commit file chains). + */ + for (i = 1; i < argc ; i++) { + char *arg = argv[i]; + struct commit *commit; + + if (!strcmp(arg, "--cache")) { + read_cache_file(argv[++i]); + continue; + } + + if (!strcmp(arg, "--edges")) { + show_edges = 1; + continue; + } + + if (arg[0] == '^') { + arg++; + basemask |= 1<<nr; + } + if (nr >= MAX_COMMITS || get_sha1(arg, sha1[nr])) + usage("git-rev-tree [--edges] [--cache <cache-file>] <commit-id> [<commit-id>]"); + + commit = lookup_commit_reference(sha1[nr]); + if (!commit || parse_commit(commit) < 0) + die("bad commit object"); + commit_list_insert(commit, &list); + nr++; + } + + /* + * Parse all the commits in date order. + * + * We really should stop once we know enough, but that's a + * decision that isn't trivial to make. + */ + while (list) + pop_most_recent_commit(&list, REACHABLE); + + /* + * Now we have the maximal tree. Walk the different sha files back to the root. + */ + for (i = 0; i < nr; i++) + mark_reachable(&lookup_commit_reference(sha1[i])->object, 1 << i); + + /* + * Now print out the results.. + */ + for (i = 0; i < nr_objs; i++) { + struct object *obj = objs[i]; + struct commit *commit; + struct commit_list *p; + + if (obj->type != commit_type) + continue; + + commit = (struct commit *) obj; + + if (!interesting(commit)) + continue; + + printf("%lu %s:%d", commit->date, sha1_to_hex(obj->sha1), + cmit_flags(commit)); + p = commit->parents; + while (p) { + printf(" %s:%d", sha1_to_hex(p->item->object.sha1), + cmit_flags(p->item)); + p = p->next; + } + printf("\n"); + } + return 0; +} @@ -0,0 +1,68 @@ +#include "rsh.h" + +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> + +#include "cache.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 i; + + 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); + } + /* ssh <host> 'cd <path>; stdio-pull <arg...> <commit-id>' */ + snprintf(command, COMMAND_SIZE, + "%s='%s' %s", + GIT_DIR_ENVIRONMENT, path, remote_prog); + *path = '\0'; + posn = command + strlen(command); + for (i = 0; i < rmt_argc; i++) { + *(posn++) = ' '; + strncpy(posn, rmt_argv[i], COMMAND_SIZE - (posn - command)); + posn += strlen(rmt_argv[i]); + if (posn - command + 4 >= COMMAND_SIZE) { + return error("Command line too long"); + } + } + strcpy(posn, " -"); + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv)) { + return error("Couldn't create socket"); + } + if (!fork()) { + close(sv[1]); + dup2(sv[0], 0); + dup2(sv[0], 1); + execlp("ssh", "ssh", 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/send-pack.c b/send-pack.c new file mode 100644 index 0000000000..2994df401a --- /dev/null +++ b/send-pack.c @@ -0,0 +1,275 @@ +#include "cache.h" +#include "refs.h" +#include "pkt-line.h" + +static const char send_pack_usage[] = +"git-send-pack [--exec=git-receive-pack] [host:]directory [heads]*"; +static const char *exec = "git-receive-pack"; +static int send_all = 0; + +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 exec_pack_objects(void) +{ + static char *args[] = { + "git-pack-objects", + "--stdout", + NULL + }; + execvp("git-pack-objects", args); + die("git-pack-objects exec failed (%s)", strerror(errno)); +} + +static void exec_rev_list(struct ref *refs) +{ + static char *args[1000]; + int i = 0; + + args[i++] = "git-rev-list"; /* 0 */ + args[i++] = "--objects"; /* 1 */ + while (refs) { + char *buf = malloc(100); + if (i > 900) + die("git-rev-list environment overflow"); + if (!is_zero_sha1(refs->old_sha1)) { + args[i++] = buf; + snprintf(buf, 50, "^%s", sha1_to_hex(refs->old_sha1)); + buf += 50; + } + if (!is_zero_sha1(refs->new_sha1)) { + args[i++] = buf; + snprintf(buf, 50, "%s", sha1_to_hex(refs->new_sha1)); + } + refs = refs->next; + } + args[i] = NULL; + execvp("git-rev-list", args); + die("git-rev-list exec failed (%s)", strerror(errno)); +} + +static void rev_list(int fd, struct ref *refs) +{ + int pipe_fd[2]; + pid_t pack_objects_pid; + + if (pipe(pipe_fd) < 0) + die("rev-list setup: pipe failed"); + pack_objects_pid = fork(); + if (!pack_objects_pid) { + dup2(pipe_fd[0], 0); + dup2(fd, 1); + close(pipe_fd[0]); + close(pipe_fd[1]); + close(fd); + exec_pack_objects(); + die("pack-objects setup failed"); + } + if (pack_objects_pid < 0) + die("pack-objects fork failed"); + dup2(pipe_fd[1], 1); + close(pipe_fd[0]); + close(pipe_fd[1]); + close(fd); + exec_rev_list(refs); +} + +static int pack_objects(int fd, struct ref *refs) +{ + pid_t rev_list_pid; + + rev_list_pid = fork(); + if (!rev_list_pid) { + rev_list(fd, refs); + die("rev-list setup failed"); + } + if (rev_list_pid < 0) + die("rev-list fork failed"); + /* + * We don't wait for the rev-list pipeline in the parent: + * we end up waiting for the other end instead + */ + return 0; +} + +static int read_ref(const char *ref, unsigned char *sha1) +{ + int fd, ret; + char buffer[60]; + + fd = open(git_path("%s", ref), O_RDONLY); + if (fd < 0) + return -1; + ret = -1; + if (read(fd, buffer, sizeof(buffer)) >= 40) + ret = get_sha1_hex(buffer, sha1); + close(fd); + return ret; +} + +static int ref_newer(const unsigned char *new_sha1, const unsigned char *old_sha1) +{ + if (!has_sha1_file(old_sha1)) + return 0; + /* + * FIXME! It is not correct to say that the new one is newer + * just because we don't have the old one! + * + * We should really see if we can reach the old_sha1 commit + * from the new_sha1 one. + */ + return 1; +} + +static int local_ref_nr_match; +static char **local_ref_match; +static struct ref *local_ref_list; +static struct ref **local_last_ref; + +static int try_to_match(const char *refname, const unsigned char *sha1) +{ + struct ref *ref; + int len; + + if (!path_match(refname, local_ref_nr_match, local_ref_match)) { + if (!send_all) + return 0; + + /* If we have it listed already, skip it */ + for (ref = local_ref_list ; ref ; ref = ref->next) { + if (!strcmp(ref->name, refname)) + return 0; + } + } + + len = strlen(refname)+1; + ref = xmalloc(sizeof(*ref) + len); + memset(ref->old_sha1, 0, 20); + memcpy(ref->new_sha1, sha1, 20); + memcpy(ref->name, refname, len); + ref->next = NULL; + *local_last_ref = ref; + local_last_ref = &ref->next; + return 0; +} + +static int send_pack(int in, int out, int nr_match, char **match) +{ + struct ref *ref_list, **last_ref; + struct ref *ref; + int new_refs; + + /* First we get all heads, whether matching or not.. */ + last_ref = get_remote_heads(in, &ref_list, 0, NULL); + + /* + * Go through the refs, see if we want to update + * any of them.. + */ + for (ref = ref_list; ref; ref = ref->next) { + unsigned char new_sha1[20]; + char *name = ref->name; + + if (nr_match && !path_match(name, nr_match, match)) + continue; + + if (read_ref(name, new_sha1) < 0) + continue; + + if (!memcmp(ref->old_sha1, new_sha1, 20)) { + fprintf(stderr, "'%s' unchanged\n", name); + continue; + } + + if (!ref_newer(new_sha1, ref->old_sha1)) { + error("remote '%s' points to object I don't have", name); + continue; + } + + /* Ok, mark it for update */ + memcpy(ref->new_sha1, new_sha1, 20); + } + + /* + * See if we have any refs that the other end didn't have + */ + if (nr_match) { + local_ref_nr_match = nr_match; + local_ref_match = match; + local_ref_list = ref_list; + local_last_ref = last_ref; + for_each_ref(try_to_match); + } + + /* + * Finally, tell the other end! + */ + new_refs = 0; + for (ref = ref_list; ref; ref = ref->next) { + char old_hex[60], *new_hex; + if (is_zero_sha1(ref->new_sha1)) + continue; + new_refs++; + strcpy(old_hex, sha1_to_hex(ref->old_sha1)); + new_hex = sha1_to_hex(ref->new_sha1); + packet_write(out, "%s %s %s", old_hex, new_hex, ref->name); + fprintf(stderr, "'%s': updating from %s to %s\n", ref->name, old_hex, new_hex); + } + + packet_flush(out); + if (new_refs) + pack_objects(out, ref_list); + close(out); + return 0; +} + +int main(int argc, char **argv) +{ + int i, nr_heads = 0; + char *dest = NULL; + char **heads = NULL; + int fd[2], ret; + pid_t pid; + + argv++; + for (i = 1; i < argc; i++, argv++) { + char *arg = *argv; + + if (*arg == '-') { + if (!strncmp(arg, "--exec=", 7)) { + exec = arg + 7; + continue; + } + if (!strcmp(arg, "--all")) { + send_all = 1; + continue; + } + usage(send_pack_usage); + } + if (!dest) { + dest = arg; + continue; + } + heads = argv; + nr_heads = argc - i; + break; + } + if (!dest) + usage(send_pack_usage); + pid = git_connect(fd, dest, exec); + if (pid < 0) + return 1; + ret = send_pack(fd[0], fd[1], nr_heads, heads); + close(fd[0]); + close(fd[1]); + finish_connect(pid); + return ret; +} diff --git a/sha1_file.c b/sha1_file.c new file mode 100644 index 0000000000..5ec5598d7d --- /dev/null +++ b/sha1_file.c @@ -0,0 +1,1380 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + * + * This handles basic git sha1 object files - packing, unpacking, + * creation etc. + */ +#include <sys/types.h> +#include <dirent.h> +#include "cache.h" +#include "delta.h" +#include "pack.h" + +#ifndef O_NOATIME +#if defined(__linux__) && (defined(__i386__) || defined(__PPC__)) +#define O_NOATIME 01000000 +#else +#define O_NOATIME 0 +#endif +#endif + +static unsigned int sha1_file_open_flag = O_NOATIME; + +static unsigned hexval(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + return ~0; +} + +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; +} + +static int get_sha1_file(const char *path, unsigned char *result) +{ + char buffer[60]; + int fd = open(path, O_RDONLY); + int len; + + if (fd < 0) + return -1; + len = read(fd, buffer, sizeof(buffer)); + close(fd); + if (len < 40) + return -1; + return get_sha1_hex(buffer, result); +} + +static char *git_dir, *git_object_dir, *git_index_file, *git_refs_dir; +static void setup_git_env(void) +{ + git_dir = gitenv(GIT_DIR_ENVIRONMENT); + if (!git_dir) + git_dir = DEFAULT_GIT_DIR_ENVIRONMENT; + git_object_dir = gitenv(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 = gitenv(INDEX_ENVIRONMENT); + if (!git_index_file) { + git_index_file = xmalloc(strlen(git_dir) + 7); + sprintf(git_index_file, "%s/index", 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; +} + +int safe_create_leading_directories(char *path) +{ + char *pos = path; + + while (pos) { + pos = strchr(pos, '/'); + if (!pos) + break; + *pos = 0; + if (mkdir(path, 0777) < 0) + if (errno != EEXIST) { + *pos = '/'; + return -1; + } + *pos++ = '/'; + } + return 0; +} + +int get_sha1(const char *str, unsigned char *sha1) +{ + static const char *prefix[] = { + "", + "refs", + "refs/tags", + "refs/heads", + "refs/snap", + NULL + }; + const char **p; + + if (!get_sha1_hex(str, sha1)) + return 0; + + for (p = prefix; *p; p++) { + char * pathname = git_path("%s/%s", *p, str); + if (!get_sha1_file(pathname, sha1)) + return 0; + } + + return -1; +} + +char * sha1_to_hex(const unsigned char *sha1) +{ + static char buffer[50]; + static const char hex[] = "0123456789abcdef"; + char *buf = buffer; + int i; + + for (i = 0; i < 20; i++) { + unsigned int val = *sha1++; + *buf++ = hex[val >> 4]; + *buf++ = hex[val & 0xf]; + } + 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 "strdup()" 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; +} + +struct alternate_object_database *alt_odb; + +/* + * Prepare alternate object database registry. + * alt_odb points at an array of struct alternate_object_database. + * This array is terminated with an element that has both its base + * and name set to NULL. alt_odb[n] comes from n'th non-empty + * element from colon separated ALTERNATE_DB_ENVIRONMENT environment + * variable, and 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. + * This function allocates the alt_odb array and all the strings + * pointed by base fields of the array elements with one xmalloc(); + * the string pool immediately follows the array. + */ +void prepare_alt_odb(void) +{ + int pass, totlen, i; + const char *cp, *last; + char *op = NULL; + const char *alt = gitenv(ALTERNATE_DB_ENVIRONMENT) ? : ""; + + if (alt_odb) + return; + /* The first pass counts how large an area to allocate to + * hold the entire alt_odb structure, including array of + * structs and path buffers for them. The second pass fills + * the structure and prepares the path buffers for use by + * fill_sha1_path(). + */ + for (totlen = pass = 0; pass < 2; pass++) { + last = alt; + i = 0; + do { + cp = strchr(last, ':') ? : last + strlen(last); + if (last != cp) { + /* 43 = 40-byte + 2 '/' + terminating NUL */ + int pfxlen = cp - last; + int entlen = pfxlen + 43; + if (pass == 0) + totlen += entlen; + else { + alt_odb[i].base = op; + alt_odb[i].name = op + pfxlen + 1; + memcpy(op, last, pfxlen); + op[pfxlen] = op[pfxlen + 3] = '/'; + op[entlen-1] = 0; + op += entlen; + } + i++; + } + while (*cp && *cp == ':') + cp++; + last = cp; + } while (*cp); + if (pass) + break; + alt_odb = xmalloc(sizeof(*alt_odb) * (i + 1) + totlen); + alt_odb[i].base = alt_odb[i].name = NULL; + op = (char*)(&alt_odb[i+1]); + } +} + +static char *find_sha1_file(const unsigned char *sha1, struct stat *st) +{ + int i; + char *name = sha1_file_name(sha1); + + if (!stat(name, st)) + return name; + prepare_alt_odb(); + for (i = 0; (name = alt_odb[i].name) != NULL; i++) { + fill_sha1_path(name, sha1); + if (!stat(alt_odb[i].base, st)) + return alt_odb[i].base; + } + return NULL; +} + +#define PACK_MAX_SZ (1<<26) +static int pack_used_ctr; +static unsigned long pack_mapped; +struct packed_git *packed_git; + +static int check_packed_git_idx(const char *path, unsigned long *idx_size_, + void **idx_map_) +{ + void *idx_map; + unsigned int *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 = mmap(NULL, idx_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (idx_map == MAP_FAILED) + return -1; + + 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 too small"); + nr = 0; + for (i = 0; i < 256; i++) { + unsigned int n = ntohl(index[i]); + if (n < nr) + return error("non-monotonic index"); + 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"); + + return 0; +} + +static int unuse_one_packed_git(void) +{ + struct packed_git *p, *lru = NULL; + + for (p = packed_git; p; p = p->next) { + if (p->pack_use_cnt || !p->pack_base) + continue; + if (!lru || p->pack_last_used < lru->pack_last_used) + lru = p; + } + if (!lru) + return 0; + munmap(lru->pack_base, lru->pack_size); + lru->pack_base = NULL; + return 1; +} + +void unuse_packed_git(struct packed_git *p) +{ + p->pack_use_cnt--; +} + +int use_packed_git(struct packed_git *p) +{ + if (!p->pack_base) { + int fd; + struct stat st; + void *map; + + pack_mapped += p->pack_size; + while (PACK_MAX_SZ < pack_mapped && unuse_one_packed_git()) + ; /* nothing */ + fd = open(p->pack_name, O_RDONLY); + if (fd < 0) + die("packfile %s cannot be opened", p->pack_name); + if (fstat(fd, &st)) { + close(fd); + die("packfile %s cannot be opened", p->pack_name); + } + if (st.st_size != p->pack_size) + die("packfile %s size mismatch.", p->pack_name); + map = mmap(NULL, p->pack_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (map == MAP_FAILED) + die("packfile %s cannot be mapped.", p->pack_name); + p->pack_base = map; + + /* Check if the pack file matches with the index file. + * this is cheap. + */ + if (memcmp((char*)(p->index_base) + p->index_size - 40, + p->pack_base + p->pack_size - 20, 20)) + die("packfile %s does not match index.", p->pack_name); + } + p->pack_last_used = pack_used_ctr++; + p->pack_use_cnt++; + return 0; +} + +struct packed_git *add_packed_git(char *path, int path_len) +{ + struct stat st; + struct packed_git *p; + unsigned long idx_size; + void *idx_map; + + 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->pack_base = NULL; + p->pack_last_used = 0; + p->pack_use_cnt = 0; + return p; +} + +static void prepare_packed_git_one(char *objdir) +{ + char path[PATH_MAX]; + int len; + DIR *dir; + struct dirent *de; + + sprintf(path, "%s/pack", objdir); + len = strlen(path); + dir = opendir(path); + if (!dir) + return; + path[len++] = '/'; + while ((de = readdir(dir)) != NULL) { + int namelen = strlen(de->d_name); + struct packed_git *p; + + if (strcmp(de->d_name + namelen - 4, ".idx")) + continue; + + /* we have .idx. Is it a file we can map? */ + strcpy(path + len, de->d_name); + p = add_packed_git(path, len + namelen); + if (!p) + continue; + p->next = packed_git; + packed_git = p; + } + closedir(dir); +} + +void prepare_packed_git(void) +{ + int i; + static int run_once = 0; + + if (run_once++) + return; + + prepare_packed_git_one(get_object_directory()); + prepare_alt_odb(); + for (i = 0; alt_odb[i].base != NULL; i++) { + alt_odb[i].name[0] = 0; + prepare_packed_git_one(alt_odb[i].base); + } +} + +int check_sha1_signature(const unsigned char *sha1, void *map, unsigned long size, const char *type) +{ + char header[100]; + unsigned char real_sha1[20]; + SHA_CTX c; + + SHA1_Init(&c); + SHA1_Update(&c, header, 1+sprintf(header, "%s %lu", type, size)); + SHA1_Update(&c, map, size); + SHA1_Final(real_sha1, &c); + return memcmp(sha1, real_sha1, 20) ? -1 : 0; +} + +static void *map_sha1_file_internal(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 = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (-1 == (int)(long)map) + return NULL; + *size = st.st_size; + return map; +} + +int unpack_sha1_header(z_stream *stream, void *map, unsigned long mapsize, void *buffer, unsigned long size) +{ + /* Get the data stream */ + memset(stream, 0, sizeof(*stream)); + stream->next_in = map; + stream->avail_in = mapsize; + stream->next_out = buffer; + stream->avail_out = size; + + inflateInit(stream); + return inflate(stream, 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); + + memcpy(buf, buffer + bytes, stream->total_out - bytes); + bytes = stream->total_out - bytes; + 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. + */ +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); +} + +/* forward declaration for a mutually recursive function */ +static int packed_object_info(struct pack_entry *entry, + char *type, unsigned long *sizep); + +static int packed_delta_info(unsigned char *base_sha1, + unsigned long delta_size, + unsigned long left, + char *type, + unsigned long *sizep, + struct packed_git *p) +{ + struct pack_entry base_ent; + + if (left < 20) + die("truncated pack file"); + + /* The base entry _must_ be in the same pack */ + if (!find_pack_entry_one(base_sha1, &base_ent, p)) + die("failed to find delta-pack base object %s", + sha1_to_hex(base_sha1)); + + /* 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(&base_ent, type, NULL)) + die("cannot get info for delta-pack base"); + + if (sizep) { + const unsigned char *data; + unsigned char delta_head[64]; + unsigned long result_size; + z_stream stream; + int st; + + memset(&stream, 0, sizeof(stream)); + + data = stream.next_in = base_sha1 + 20; + stream.avail_in = left - 20; + stream.next_out = delta_head; + stream.avail_out = sizeof(delta_head); + + inflateInit(&stream); + st = inflate(&stream, Z_FINISH); + 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; + get_delta_hdr_size(&data); /* ignore base size */ + + /* Read the result size */ + result_size = get_delta_hdr_size(&data); + *sizep = result_size; + } + return 0; +} + +static unsigned long unpack_object_header(struct packed_git *p, unsigned long offset, + enum object_type *type, unsigned long *sizep) +{ + unsigned shift; + unsigned char *pack, c; + unsigned long size; + + if (offset >= p->pack_size) + die("object offset outside of pack file"); + + pack = p->pack_base + offset; + c = *pack++; + offset++; + *type = (c >> 4) & 7; + size = c & 15; + shift = 4; + while (c & 0x80) { + if (offset >= p->pack_size) + die("object offset outside of pack file"); + c = *pack++; + offset++; + size += (c & 0x7f) << shift; + shift += 7; + } + *sizep = size; + return offset; +} + +void packed_object_info_detail(struct pack_entry *e, + char *type, + unsigned long *size, + unsigned long *store_size, + int *delta_chain_length, + unsigned char *base_sha1) +{ + struct packed_git *p = e->p; + unsigned long offset, left; + unsigned char *pack; + enum object_type kind; + + offset = unpack_object_header(p, e->offset, &kind, size); + pack = p->pack_base + offset; + left = p->pack_size - offset; + if (kind != OBJ_DELTA) + *delta_chain_length = 0; + else { + int chain_length = 0; + memcpy(base_sha1, pack, 20); + do { + struct pack_entry base_ent; + unsigned long junk; + + find_pack_entry_one(pack, &base_ent, p); + offset = unpack_object_header(p, base_ent.offset, + &kind, &junk); + pack = p->pack_base + offset; + chain_length++; + } while (kind == OBJ_DELTA); + *delta_chain_length = chain_length; + } + switch (kind) { + case OBJ_COMMIT: + strcpy(type, "commit"); + break; + case OBJ_TREE: + strcpy(type, "tree"); + break; + case OBJ_BLOB: + strcpy(type, "blob"); + break; + case OBJ_TAG: + strcpy(type, "tag"); + break; + default: + die("corrupted pack file"); + } + *store_size = 0; /* notyet */ +} + +static int packed_object_info(struct pack_entry *entry, + char *type, unsigned long *sizep) +{ + struct packed_git *p = entry->p; + unsigned long offset, size, left; + unsigned char *pack; + enum object_type kind; + int retval; + + if (use_packed_git(p)) + die("cannot map packed file"); + + offset = unpack_object_header(p, entry->offset, &kind, &size); + pack = p->pack_base + offset; + left = p->pack_size - offset; + + switch (kind) { + case OBJ_DELTA: + retval = packed_delta_info(pack, size, left, type, sizep, p); + unuse_packed_git(p); + return retval; + case OBJ_COMMIT: + strcpy(type, "commit"); + break; + case OBJ_TREE: + strcpy(type, "tree"); + break; + case OBJ_BLOB: + strcpy(type, "blob"); + break; + case OBJ_TAG: + strcpy(type, "tag"); + break; + default: + die("corrupted pack file"); + } + if (sizep) + *sizep = size; + unuse_packed_git(p); + return 0; +} + +/* forward declaration for a mutually recursive function */ +static void *unpack_entry(struct pack_entry *, char *, unsigned long *); + +static void *unpack_delta_entry(unsigned char *base_sha1, + unsigned long delta_size, + unsigned long left, + char *type, + unsigned long *sizep, + struct packed_git *p) +{ + struct pack_entry base_ent; + void *data, *delta_data, *result, *base; + unsigned long data_size, result_size, base_size; + z_stream stream; + int st; + + if (left < 20) + die("truncated pack file"); + data = base_sha1 + 20; + data_size = left - 20; + delta_data = xmalloc(delta_size); + + memset(&stream, 0, sizeof(stream)); + + stream.next_in = data; + stream.avail_in = data_size; + stream.next_out = delta_data; + stream.avail_out = delta_size; + + inflateInit(&stream); + st = inflate(&stream, Z_FINISH); + inflateEnd(&stream); + if ((st != Z_STREAM_END) || stream.total_out != delta_size) + die("delta data unpack failed"); + + /* The base entry _must_ be in the same pack */ + if (!find_pack_entry_one(base_sha1, &base_ent, p)) + die("failed to find delta-pack base object %s", + sha1_to_hex(base_sha1)); + base = unpack_entry_gently(&base_ent, type, &base_size); + if (!base) + die("failed to read delta-pack base object %s", + sha1_to_hex(base_sha1)); + 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; +} + +static void *unpack_non_delta_entry(unsigned char *data, + unsigned long size, + unsigned long left) +{ + int st; + z_stream stream; + unsigned char *buffer; + + buffer = xmalloc(size + 1); + buffer[size] = 0; + memset(&stream, 0, sizeof(stream)); + stream.next_in = data; + stream.avail_in = left; + stream.next_out = buffer; + stream.avail_out = size; + + inflateInit(&stream); + st = inflate(&stream, Z_FINISH); + inflateEnd(&stream); + if ((st != Z_STREAM_END) || stream.total_out != size) { + free(buffer); + return NULL; + } + + return buffer; +} + +static void *unpack_entry(struct pack_entry *entry, + char *type, unsigned long *sizep) +{ + struct packed_git *p = entry->p; + void *retval; + + if (use_packed_git(p)) + die("cannot map packed file"); + retval = unpack_entry_gently(entry, type, sizep); + unuse_packed_git(p); + if (!retval) + die("corrupted pack file"); + return retval; +} + +/* The caller is responsible for use_packed_git()/unuse_packed_git() pair */ +void *unpack_entry_gently(struct pack_entry *entry, + char *type, unsigned long *sizep) +{ + struct packed_git *p = entry->p; + unsigned long offset, size, left; + unsigned char *pack; + enum object_type kind; + void *retval; + + offset = unpack_object_header(p, entry->offset, &kind, &size); + pack = p->pack_base + offset; + left = p->pack_size - offset; + switch (kind) { + case OBJ_DELTA: + retval = unpack_delta_entry(pack, size, left, type, sizep, p); + return retval; + case OBJ_COMMIT: + strcpy(type, "commit"); + break; + case OBJ_TREE: + strcpy(type, "tree"); + break; + case OBJ_BLOB: + strcpy(type, "blob"); + break; + case OBJ_TAG: + strcpy(type, "tag"); + break; + default: + return NULL; + } + *sizep = size; + retval = unpack_non_delta_entry(pack, size, left); + 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; + memcpy(sha1, (index + 24 * n + 4), 20); + return 0; +} + +int find_pack_entry_one(const unsigned char *sha1, + struct pack_entry *e, struct packed_git *p) +{ + unsigned int *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 = memcmp(index + 24 * mi + 4, sha1, 20); + if (!cmp) { + e->offset = ntohl(*((int*)(index + 24 * mi))); + memcpy(e->sha1, sha1, 20); + e->p = p; + return 1; + } + if (cmp > 0) + hi = mi; + else + lo = mi+1; + } while (lo < hi); + return 0; +} + +static int find_pack_entry(const unsigned char *sha1, struct pack_entry *e) +{ + struct packed_git *p; + prepare_packed_git(); + + for (p = packed_git; p; p = p->next) { + if (find_pack_entry_one(sha1, e, p)) + return 1; + } + return 0; +} + +int sha1_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_internal(sha1, &mapsize); + if (!map) { + struct pack_entry e; + + if (!find_pack_entry(sha1, &e)) + return error("unable to find %s", sha1_to_hex(sha1)); + return packed_object_info(&e, type, sizep); + } + if (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; +} + +static void *read_packed_sha1(const unsigned char *sha1, char *type, unsigned long *size) +{ + struct pack_entry e; + + if (!find_pack_entry(sha1, &e)) { + error("cannot read sha1_file for %s", sha1_to_hex(sha1)); + return NULL; + } + return unpack_entry(&e, type, size); +} + +void * read_sha1_file(const unsigned char *sha1, char *type, unsigned long *size) +{ + unsigned long mapsize; + void *map, *buf; + struct pack_entry e; + + if (find_pack_entry(sha1, &e)) + return read_packed_sha1(sha1, type, size); + map = map_sha1_file_internal(sha1, &mapsize); + if (map) { + buf = unpack_sha1_file(map, mapsize, type, size); + munmap(map, mapsize); + return buf; + } + return NULL; +} + +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]; + + memcpy(actual_sha1, sha1, 20); + 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) + memcpy(actual_sha1_return, actual_sha1, 20); + return buffer; + } + /* Handle references */ + else if (!strcmp(type, "commit")) + ref_type = "tree "; + else if (!strcmp(type, "tag")) + ref_type = "object "; + else { + free(buffer); + return NULL; + } + ref_length = strlen(ref_type); + + if (memcmp(buffer, ref_type, ref_length) || + get_sha1_hex(buffer + ref_length, actual_sha1)) { + free(buffer); + return NULL; + } + /* Now we have the ID of the referred-to object in + * actual_sha1. Check again. */ + } +} + +char *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); + + return sha1_file_name(sha1); +} + +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, ret; + + /* Normally if we have it in the pack then we do not bother writing + * it out into .git/objects/??/?{38} file. + */ + filename = write_sha1_file_prepare(buf, len, type, sha1, hdr, &hdrlen); + if (returnsha1) + memcpy(returnsha1, sha1, 20); + 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) { + fprintf(stderr, "sha1 file %s: %s", filename, strerror(errno)); + return -1; + } + + snprintf(tmpfile, sizeof(tmpfile), "%s/obj_XXXXXX", get_object_directory()); + + fd = mkstemp(tmpfile); + if (fd < 0) { + fprintf(stderr, "unable to create temporary sha1 filename %s: %s", tmpfile, strerror(errno)); + return -1; + } + + /* Set it up */ + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, Z_BEST_COMPRESSION); + size = 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; + while (deflate(&stream, 0) == Z_OK) + /* nothing */; + + /* 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(fd, compressed, size) != size) + die("unable to write file"); + fchmod(fd, 0444); + close(fd); + free(compressed); + + ret = link(tmpfile, filename); + if (ret < 0) { + ret = errno; + + /* + * 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. + * + * When this succeeds, we just return 0. We have nothing + * left to unlink. + */ + if (ret == EXDEV && !rename(tmpfile, filename)) + return 0; + } + unlink(tmpfile); + if (ret) { + if (ret != EEXIST) { + fprintf(stderr, "unable to write sha1 filename %s: %s", filename, strerror(ret)); + return -1; + } + /* FIXME!!! Collision check here ? */ + } + + return 0; +} + +int write_sha1_to_fd(int fd, const unsigned char *sha1) +{ + ssize_t size; + unsigned long objsize; + int posn = 0; + void *buf = map_sha1_file_internal(sha1, &objsize); + z_stream stream; + if (!buf) { + unsigned char *unpacked; + unsigned long len; + char type[20]; + char hdr[50]; + int hdrlen; + // need to unpack and recompress it by itself + unpacked = read_packed_sha1(sha1, type, &len); + + hdrlen = sprintf(hdr, "%s %lu", type, len) + 1; + + /* Set it up */ + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, Z_BEST_COMPRESSION); + 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); + + objsize = stream.total_out; + } + + do { + size = write(fd, buf + posn, objsize - posn); + if (size <= 0) { + if (!size) { + fprintf(stderr, "write closed"); + } else { + perror("write "); + } + return -1; + } + posn += size; + } while (posn < objsize); + return 0; +} + +int write_sha1_from_fd(const unsigned char *sha1, int fd) +{ + char *filename = sha1_file_name(sha1); + + int local; + z_stream stream; + unsigned char real_sha1[20]; + unsigned char buf[4096]; + unsigned char discard[4096]; + int ret; + SHA_CTX c; + + local = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0666); + + if (local < 0) + return error("Couldn't open %s\n", filename); + + memset(&stream, 0, sizeof(stream)); + + inflateInit(&stream); + + SHA1_Init(&c); + + do { + ssize_t size; + size = read(fd, buf, 4096); + if (size <= 0) { + close(local); + unlink(filename); + if (!size) + return error("Connection closed?"); + perror("Reading from connection"); + return -1; + } + write(local, buf, size); + stream.avail_in = size; + stream.next_in = buf; + 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); + + } while (ret == Z_OK); + inflateEnd(&stream); + + close(local); + SHA1_Final(real_sha1, &c); + if (ret != Z_STREAM_END) { + unlink(filename); + return error("File %s corrupted", sha1_to_hex(sha1)); + } + if (memcmp(sha1, real_sha1, 20)) { + unlink(filename); + return error("File %s has bad hash\n", sha1_to_hex(sha1)); + } + + return 0; +} + +int has_sha1_pack(const unsigned char *sha1) +{ + struct pack_entry e; + return find_pack_entry(sha1, &e); +} + +int has_sha1_file(const unsigned char *sha1) +{ + struct stat st; + struct pack_entry e; + + if (find_pack_entry(sha1, &e)) + return 1; + return find_sha1_file(sha1, &st) ? 1 : 0; +} + +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; + unsigned char hdr[50]; + int hdrlen; + + buf = ""; + if (size) + buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if ((int)(long)buf == -1) + return -1; + + if (!type) + type = "blob"; + if (write_object) + ret = write_sha1_file(buf, size, type, sha1); + else { + write_sha1_file_prepare(buf, size, type, sha1, hdr, &hdrlen); + ret = 0; + } + if (size) + munmap(buf, size); + return ret; +} diff --git a/show-index.c b/show-index.c new file mode 100644 index 0000000000..c21d660b62 --- /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 idex"); + 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/ssh-pull.c b/ssh-pull.c new file mode 100644 index 0000000000..26356dd7d8 --- /dev/null +++ b/ssh-pull.c @@ -0,0 +1,97 @@ +#include "cache.h" +#include "commit.h" +#include "rsh.h" +#include "pull.h" +#include "refs.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; +} + +static 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 fetch_ref(char *ref, unsigned char *sha1) +{ + signed char remote; + char type = 'r'; + write(fd_out, &type, 1); + write(fd_out, ref, strlen(ref) + 1); + read(fd_in, &remote, 1); + if (remote < 0) + return remote; + read(fd_in, sha1, 20); + return 0; +} + +int main(int argc, char **argv) +{ + char *commit_id; + char *url; + int arg = 1; + const char *prog = getenv("GIT_SSH_PUSH") ? : "git-ssh-push"; + + 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++; + } + arg++; + } + if (argc < arg + 2) { + usage("git-ssh-pull [-c] [-t] [-a] [-v] [-d] [--recover] [-w ref] commit-id url"); + 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(commit_id)) + return 1; + + return 0; +} diff --git a/ssh-push.c b/ssh-push.c new file mode 100644 index 0000000000..aac70af514 --- /dev/null +++ b/ssh-push.c @@ -0,0 +1,131 @@ +#include "cache.h" +#include "rsh.h" +#include "refs.h" + +#include <string.h> + +static unsigned char local_version = 1; +static unsigned char remote_version = 0; + +static int verbose = 0; + +static int serve_object(int fd_in, int fd_out) { + ssize_t size; + unsigned char sha1[20]; + signed char remote; + int posn = 0; + 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); + + if (verbose) + fprintf(stderr, "Serving %s\n", sha1_to_hex(sha1)); + + remote = 0; + + if (!has_sha1_file(sha1)) { + 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; + + return write_sha1_to_fd(fd_out, sha1); +} + +static 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; +} + +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 (read(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; + write(fd_out, &remote, 1); + if (remote) + return 0; + write(fd_out, sha1, 20); + return 0; +} + + +static 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; + if (type == 'r' && serve_ref(fd_in, fd_out)) + return; + } while (1); +} + +static const char *ssh_push_usage = + "git-ssh-push [-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 = getenv("GIT_SSH_PULL") ? : "git-ssh-pull"; + unsigned char sha1[20]; + char hex[41]; + + 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)) + usage(ssh_push_usage); + 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..9d9d8bed91 --- /dev/null +++ b/strbuf.c @@ -0,0 +1,44 @@ +#include <stdio.h> +#include <stdlib.h> +#include "strbuf.h" +#include "cache.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/stripspace.c b/stripspace.c new file mode 100644 index 0000000000..96cd0a88f0 --- /dev/null +++ b/stripspace.c @@ -0,0 +1,48 @@ +#include <stdio.h> +#include <string.h> +#include <ctype.h> + +/* + * Remove empty lines from the beginning and end. + * + * Turn multiple consecutive empty lines into just one + * empty line. + */ +static void cleanup(char *line) +{ + int len = strlen(line); + + if (len > 1 && line[len-1] == '\n') { + do { + unsigned char c = line[len-2]; + if (!isspace(c)) + break; + line[len-2] = '\n'; + len--; + line[len] = 0; + } while (len > 1); + } +} + +int main(int argc, char **argv) +{ + int empties = -1; + char line[1024]; + + while (fgets(line, sizeof(line), stdin)) { + cleanup(line); + + /* Not just an empty line? */ + if (line[0] != '\n') { + if (empties > 0) + putchar('\n'); + empties = 0; + fputs(line, stdout); + continue; + } + if (empties < 0) + continue; + empties++; + } + return 0; +} diff --git a/t/Makefile b/t/Makefile new file mode 100644 index 0000000000..6882e23be5 --- /dev/null +++ b/t/Makefile @@ -0,0 +1,15 @@ +# Run tests +# +# Copyright (c) 2005 Junio C Hamano +# + +#GIT_TEST_OPTS=--verbose --debug + +T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) + +all: + @$(foreach t,$T,echo "*** $t ***"; sh $t $(GIT_TEST_OPTS) || exit; ) + @rm -fr trash + +clean: + rm -fr trash diff --git a/t/README b/t/README new file mode 100644 index 0000000000..ab47ef9c5e --- /dev/null +++ b/t/README @@ -0,0 +1,208 @@ +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-db in an empty repo. + * ok 2: .git/objects should have 256 subdirectories. + * ok 3: git-update-cache without --add should fail adding. + ... + * ok 23: no diff after checkout and git-update-cache --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-cache --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) + +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-cache without --add should fail adding.' \ + 'git-update-cache 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/diff-lib.sh b/t/diff-lib.sh new file mode 100644 index 0000000000..a912f435aa --- /dev/null +++ b/t/diff-lib.sh @@ -0,0 +1,35 @@ +: + +_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' <"$1" >.tmp-1 + sed -e '/^[dis]*imilarity index [0-9]*%$/d' <"$2" >.tmp-2 + diff -u .tmp-1 .tmp-2 && rm -f .tmp-1 .tmp-2 +} diff --git a/t/lib-read-tree-m-3way.sh b/t/lib-read-tree-m-3way.sh new file mode 100644 index 0000000000..d6645b4f42 --- /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-cache --add $p && + git-update-cache --add Z/$p' + done +done +echo This is SS from the original tree. >SS +test_expect_success \ + 'adding test file SS' \ + 'git-update-cache --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-cache --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-cache --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-cache $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-cache --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-cache --add LL && + git-update-cache 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-cache 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-cache --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-cache -a' + +to_remove=$(echo ?D Z/?D) +rm -f $to_remove +test_expect_success \ + 'change in branch B (removal)' \ + "git-update-cache --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-cache $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-cache --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-cache --add LL && + git-update-cache 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-cache 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-cache --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-cache --prefix=.orig-O/ -f -q -a && + rm -f .git/index && + git-read-tree $tree_A && + mkdir .orig-A && + git-checkout-cache --prefix=.orig-A/ -f -q -a && + rm -f .git/index && + git-read-tree $tree_B && + mkdir .orig-B && + git-checkout-cache --prefix=.orig-B/ -f -q -a' diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh new file mode 100755 index 0000000000..547488bd25 --- /dev/null +++ b/t/t0000-basic.sh @@ -0,0 +1,179 @@ +#!/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. +' +. ./test-lib.sh + +################################################################ +# init-db 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-db in an empty repo.' \ + 'cmp -s /dev/null should-be-empty' + +# also it should have 257 subdirectories. 258 is counting "objects" +find .git/objects -type d -print >full-of-directories +test_expect_success \ + '.git/objects should have 257 subdirectories.' \ + 'test $(wc -l < full-of-directories) = 258' + +################################################################ +# Basics of the basics + +# updating a new file without --add should fail. +test_expect_failure \ + 'git-update-cache without --add should fail adding.' \ + 'git-update-cache should-be-empty' + +# and with --add it should succeed, even if it is empty (it used to fail). +test_expect_success \ + 'git-update-cache with --add should succeed.' \ + 'git-update-cache --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-cache without --remove should fail removing.' \ + 'git-update-cache should-be-empty' + +test_expect_success \ + 'git-update-cache with --remove should be able to remove.' \ + 'git-update-cache --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-cache --add.' \ + 'find path* ! -type d -print0 | xargs -0 git-update-cache --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' + +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 +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' + +################################################################ +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-cache --refresh should succeed.' \ + 'git-update-cache --refresh' + +test_expect_success \ + 'no diff after checkout and git-update-cache --refresh.' \ + 'git-diff-files >current && cmp -s current /dev/null' + +test_done diff --git a/t/t0100-environment-names.sh b/t/t0100-environment-names.sh new file mode 100755 index 0000000000..9f851bc114 --- /dev/null +++ b/t/t0100-environment-names.sh @@ -0,0 +1,84 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='general environment name warning test. + +This test makes sure that use of deprecated environment variables +trigger the warnings from gitenv().' + +env_vars='GIT_AUTHOR_DATE:AUTHOR_DATE +GIT_AUTHOR_EMAIL:AUTHOR_EMAIL +GIT_AUTHOR_NAME:AUTHOR_NAME +GIT_COMMITTER_EMAIL:COMMIT_AUTHOR_EMAIL +GIT_COMMITTER_NAME:COMMIT_AUTHOR_NAME +GIT_ALTERNATE_OBJECT_DIRECTORIES:SHA1_FILE_DIRECTORIES +GIT_OBJECT_DIRECTORY:SHA1_FILE_DIRECTORY +' + +. ./test-lib.sh + +export_them () { + for ev in $env_vars + do + new=$(expr "$ev" : '\(.*\):') + old=$(expr "$ev" : '.*:\(.*\)') + # Build and eval the following: + # case "${VAR+set}" in set) export VAR;; esac + evstr='case "${'$new'+set}" in set) export '$new';; esac' + eval "$evstr" + evstr='case "${'$old'+set}" in set) export '$old';; esac' + eval "$evstr" + done +} + +date >path0 +git-update-cache --add path0 +tree=$(git-write-tree) + +AUTHOR_DATE='Wed May 11 23:55:18 2005' +AUTHOR_EMAIL='author@example.xz' +AUTHOR_NAME='A U Thor' +COMMIT_AUTHOR_EMAIL='author@example.xz' +COMMIT_AUTHOR_NAME='A U Thor' +SHA1_FILE_DIRECTORY=.git/objects + +export_them + +echo 'foo' | git-commit-tree $tree >/dev/null 2>errmsg +cat >expected-err <<\EOF +warning: Attempting to use SHA1_FILE_DIRECTORY +warning: GIT environment variables have been renamed. +warning: Please adjust your scripts and environment. +warning: old AUTHOR_DATE => new GIT_AUTHOR_DATE +warning: old AUTHOR_EMAIL => new GIT_AUTHOR_EMAIL +warning: old AUTHOR_NAME => new GIT_AUTHOR_NAME +warning: old COMMIT_AUTHOR_EMAIL => new GIT_COMMITTER_EMAIL +warning: old COMMIT_AUTHOR_NAME => new GIT_COMMITTER_NAME +warning: old SHA1_FILE_DIRECTORY => new GIT_OBJECT_DIRECTORY +EOF +sed -ne '/^warning: /p' <errmsg >generated-err + +test_expect_success \ + 'using old names should issue warnings.' \ + 'cmp generated-err expected-err' + +for ev in $env_vars +do + new=$(expr "$ev" : '\(.*\):') + old=$(expr "$ev" : '.*:\(.*\)') + # Build and eval the following: + # NEWENV=$OLDENV + evstr="$new=\$$old" + eval "$evstr" +done +export_them +echo 'foo' | git-commit-tree $tree >/dev/null 2>errmsg +sed -ne '/^warning: /p' <errmsg >generated-err + +test_expect_success \ + 'using old names but having new names should not issue warnings.' \ + 'cmp generated-err /dev/null' + +test_done diff --git a/t/t0110-environment-names-old.sh b/t/t0110-environment-names-old.sh new file mode 100755 index 0000000000..c548b9b497 --- /dev/null +++ b/t/t0110-environment-names-old.sh @@ -0,0 +1,132 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Using new and old environment names. + +This test makes sure that use of deprecated environment variables +still works, using both new and old names makes new one take precedence, +and GIT_DIR and GIT_ALTERNATE_OBJECT_DIRECTORIES mechanism works.' + +env_vars='GIT_AUTHOR_DATE:AUTHOR_DATE +GIT_AUTHOR_EMAIL:AUTHOR_EMAIL +GIT_AUTHOR_NAME:AUTHOR_NAME +GIT_COMMITTER_EMAIL:COMMIT_AUTHOR_EMAIL +GIT_COMMITTER_NAME:COMMIT_AUTHOR_NAME +GIT_ALTERNATE_OBJECT_DIRECTORIES:SHA1_FILE_DIRECTORIES +GIT_OBJECT_DIRECTORY:SHA1_FILE_DIRECTORY +' + +. ./test-lib.sh + +export_them () { + for ev in $env_vars + do + new=$(expr "$ev" : '\(.*\):') + old=$(expr "$ev" : '.*:\(.*\)') + # Build and eval the following: + # case "${VAR+set}" in set) export VAR;; esac + evstr='case "${'$new'+set}" in set) export '$new';; esac' + eval "$evstr" + evstr='case "${'$old'+set}" in set) export '$old';; esac' + eval "$evstr" + done +} + +SHA1_FILE_DIRECTORY=.svn/objects ;# whoa +export SHA1_FILE_DIRECTORY + +rm -fr .git +mkdir .svn +test_expect_success \ + 'using SHA1_FILE_DIRECTORY in git-init-db' \ + 'git-init-db && test -d .svn/objects/cb' + +unset SHA1_FILE_DIRECTORY +GIT_DIR=.svn +export GIT_DIR +rm -fr .git .svn +mkdir .svn +test_expect_success \ + 'using GIT_DIR in git-init-db' \ + 'git-init-db && test -d .svn/objects/cb' + +date >path0 +test_expect_success \ + 'using GIT_DIR in git-update-cache' \ + 'git-update-cache --add path0 && test -f .svn/index' + +sedScript='s|\(..\)|.svn/objects/\1/|' + +test_expect_success \ + 'using GIT_DIR in git-write-tree' \ + 'tree=$(git-write-tree) && + test -f $(echo "$tree" | sed -e "$sedScript")' + +AUTHOR_DATE='Sat May 14 00:00:00 2005 -0000' +AUTHOR_EMAIL='author@example.xz' +AUTHOR_NAME='A U Thor' +COMMIT_AUTHOR_EMAIL='author@example.xz' +COMMIT_AUTHOR_NAME='A U Thor' +export_them + +test_expect_success \ + 'using GIT_DIR and old variable names in git-commit-tree' \ + 'commit=$(echo foo | git-commit-tree $tree) && + test -f $(echo "$commit" | sed -e "$sedScript")' + +test_expect_success \ + 'using GIT_DIR in git-cat-file' \ + 'git-cat-file commit $commit >current' + +cat >expected <<\EOF +author A U Thor <author@example.xz> +committer A U Thor <author@example.xz> +EOF +test_expect_success \ + 'verify old AUTHOR variables were used correctly in commit' \ + 'sed -ne '\''/^\(author\)/s|>.*|>|p'\'' -e'\''/^\(committer\)/s|>.*|>|p'\''\ current > out && cmp out expected' + +unset GIT_DIR +test_expect_success \ + 'git-init-db without GIT_DIR' \ + 'git-init-db && test -d .git && test -d .git/objects/ef' + +SHA1_FILE_DIRECTORIES=.svn/objects +export SHA1_FILE_DIRECTORIES + +test_expect_success \ + 'using SHA1_FILE_DIRECTORIES with git-ls-tree' \ + 'git-ls-tree $commit && git-ls-tree $tree' + +GIT_AUTHOR_DATE='Sat May 14 12:00:00 2005 -0000' +GIT_AUTHOR_EMAIL='rohtua@example.xz' +GIT_AUTHOR_NAME='R O Htua' +GIT_COMMITTER_EMAIL='rohtua@example.xz' +GIT_COMMITTER_NAME='R O Htua' +export_them + +sedScript='s|\(..\)|.git/objects/\1/|' +test_expect_success \ + 'using new author variables with git-commit-tree' \ + 'commit2=$(echo foo | git-commit-tree $tree) && + test -f $(echo "$commit2" | sed -e "$sedScript")' + +GIT_ALTERNATE_OBJECT_DIRECTORIES=.git/objects +GIT_DIR=nowhere +export GIT_DIR GIT_ALTERNATE_OBJECT_DIRECTORIES + +test_expect_success \ + 'git-cat-file with GIT_DIR and GIT_ALTERNATE_OBJECT_DIRECTORIES' \ + 'git-cat-file commit $commit2 >current' + +cat >expected <<\EOF +author R O Htua <rohtua@example.xz> +committer R O Htua <rohtua@example.xz> +EOF +test_expect_success \ + 'verify new AUTHOR variables were used correctly in commit.' \ + 'sed -ne '\''/^\(author\)/s|>.*|>|p'\'' -e'\''/^\(committer\)/s|>.*|>|p'\''\ current > out && cmp out expected' + +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..89f0e81f23 --- /dev/null +++ b/t/t1000-read-tree-m-3way.sh @@ -0,0 +1,517 @@ +#!/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-cache -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. The table also lists alternative semantics which is not +currently implemented. + +"git-diff-tree -m O A B" + + O A B result index requirements +------------------------------------------------------------------- + 1 missing missing missing - must not exist. + ------------------------------------------------------------------ + 2 missing missing exists no merge must not exist. + ------------------------------------ + (ALT) take B* must match B, if exists. + ------------------------------------------------------------------ + 3 missing exists missing no merge must match A and be + up-to-date, if exists. + ------------------------------------ + (ALT) 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 no merge must match A and be + up-to-date, if exists. + ------------------------------------ + (ALT) take A must match A, if exists. + ------------------------------------------------------------------ + 6 exists missing missing no merge must not exist. + ------------------------------------ + (ALT) remove must not exist. + ------------------------------------------------------------------ + 7 exists missing O!=B no merge must not exist. + ------------------------------------------------------------------ + 8 exists missing O==B no merge must not exist. + ------------------------------------ + (ALT) 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 no merge must match A and be + up-to-date, if exists. + ------------------------------------ + (ALT) 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 must match A and be + be up-to-date, if exists. + ------------------------------------ + (ALT) 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. +------------------------------------------------------------------- + +Note: if we want to implement 2ALT and 3ALT we need to be careful. +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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --add NN && + git-read-tree -m $tree_O $tree_A $tree_B" + +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..b3e6bd57d0 --- /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-rev-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-cache --add nitfol bozbar rezrov && + treeH=`git-write-tree` && + echo treeH $treeH && + git-ls-tree $treeH && + + cat bozbar-new >bozbar && + git-update-cache --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-cache -u -f -q -a && + git-update-cache --add yomin && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >4.out || exit + 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-cache -u -f -q -a && + echo yomin >yomin && + git-update-cache --add yomin && + echo yomin yomin >yomin && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >5.out || exit + 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-cache -u -f -q -a && + git-update-cache --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-cache -u -f -q -a && + echo frotz >frotz && + git-update-cache --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-cache -u -f -q -a && + echo frotz frotz >frotz && + git-update-cache --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-cache -u -f -q -a && + echo frotz frotz >frotz && + git-update-cache --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-cache -u -f -q -a && + echo rezrov >rezrov && + git-update-cache --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-cache -u -f -q -a && + echo rezrov >rezrov && + git-update-cache --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-cache -u -f -q -a && + echo rezrov rezrov >rezrov && + git-update-cache --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-cache -u -f -q -a && + echo rezrov rezrov >rezrov && + git-update-cache --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-cache -u -f -q -a && + echo nitfol nitfol >nitfol && + git-update-cache --add nitfol && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >14.out || exit + 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-cache -u -f -q -a && + echo nitfol nitfol >nitfol && + git-update-cache --add nitfol && + echo nitfol nitfol nitfol >nitfol && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >15.out || exit + 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-cache -u -f -q -a && + echo bozbar bozbar >bozbar && + git-update-cache --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-cache -u -f -q -a && + echo bozbar bozbar >bozbar && + git-update-cache --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-cache -u -f -q -a && + cat bozbar-new >bozbar && + git-update-cache --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-cache -u -f -q -a && + cat bozbar-new >bozbar && + git-update-cache --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-cache -u -f -q -a && + cat bozbar-old >bozbar && + git-update-cache --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-cache -u -f -q -a && + cat bozbar-old >bozbar && + git-update-cache --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-cache -u -f -q -a && + sed -e "s/such as/SUCH AS/" bozbar-old >bozbar && + git-update-cache --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-cache --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-cache --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-cache --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..2f1ee79698 --- /dev/null +++ b/t/t1002-read-tree-m-u-2way.sh @@ -0,0 +1,324 @@ +#!/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 && + echo yomin >yomin && + git-update-cache --add nitfol bozbar rezrov && + treeH=`git-write-tree` && + echo treeH $treeH && + git-ls-tree $treeH && + + echo gnusto >bozbar && + git-update-cache --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 && + 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' + +echo '+100644 X 0 yomin' >expected + +test_expect_success \ + '4 - carry forward local addition.' \ + 'rm -f .git/index && + git-update-cache --add yomin && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >4.out || exit + diff --unified=0 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 && + echo yomin >yomin && + git-update-cache --add yomin && + echo yomin yomin >yomin && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >5.out || exit + diff --unified=0 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 && + git-update-cache --add frotz && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >6.out && + diff --unified=0 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 && + echo frotz >frotz && + git-update-cache --add frotz && + echo frotz frotz >frotz && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >7.out && + diff --unified=0 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 && + echo frotz frotz >frotz && + git-update-cache --add frotz && + if git-read-tree -m -u $treeH $treeM; then false; else :; fi' + +test_expect_success \ + '9 - conflicting addition.' \ + 'rm -f .git/index && + echo frotz frotz >frotz && + git-update-cache --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 && + echo rezrov >rezrov && + git-update-cache --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 && + echo rezrov >rezrov && + git-update-cache --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 && + echo rezrov rezrov >rezrov && + git-update-cache --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 && + echo rezrov rezrov >rezrov && + git-update-cache --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 && + echo nitfol nitfol >nitfol && + git-update-cache --add nitfol && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >14.out || exit + diff --unified=0 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 && + echo nitfol nitfol >nitfol && + git-update-cache --add nitfol && + echo nitfol nitfol nitfol >nitfol && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >15.out || exit + diff --unified=0 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 && + echo bozbar bozbar >bozbar && + git-update-cache --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 && + echo bozbar bozbar >bozbar && + git-update-cache --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 && + echo gnusto >bozbar && + git-update-cache --add bozbar && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >18.out && + diff --unified=0 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 && + echo gnusto >bozbar && + git-update-cache --add bozbar && + echo gnusto gnusto >bozbar && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >19.out && + diff --unified=0 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 && + echo bozbar >bozbar && + git-update-cache --add bozbar && + git-read-tree -m -u $treeH $treeM && + git-ls-files --stage >20.out && + diff --unified=0 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 && + echo bozbar >bozbar && + git-update-cache --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-cache --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-cache --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-cache --add DF && + git-read-tree -m -u $treeDF $treeDFDF && + git-ls-files --stage >DFDFcheck.out && + diff --unified=0 DFDF.out DFDFcheck.out && + check_cache_at DF/DF clean' + +test_done diff --git a/t/t1005-read-tree-m-2way-emu23.sh b/t/t1005-read-tree-m-2way-emu23.sh new file mode 100644 index 0000000000..d80752ddd5 --- /dev/null +++ b/t/t1005-read-tree-m-2way-emu23.sh @@ -0,0 +1,422 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Two way merge with read-tree --emu23 $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-rev-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 --emu23 "$1" "$2" && + git-ls-files --stage && + git-merge-cache git-merge-one-file-script -a && + 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 +} + +check_stages () { + cat >expected_stages + git-ls-files --stage | sed -e "s/ $_x40 / X /" >current_stages + diff -u expected_stages current_stages +} + +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-cache --add nitfol bozbar rezrov && + treeH=`git-write-tree` && + echo treeH $treeH && + git-ls-tree $treeH && + + cat bozbar-new >bozbar && + git-update-cache --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' + +# "read-tree -m H I+H M" but I is empty so this is "read-tree -m H H M". +# +# bozbar [O && A && B && O==A && O!=B (#14) ==> B] take M by read-tree +# frotz [!O && !A && B (#2) ==> B] take M by read-tree +# nitfol [O && A && B && O==A && O==B (#15) ==> B] take M by read-tree +# rezrov [O && A && !B && O==A (#10) ==> no merge] removed by script +# +# Earlier one did not have #2ALT so taking M was done by the script, +# which also updated the work tree and making frotz clean. With #2ALT, +# this is resolved by read-tree itself and the path is left dirty +# because we are not testing "read-tree -u --emu23". +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 && # same as pure 2-way again. + 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-cache -u -f -q -a && + git-update-cache --add yomin && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >4.out || exit + diff -u M.out 4.out >4diff.out + compare_change 4diff.out expected && + check_cache_at yomin clean' + +# "read-tree -m H I+H M" where !H && !M; so (I+H) not being up-to-date +# should not matter. Thanks to #3ALT, this is now possible. +test_expect_success \ + '5 - carry forward local addition.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-cache -u -f -q -a && + echo yomin >yomin && + git-update-cache --add yomin && + echo yomin yomin >yomin && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >5.out || exit + diff -u M.out 5.out >5diff.out + compare_change 5diff.out expected && + check_cache_at yomin dirty' + +# "read-tree -m H I+H M" where !H && M && (I+H) == M, so this should +# succeed (even the entry is clean), now thanks to #5ALT. +test_expect_success \ + '6 - local addition already has the same.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-cache -u -f -q -a && + git-update-cache --add frotz && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >6.out && + diff -u M.out 6.out && + check_cache_at frotz clean' + +# Exactly the same pattern as above but with dirty cache. This also +# should succeed, now thanks to #5ALT. +test_expect_success \ + '7 - local addition already has the same.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-cache -u -f -q -a && + echo frotz >frotz && + git-update-cache --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-cache -u -f -q -a && + echo frotz frotz >frotz && + git-update-cache --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-cache -u -f -q -a && + echo frotz frotz >frotz && + git-update-cache --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-cache -u -f -q -a && + echo rezrov >rezrov && + git-update-cache --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-cache -u -f -q -a && + echo rezrov >rezrov && + git-update-cache --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-cache -u -f -q -a && + echo rezrov rezrov >rezrov && + git-update-cache --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-cache -u -f -q -a && + echo rezrov rezrov >rezrov && + git-update-cache --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-cache -u -f -q -a && + echo nitfol nitfol >nitfol && + git-update-cache --add nitfol && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >14.out || exit + 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-cache -u -f -q -a && + echo nitfol nitfol >nitfol && + git-update-cache --add nitfol && + echo nitfol nitfol nitfol >nitfol && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >15.out || exit + diff -u M.out 15.out >15diff.out + compare_change 15diff.out expected && + check_cache_at nitfol dirty' + +# This is different from straight 2-way merge in that it leaves +# three stages of bozbar in the index file without failing, so +# the user can run git-diff-stages to examine the situation. +# With #2ALT, frotz is resolved internally. +test_expect_success \ + '16 - conflicting local change.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-cache -u -f -q -a && + echo bozbar bozbar >bozbar && + git-update-cache --add bozbar && + git-read-tree --emu23 $treeH $treeM && + check_stages' <<\EOF +100644 X 1 bozbar +100644 X 2 bozbar +100644 X 3 bozbar +100644 X 0 frotz +100644 X 0 nitfol +100644 X 1 rezrov +100644 X 2 rezrov +EOF + +test_expect_success \ + '17 - conflicting local change.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-cache -u -f -q -a && + echo bozbar bozbar >bozbar && + git-update-cache --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-cache -u -f -q -a && + cat bozbar-new >bozbar && + git-update-cache --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-cache -u -f -q -a && + cat bozbar-new >bozbar && + git-update-cache --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-cache -u -f -q -a && + cat bozbar-old >bozbar && + git-update-cache --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-cache -u -f -q -a && + cat bozbar-old >bozbar && + git-update-cache --add bozbar && + echo gnusto gnusto >bozbar && + if read_tree_twoway $treeH $treeM; then false; else :; fi' + +echo '-100644 X 0 bozbar ++100644 X 0 bozbar' >expected + +# This fails with straight two-way fast forward, but emu23 +# can merge them. +test_expect_success \ + '22 - local change cache updated.' \ + 'rm -f .git/index && + git-read-tree $treeH && + git-checkout-cache -u -f -q -a && + sed -e "s/such as/SUCH AS/" bozbar-old >bozbar && + git-update-cache --add bozbar && + read_tree_twoway $treeH $treeM && + git-ls-files --stage >22.out || exit + diff -u M.out 22.out >22diff.out + compare_change 22diff.out && + check_cache_at bozbar clean' + +# 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-cache --add DF && + treeDF=`git-write-tree` && + echo treeDF $treeDF && + git-ls-tree $treeDF && + git-ls-files --stage >DF.out + + rm -f DF && + mkdir DF && + echo DF/DF >DF/DF && + git-update-cache --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 (#1)' \ + 'rm -f .git/index && + rm -fr DF && + echo DF >DF && + git-update-cache --add DF && + read_tree_twoway $treeDF $treeDFDF && + git-ls-files --stage >DFDFcheck.out && + diff -u DFDF.out DFDFcheck.out && + check_cache_at DF/DF clean && # different from pure 2-way + :' + +# The other way around +test_expect_success \ + 'DF vs DF/DF case test (#2)' \ + 'rm -f .git/index && + rm -fr DF && + mkdir DF && + echo DF/DF >DF/DF && + git-update-cache --add DF/DF && + read_tree_twoway $treeDFDF $treeDF && + git-ls-files --stage >DFDFcheck.out && + diff -u DF.out DFDFcheck.out && + check_cache_at DF clean && # different from pure 2-way + :' + +# Emu23 can grok I having more than H. Make sure we did not +# botch the conflict tests (fixed). +test_expect_success \ + 'DF vs DF/DF case test (#3).' \ + 'rm -f .git/index && + rm -fr DF && + mkdir DF && + echo DF/DF >DF/DF && + git-update-cache --add DF/DF && + # This should fail because I and H have a conflict + # at DF. + if git-read-tree --emu23 $treeDF $treeDFDF + then false + else true + fi' + +test_done diff --git a/t/t1100-commit-tree-options.sh b/t/t1100-commit-tree-options.sh new file mode 100644 index 0000000000..e59f724f2a --- /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= 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/t2000-checkout-cache-clash.sh b/t/t2000-checkout-cache-clash.sh new file mode 100755 index 0000000000..a2c42602da --- /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-cache 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-cache 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-cache --add various paths.' \ + 'git-update-cache --add path0 path1/file1' + +rm -fr path0 path1 +mkdir path0 +date >path0/file0 +date >path1 + +test_expect_failure \ + 'git-checkout-cache without -f should fail on conflicting work tree.' \ + 'git-checkout-cache -a' + +test_expect_success \ + 'git-checkout-cache with -f should succeed.' \ + 'git-checkout-cache -f -a' + +test_expect_success \ + 'git-checkout-cache 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..f0e3d1d8c3 --- /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-cache 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-cache --add path0/file0' \ + 'git-update-cache --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-cache --add path1/file1' \ + 'git-update-cache --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-cache -f -a' +test_debug 'show_files $tree1' + +ln -s path0 path1 +test_expect_success \ + 'git-update-cache --add a symlink.' \ + 'git-update-cache --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-cache -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..69146acc3a --- /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-cache -u test. + +With -u flag, git-checkout-cache internally runs the equivalent of +git-update-cache --refresh on the checked out entry.' + +. ./test-lib.sh + +test_expect_success \ +'preparation' ' +echo frotz >path0 && +git-update-cache --add path0 && +t=$(git-write-tree)' + +test_expect_failure \ +'without -u, git-checkout-cache smudges stat information.' ' +rm -f path0 && +git-read-tree $t && +git-checkout-cache -f -a && +git-diff-files | diff - /dev/null' + +test_expect_success \ +'with -u, git-checkout-cache picks up stat information from new files.' ' +rm -f path0 && +git-read-tree $t && +git-checkout-cache -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 100644 index 0000000000..6ec28179be --- /dev/null +++ b/t/t2003-checkout-cache-mkdir.sh @@ -0,0 +1,95 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-checkout-cache --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-cache --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-cache -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-cache --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-cache --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-cache --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-cache --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-cache --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-cache --prefix=tmp- -f -a && + test -f tmp-path0 && + test ! -h tmp-path1 && + test -d tmp-path1 && + test -f tmp-path1/file1' + diff --git a/t/t2100-update-cache-badpath.sh b/t/t2100-update-cache-badpath.sh new file mode 100755 index 0000000000..86b7375c6c --- /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-cache 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-cache --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-cache --add to add various paths.' \ + 'git-update-cache --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-cache to add conflicting path $p should fail." \ + "git-update-cache --add -- $p" +done +test_done diff --git a/t/t3000-ls-files-others.sh b/t/t3000-ls-files-others.sh new file mode 100755 index 0000000000..1f461e3e81 --- /dev/null +++ b/t/t3000-ls-files-others.sh @@ -0,0 +1,34 @@ +#!/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 +' +. ./test-lib.sh + +date >path0 +ln -s xyzzy path1 +mkdir path2 +date >path2/file2 +test_expect_success \ + 'git-ls-files --others to show output.' \ + 'git-ls-files --others >output' +cat >expected <<EOF +output +path0 +path1 +path2/file2 +EOF + +test_expect_success \ + 'git-ls-files --others should pick up symlinks.' \ + 'diff output expected' +test_done diff --git a/t/t3010-ls-files-killed.sh b/t/t3010-ls-files-killed.sh new file mode 100755 index 0000000000..c4d6d2163f --- /dev/null +++ b/t/t3010-ls-files-killed.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='git-ls-files -k flag 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. +' +. ./test-lib.sh + +date >path0 +ln -s xyzzy path1 +mkdir path2 path3 +date >path2/file2 +date >path3/file3 +test_expect_success \ + 'git-update-cache --add to add various paths.' \ + "git-update-cache --add -- path0 path1 path?/file?" + +rm -fr path? +date >path2 +ln -s frotz path3 +ln -s nitfol path5 +mkdir path0 path1 path6 +date >path0/file0 +date >path1/file1 +date >path6/file6 + +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_done diff --git a/t/t3100-ls-tree-restrict.sh b/t/t3100-ls-tree-restrict.sh new file mode 100644 index 0000000000..61a7c7f642 --- /dev/null +++ b/t/t3100-ls-tree-restrict.sh @@ -0,0 +1,131 @@ +#!/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-cache --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 +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 filtered with path' \ + 'git-ls-tree $tree path >current && + cat >expected <<\EOF && +EOF + test_output' + + +test_expect_success \ + 'ls-tree filtered with path1 path0' \ + 'git-ls-tree $tree path1 path0 >current && + cat >expected <<\EOF && +120000 blob X path1 +100644 blob X path0 +EOF + test_output' + +test_expect_success \ + 'ls-tree filtered with path0/' \ + 'git-ls-tree $tree path0/ >current && + cat >expected <<\EOF && +EOF + test_output' + +test_expect_success \ + 'ls-tree filtered with path2' \ + 'git-ls-tree $tree path2 >current && + cat >expected <<\EOF && +040000 tree X path2 +040000 tree X path2/baz +120000 blob X path2/bazbo +100644 blob X path2/foo +EOF + test_output' + +test_expect_success \ + 'ls-tree filtered with path2/baz' \ + 'git-ls-tree $tree path2/baz >current && + cat >expected <<\EOF && +040000 tree X path2/baz +100644 blob X path2/baz/b +EOF + test_output' + +test_expect_success \ + 'ls-tree filtered with path2' \ + 'git-ls-tree $tree path2 >current && + cat >expected <<\EOF && +040000 tree X path2 +040000 tree X path2/baz +120000 blob X path2/bazbo +100644 blob X path2/foo +EOF + test_output' + +test_expect_success \ + 'ls-tree filtered with path2/' \ + 'git-ls-tree $tree path2/ >current && + cat >expected <<\EOF && +040000 tree X path2 +040000 tree X path2/baz +120000 blob X path2/bazbo +100644 blob X path2/foo +EOF + test_output' + +test_done diff --git a/t/t4000-diff-format.sh b/t/t4000-diff-format.sh new file mode 100755 index 0000000000..3accb14f00 --- /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 + +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-cache --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' +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.' \ + 'cmp -s current expected' + +test_expect_success \ + 'build same diff using git-diff-helper.' \ + 'git-diff-files -z | git-diff-helper -z >current' + + +test_expect_success \ + 'validate git-diff-helper output.' \ + 'cmp -s current expected' + +test_done diff --git a/t/t4001-diff-rename.sh b/t/t4001-diff-rename.sh new file mode 100755 index 0000000000..80edae6682 --- /dev/null +++ b/t/t4001-diff-rename.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +test_description='Test rename detection in diff engine. + +' +. ./test-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-cache --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-cache --add --remove path0 path1' + +test_expect_success \ + 'git-diff-cache -p -M after rename and editing.' \ + 'git-diff-cache -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.' \ + 'diff -I "similarity.*" >/dev/null current expected' + +test_done diff --git a/t/t4002-diff-basic.sh b/t/t4002-diff-basic.sh new file mode 100644 index 0000000000..5119323a0d --- /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 N AA +:000000 100644 0000000000000000000000000000000000000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 N AN +:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D DD +:000000 040000 0000000000000000000000000000000000000000 6d50f65d3bdab91c63444294d38f08aeff328e42 N DF +:100644 000000 141c1f1642328e4bc46a7d801a71da392e66791e 0000000000000000000000000000000000000000 D DM +:100644 000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 0000000000000000000000000000000000000000 D DN +:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 N 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 N AA +:000000 100644 0000000000000000000000000000000000000000 7e426fb079479fd67f6d81f984e4ec649a44bc25 N AN +:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D DD +:000000 100644 0000000000000000000000000000000000000000 68a6d8b91da11045cf4aa3a5ab9f2a781c701249 N DF/DF +:100644 000000 141c1f1642328e4bc46a7d801a71da392e66791e 0000000000000000000000000000000000000000 D DM +:100644 000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 0000000000000000000000000000000000000000 D DN +:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 N 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 N Z/AA +:000000 100644 0000000000000000000000000000000000000000 087494262084cefee7ed484d20c8dc0580791272 N 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 N AA +:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D DD +:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 N DF +:100644 100644 141c1f1642328e4bc46a7d801a71da392e66791e 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 M DM +:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 N LL +:100644 000000 03f24c8c4700babccfd28b654e7e8eac402ad6cd 0000000000000000000000000000000000000000 D MD +:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 19989d4559aae417fedee240ccf2ba315ea4dc2b M MM +:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 N 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 N AA +:100644 000000 bcc68ef997017466d5c9094bcf7692295f588c9a 0000000000000000000000000000000000000000 D DD +:000000 100644 0000000000000000000000000000000000000000 71420ab81e254145d26d6fc0cddee64c1acd4787 N DF +:100644 100644 141c1f1642328e4bc46a7d801a71da392e66791e 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 M DM +:000000 100644 0000000000000000000000000000000000000000 1d41122ebdd7a640f29d3c9cc4f9d70094374762 N LL +:100644 000000 03f24c8c4700babccfd28b654e7e8eac402ad6cd 0000000000000000000000000000000000000000 D MD +:100644 100644 b258508afb7ceb449981bd9d63d2d3e971bf8d34 19989d4559aae417fedee240ccf2ba315ea4dc2b M MM +:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 N 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 N 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 N 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 N DF +:040000 000000 6d50f65d3bdab91c63444294d38f08aeff328e42 0000000000000000000000000000000000000000 D DF +:000000 100644 0000000000000000000000000000000000000000 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 N DM +:000000 100644 0000000000000000000000000000000000000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 N DN +:100644 000000 103d9f89b50b9aad03054b579be5e7aa665f2d57 0000000000000000000000000000000000000000 D MD +:100644 100644 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 19989d4559aae417fedee240ccf2ba315ea4dc2b M MM +:100644 100644 a716d58de4a570e0038f5c307bd8db34daea021f bd084b0c27c7b6cc34f11d6d0509a29be3caf970 M MN +:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 N 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 N DF +:100644 000000 68a6d8b91da11045cf4aa3a5ab9f2a781c701249 0000000000000000000000000000000000000000 D DF/DF +:000000 100644 0000000000000000000000000000000000000000 3c4d8de5fbad08572bab8e10eef8dbb264cf0231 N DM +:000000 100644 0000000000000000000000000000000000000000 35abde1506ddf806572ff4d407bd06885d0f8ee9 N DN +:100644 000000 103d9f89b50b9aad03054b579be5e7aa665f2d57 0000000000000000000000000000000000000000 D MD +:100644 100644 b431b272d829ff3aa4d1a5085f4394ab4d3305b6 19989d4559aae417fedee240ccf2ba315ea4dc2b M MM +:100644 100644 a716d58de4a570e0038f5c307bd8db34daea021f bd084b0c27c7b6cc34f11d6d0509a29be3caf970 M MN +:000000 100644 0000000000000000000000000000000000000000 15885881ea69115351c09b38371f0348a3fb8c67 N 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 N Z/DM +:000000 100644 0000000000000000000000000000000000000000 beb5d38c55283d280685ea21a0e50cfcc0ca064a N 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 N 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-cache --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-cache --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-cache --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-cache -f -a && + git-read-tree -m $tree_O || (exit 1) + git-update-cache --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-cache -f -a && + git-read-tree -m $tree_O || (exit 1) + git-update-cache --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-cache -f -a && + git-read-tree -m $tree_A || (exit 1) + git-update-cache --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 100644 index 0000000000..8e3091abb6 --- /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-cache --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-cache --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-cache -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-cache --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-cache -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-cache --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-cache as well, should not be mentioned. + +GIT_DIFF_OPTS=--unified=0 \ + git-diff-cache -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 100644 index 0000000000..010dd87eef --- /dev/null +++ b/t/t4004-diff-rename-symlink.sh @@ -0,0 +1,66 @@ +#!/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 + +test_expect_success \ + 'prepare reference tree' \ + 'echo xyzzy | tr -d '\\\\'012 >yomin && + ln -s xyzzy frotz && + git-update-cache --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-cache --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-cache -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' \ + 'diff -u current expected' + +test_done diff --git a/t/t4005-diff-rename-2.sh b/t/t4005-diff-rename-2.sh new file mode 100644 index 0000000000..cee06e4c5d --- /dev/null +++ b/t/t4005-diff-rename-2.sh @@ -0,0 +1,166 @@ +#!/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-cache --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-cache --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-cache -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' + +# make sure diff-helper can grok it. +mv expected diff-raw +GIT_DIFF_OPTS=--unified=0 git-diff-helper <diff-raw >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 diff-helper (#1)' \ + 'compare_diff_patch current expected' + +################################################################ + +test_expect_success \ + 'prepare work tree again' \ + 'mv COPYING.2 COPYING && + git-update-cache --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-cache -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' + +# make sure diff-helper can grok it. +mv expected diff-raw +GIT_DIFF_OPTS=--unified=0 git-diff-helper <diff-raw >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 diff-helper (#2)' \ + 'compare_diff_patch 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-cache --add --remove COPYING COPYING.1' + +git-diff-cache -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' + +# make sure diff-helper can grok it. +mv expected diff-raw +GIT_DIFF_OPTS=--unified=0 git-diff-helper <diff-raw >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 diff-helper (#3)' \ + 'compare_diff_patch current expected' + +test_done diff --git a/t/t4006-diff-mode.sh b/t/t4006-diff-mode.sh new file mode 100644 index 0000000000..90fd21ff1f --- /dev/null +++ b/t/t4006-diff-mode.sh @@ -0,0 +1,34 @@ +#!/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-cache --add rezrov && + tree=`git-write-tree` && + echo $tree' + +test_expect_success \ + 'chmod' \ + 'chmod +x rezrov && + git-update-cache rezrov && + git-diff-cache $tree >current' + +_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 100644 index 0000000000..ab83ea3b8a --- /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-cache --add path0/COPYING && + tree=$(git-write-tree) && + echo $tree' + +test_expect_success \ + 'prepare work tree' \ + 'cp path0/COPYING path1/COPYING && + git-update-cache --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-cache -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-cache -C $tree path1 >current + +cat >expected <<\EOF +:000000 100644 0000000000000000000000000000000000000000 6ff87c4664981e4397625791c8ea3bbb5f2279a3 N 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-cache --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-cache -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-cache -C $tree path1 >current + +cat >expected <<\EOF +:000000 100644 0000000000000000000000000000000000000000 6ff87c4664981e4397625791c8ea3bbb5f2279a3 N 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 100644 index 0000000000..040d0ddbd4 --- /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-cache --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-cache --remove file0 file1' + +test_expect_success \ + 'run diff with -B' \ + 'git-diff-cache -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-cache -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-cache -f -u -a && + mv file0 tmp && + mv file1 file0 && + mv tmp file1 && + git-update-cache file0 file1' + +test_expect_success \ + 'run diff with -B' \ + 'git-diff-cache -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-cache -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-cache file0 file1' + +test_expect_success \ + 'run diff with -B' \ + 'git-diff-cache -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-cache -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-cache -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-cache -f -u -a && + sed -e "s/git/GIT/" file0 >file1 && + sed -e "s/git/GET/" file0 >file2 && + rm -f file0 + git-update-cache --add --remove file0 file1 file2' + +test_expect_success \ + 'run diff with -B' \ + 'git-diff-cache -B "$tree" >current' + +cat >expected <<\EOF +:100644 000000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 0000000000000000000000000000000000000000 D file0 +:100644 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 08bb2fb671deff4c03a4d4a0a1315dff98d5732c M100 file1 +:000000 100644 0000000000000000000000000000000000000000 f5deac7be59e7eeab8657fd9ae706fd6a57daed2 N 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-cache -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 100644 index 0000000000..6229a5bf1a --- /dev/null +++ b/t/t4009-diff-rename-4.sh @@ -0,0 +1,175 @@ +#!/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-cache --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-cache --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-cache -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' + +# make sure diff-helper can grok it. +mv current diff-raw +GIT_DIFF_OPTS=--unified=0 git-diff-helper -z <diff-raw >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 diff-helper (#1)' \ + 'compare_diff_patch current expected' + +################################################################ + +test_expect_success \ + 'prepare work tree again' \ + 'mv COPYING.2 COPYING && + git-update-cache --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-cache -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' + +# make sure diff-helper can grok it. +mv current diff-raw +GIT_DIFF_OPTS=--unified=0 git-diff-helper -z <diff-raw >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 diff-helper (#2)' \ + 'compare_diff_patch 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-cache --add --remove COPYING COPYING.1' + +git-diff-cache -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' + +# make sure diff-helper can grok it. +mv current diff-raw +GIT_DIFF_OPTS=--unified=0 git-diff-helper -z <diff-raw >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 diff-helper (#3)' \ + 'compare_diff_patch current expected' + +test_done diff --git a/t/t4010-diff-pathspec.sh b/t/t4010-diff-pathspec.sh new file mode 100644 index 0000000000..9f2c6f6aa2 --- /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-cache --add file0 path1/file1 && + tree=`git-write-tree` && + echo "$tree" && + echo nitfol >file0 && + echo yomin >path1/file1 && + git-update-cache file0 path1/file1' + +cat >expected <<\EOF +EOF +test_expect_success \ + 'limit to path should show nothing' \ + 'git-diff-cache --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-cache --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-cache --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-cache --cached $tree file0 >current && + compare_diff_raw current expected' + +cat >expected <<\EOF +EOF +test_expect_success \ + 'limit to file0/ should emit nothing.' \ + 'git-diff-cache --cached $tree file0/ >current && + compare_diff_raw current expected' + +test_done diff --git a/t/t4100-apply-stat.sh b/t/t4100-apply-stat.sh new file mode 100644 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/t5000-tar-tree.sh b/t/t5000-tar-tree.sh new file mode 100644 index 0000000000..6d72ed34c1 --- /dev/null +++ b/t/t5000-tar-tree.sh @@ -0,0 +1,94 @@ +#!/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 pathes 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 + +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 && + (cd a && find .) | sort >a.lst' + +test_expect_success \ + 'add files to repository' \ + 'find a -type f | xargs git-update-cache --add && + find a -type l | xargs git-update-cache --add && + treeid=`git-write-tree` && + echo $treeid >treeid && + TZ= GIT_COMMITTER_DATE="2005-05-27 22:00:00" \ + git-commit-tree $treeid </dev/null >.git/HEAD' + +test_expect_success \ + 'git-tar-tree' \ + 'git-tar-tree HEAD >b.tar' + +test_expect_success \ + 'validate file modification time' \ + 'TZ= 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/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_done diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh new file mode 100755 index 0000000000..b0b9329573 --- /dev/null +++ b/t/t5300-pack-object.sh @@ -0,0 +1,168 @@ +#!/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-cache --add $i || exit + done && + cat c >d && echo foo >>d && git-update-cache --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 || exit 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-db && + 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. + exit 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-db && + 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. + exit 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-db && + 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 || exit 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 || exit 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 && + + 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 && + + 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 && + + 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 && + + :' + +test_done diff --git a/t/t6000lib.sh b/t/t6000lib.sh new file mode 100644 index 0000000000..01f796e9c8 --- /dev/null +++ b/t/t6000lib.sh @@ -0,0 +1,109 @@ +[ -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" + "$@" + export GIT_AUTHOR_EMAIL="$_save" +} + +commit_date() +{ + _commit=$1 + git-cat-file commit $_commit | sed -n "s/^committer .*> \([0-9]*\) .*/\1/p" +} + +on_committer_date() +{ + _date=$1 + shift 1 + GIT_COMMITTER_DATE=$_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-merge-order.sh b/t/t6001-rev-list-merge-order.sh new file mode 100755 index 0000000000..5dcb957ac0 --- /dev/null +++ b/t/t6001-rev-list-merge-order.sh @@ -0,0 +1,455 @@ +#!/bin/sh +# +# Copyright (c) 2005 Jon Seymour +# + +test_description='Tests git-rev-list --merge-order functionality' + +. ./test-lib.sh +. ../t6000lib.sh # t6xxx specific functions + +# test-case specific test function +check_adjacency() +{ + read previous + echo "= $previous" + while read next + do + if ! (git-cat-file commit $previous | grep "^parent $next" >/dev/null) + then + echo "^ $next" + else + echo "| $next" + fi + previous=$next + done +} + +list_duplicates() +{ + "$@" | sort | uniq -d +} + +grep_stderr() +{ + args=$1 + shift 1 + "$@" 2>&1 | grep "$args" +} + +date >path0 +git-update-cache --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 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 +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 + +tag l5 > .git/HEAD + +test_expect_success 'rev-list has correct number of entries' 'git-rev-list HEAD | wc -l | tr -s " "' <<EOF +19 +EOF + +normal_adjacency_count=$(git-rev-list HEAD | check_adjacency | grep -c "\^" | tr -d ' ') +merge_order_adjacency_count=$(git-rev-list --merge-order HEAD | check_adjacency | grep -c "\^" | tr -d ' ') +test_expect_success '--merge-order produces as many or fewer discontinuities' '[ $merge_order_adjacency_count -le $normal_adjacency_count ]' +test_output_expect_success 'simple merge order' 'git-rev-list --merge-order --show-breaks HEAD' <<EOF += l5 +| l4 +| l3 += a4 +| c3 +| c2 +| c1 +^ b4 +| b3 +| b2 +| b1 +^ a3 +| a2 +| a1 += a0 +| l2 +| l1 +| l0 += root +EOF + +test_output_expect_success 'two diamonds merge order (g6)' 'git-rev-list --merge-order --show-breaks g4' <<EOF += g4 +| h2 +^ g3 += g2 +| h1 +^ g1 += g0 +EOF + +test_output_expect_success 'multiple heads' 'git-rev-list --merge-order a3 b3 c3' <<EOF +c3 +c2 +c1 +b3 +b2 +b1 +a3 +a2 +a1 +a0 +l2 +l1 +l0 +root +EOF + +test_output_expect_success 'multiple heads, prune at a1' 'git-rev-list --merge-order a3 b3 c3 ^a1' <<EOF +c3 +c2 +c1 +b3 +b2 +b1 +a3 +a2 +EOF + +test_output_expect_success 'multiple heads, prune at l1' 'git-rev-list --merge-order a3 b3 c3 ^l1' <<EOF +c3 +c2 +c1 +b3 +b2 +b1 +a3 +a2 +a1 +a0 +l2 +EOF + +test_output_expect_success 'cross-epoch, head at l5, prune at l1' 'git-rev-list --merge-order l5 ^l1' <<EOF +l5 +l4 +l3 +a4 +c3 +c2 +c1 +b4 +b3 +b2 +b1 +a3 +a2 +a1 +a0 +l2 +EOF + +test_output_expect_success 'duplicated head arguments' 'git-rev-list --merge-order l5 l5 ^l1' <<EOF +l5 +l4 +l3 +a4 +c3 +c2 +c1 +b4 +b3 +b2 +b1 +a3 +a2 +a1 +a0 +l2 +EOF + +test_output_expect_success 'prune near merge' 'git-rev-list --merge-order a4 ^c3' <<EOF +a4 +b4 +b3 +a3 +a2 +a1 +EOF + +test_output_expect_success "head has no parent" 'git-rev-list --merge-order --show-breaks root' <<EOF += root +EOF + +test_output_expect_success "two nodes - one head, one base" 'git-rev-list --merge-order --show-breaks l0' <<EOF += l0 += root +EOF + +test_output_expect_success "three nodes one head, one internal, one base" 'git-rev-list --merge-order --show-breaks l1' <<EOF += l1 +| l0 += root +EOF + +test_output_expect_success "linear prune l2 ^root" 'git-rev-list --merge-order --show-breaks l2 ^root' <<EOF +^ l2 +| l1 +| l0 +EOF + +test_output_expect_success "linear prune l2 ^l0" 'git-rev-list --merge-order --show-breaks l2 ^l0' <<EOF +^ l2 +| l1 +EOF + +test_output_expect_success "linear prune l2 ^l1" 'git-rev-list --merge-order --show-breaks l2 ^l1' <<EOF +^ l2 +EOF + +test_output_expect_success "linear prune l5 ^a4" 'git-rev-list --merge-order --show-breaks l5 ^a4' <<EOF +^ l5 +| l4 +| l3 +EOF + +test_output_expect_success "linear prune l5 ^l3" 'git-rev-list --merge-order --show-breaks l5 ^l3' <<EOF +^ l5 +| l4 +EOF + +test_output_expect_success "linear prune l5 ^l4" 'git-rev-list --merge-order --show-breaks l5 ^l4' <<EOF +^ l5 +EOF + +test_output_expect_success "max-count 10 - merge order" 'git-rev-list --merge-order --show-breaks --max-count=10 l5' <<EOF += l5 +| l4 +| l3 += a4 +| c3 +| c2 +| c1 +^ b4 +| b3 +| b2 +EOF + +test_output_expect_success "max-count 10 - non merge 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 --merge-order' "git-rev-list --max-age=$(commit_date c3) l5" <<EOF +l5 +l4 +l3 +a4 +b4 +a3 +a2 +c3 +EOF + +test_output_expect_success '--max-age=c3, --merge-order' "git-rev-list --merge-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, --merge-order' "list_duplicates git-rev-list --merge-order a4 c3" <<EOF +EOF + +test_output_expect_success 'one specified head reachable from another c3, a4, --merge-order' "list_duplicates git-rev-list --merge-order c3 a4" <<EOF +EOF + +test_output_expect_success 'one specified head reachable from another a4, c3, no --merge-order' "list_duplicates git-rev-list a4 c3" <<EOF +EOF + +test_output_expect_success 'one specified head reachable from another c3, a4, no --merge-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 --merge-order" 'git-rev-list --merge-order --show-breaks a3 ^a3' <<EOF +EOF + +# +# can't test this now - duplicate parents can't be created +# +#test_output_expect_success 'duplicate parents' 'git-rev-list --parents --merge-order --show-breaks m3' <<EOF +#= m3 c3 a4 c3 +#| a4 c3 b4 a3 +#| b4 a3 b3 +#| b3 b2 +#^ a3 a2 +#| a2 a1 +#| a1 a0 +#^ c3 c2 +#| c2 b2 c1 +#| b2 b1 +#^ c1 b1 +#| b1 a0 +#= a0 l2 +#| l2 l1 +#| l1 l0 +#| l0 root +#= root +#EOF + +test_expect_success "head ^head no --merge-order" 'git-rev-list a3 ^a3' <<EOF +EOF + +test_output_expect_success 'simple merge order (l5r1)' 'git-rev-list --merge-order --show-breaks l5r1' <<EOF += l5r1 +| r1 +| r0 +| alt_root +^ l5 +| l4 +| l3 +| a4 +| c3 +| c2 +| c1 +^ b4 +| b3 +| b2 +| b1 +^ a3 +| a2 +| a1 +| a0 +| l2 +| l1 +| l0 += root +EOF + +test_output_expect_success 'simple merge order (r1l5)' 'git-rev-list --merge-order --show-breaks r1l5' <<EOF += r1l5 +| l5 +| l4 +| l3 +| a4 +| c3 +| c2 +| c1 +^ b4 +| b3 +| b2 +| b1 +^ a3 +| a2 +| a1 +| 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 --merge-order" <<EOF +a3 +a2 +a1 +EOF + +test_output_expect_success "--merge-order a4 l3" "git-rev-list --merge-order a4 l3" <<EOF +l3 +a4 +c3 +c2 +c1 +b4 +b3 +b2 +b1 +a3 +a2 +a1 +a0 +l2 +l1 +l0 +root +EOF + +# +# + +test_done diff --git a/t/t6002-rev-list-bisect.sh b/t/t6002-rev-list-bisect.sh new file mode 100755 index 0000000000..6a71820825 --- /dev/null +++ b/t/t6002-rev-list-bisect.sh @@ -0,0 +1,237 @@ +#!/bin/sh +# +# Copyright (c) 2005 Jon Seymour +# +test_description='Tests git-rev-list --bisect functionality' + +. ./test-lib.sh +. ../t6000lib.sh # t6xxx specific functions + +bc_expr() +{ +bc <<EOF +scale=1 +define abs(x) { if (x>=0) { return x; } else { return -x; } } +define floor(x) { save=scale; scale=0; result=x/1; scale=save; return result; } +$* +EOF +} + +# usage: test_bisection max-diff bisect-option head ^prune... +# +# e.g. test_bisection 1 --bisect l1 ^l0 +# +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_expect_success "bisection diff $_bisect_option $_head $* <= $_max_diff" "[ $(bc_expr "floor(abs($_list_size/2)-$_bisection_size)") -le $_max_diff ]" +} + +date >path0 +git-update-cache --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 +tag l5 > .git/HEAD + + +# 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 illustrate's 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..eb41f86a1b --- /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-cache --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 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 +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 + +tag l5 > .git/HEAD + +test_expect_success 'rev-list has correct number of entries' 'git-rev-list HEAD | wc -l | tr -s " "' <<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 +b3 +c3 +c2 +b2 +c1 +b1 +a3 +a2 +a1 +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 +b3 +c3 +c2 +b2 +c1 +b1 +a3 +a2 +EOF + +test_output_expect_success 'multiple heads, prune at l1' 'git-rev-list --topo-order a3 b3 c3 ^l1' <<EOF +b3 +c3 +c2 +b2 +c1 +b1 +a3 +a2 +a1 +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/test-lib.sh b/t/test-lib.sh new file mode 100755 index 0000000000..d3f71d1932 --- /dev/null +++ b/t/test-lib.sh @@ -0,0 +1,150 @@ +#!/bin/sh +# +# Copyright (c) 2005 Junio C Hamano +# + +# For repeatability, reset the environment to known value. +LANG=C +TZ=UTC +export LANG TZ +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 +unset GIT_AUTHOR_EMAIL +unset GIT_AUTHOR_NAME +unset GIT_COMMITTER_EMAIL +unset GIT_COMMITTER_NAME +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 + +# 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: $*" + 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 ;; + *) + break ;; + esac +done + +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 + + +# 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: $@" + test "$immediate" == "" || exit 1 +} + + +test_debug () { + test "$debug" == "" || eval "$1" +} + +test_expect_failure () { + test "$#" == 2 || + error "bug in the test script: not 2 parameters to test-expect-failure" + say >&3 "expecting failure: $2" + if eval >&3 2>&4 "$2" + then + test_failure_ "$@" + else + test_ok_ "$1" + fi +} + +test_expect_success () { + test "$#" == 2 || + error "bug in the test script: not 2 parameters to test-expect-success" + say >&3 "expecting success: $2" + if eval >&3 2>&4 "$2" + then + test_ok_ "$1" + else + test_failure_ "$@" + fi +} + +test_done () { + 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 + +# Test repository +test=trash +rm -fr "$test" +mkdir "$test" +cd "$test" +git-init-db 2>/dev/null || error "cannot run git-init-db" @@ -0,0 +1,93 @@ +#include "tag.h" +#include "cache.h" + +const char *tag_type = "tag"; + +struct tag *lookup_tag(const unsigned char *sha1) +{ + struct object *obj = lookup_object(sha1); + if (!obj) { + struct tag *ret = xmalloc(sizeof(struct tag)); + memset(ret, 0, sizeof(struct tag)); + created_object(sha1, &ret->object); + ret->object.type = tag_type; + return ret; + } + if (!obj->type) + obj->type = tag_type; + if (obj->type != tag_type) { + error("Object %s is a %s, not a tree", + sha1_to_hex(sha1), 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(data + 7, object)) + return -1; + + type_line = 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) + add_ref(&item->object, item->tagged); + + 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,19 @@ +#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); + +#endif /* TAG_H */ diff --git a/tar-tree.c b/tar-tree.c new file mode 100644 index 0000000000..673ac66ed6 --- /dev/null +++ b/tar-tree.c @@ -0,0 +1,439 @@ +/* + * Copyright (c) 2005 Rene Scharfe + */ +#include <time.h> +#include "cache.h" + +#define RECORDSIZE (512) +#define BLOCKSIZE (RECORDSIZE * 20) + +#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' + +#define EXT_HEADER_PATH 1 +#define EXT_HEADER_LINKPATH 2 + +static const char *tar_tree_usage = "git-tar-tree <key> [basedir]"; + +static char block[BLOCKSIZE]; +static unsigned long offset; + +static const char *basedir; +static time_t archive_time; + +struct path_prefix { + struct path_prefix *prev; + const char *name; +}; + +/* tries hard to write, either succeeds or dies in the attempt */ +static void reliable_write(void *buf, unsigned long size) +{ + while (size > 0) { + long ret = write(1, buf, size); + if (ret < 0) { + if (errno == EAGAIN) + continue; + if (errno == EPIPE) + exit(0); + die("git-tar-tree: %s", strerror(errno)); + } else if (!ret) { + die("git-tar-tree: disk full?"); + } + size -= ret; + buf += ret; + } +} + +/* writes out the whole block, but only if it is full */ +static void write_if_needed(void) +{ + if (offset == BLOCKSIZE) { + reliable_write(block, BLOCKSIZE); + offset = 0; + } +} + +/* acquire the next record from the buffer; user must call write_if_needed() */ +static char *get_record(void) +{ + char *p = block + offset; + memset(p, 0, RECORDSIZE); + offset += RECORDSIZE; + return p; +} + +/* + * The end of tar archives is marked by 1024 nul bytes and after that + * follows the rest of the block (if any). + */ +static void write_trailer(void) +{ + get_record(); + write_if_needed(); + get_record(); + write_if_needed(); + while (offset) { + get_record(); + write_if_needed(); + } +} + +/* + * queues up writes, so that all our write(2) calls write exactly one + * full block; pads writes to RECORDSIZE + */ +static void write_blocked(void *buf, unsigned long size) +{ + 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) { + reliable_write(buf, BLOCKSIZE); + size -= BLOCKSIZE; + buf += BLOCKSIZE; + } + if (size) { + memcpy(block + offset, buf, size); + buf += size; + offset += size; + } + tail = offset % RECORDSIZE; + if (tail) { + memset(block + offset, 0, RECORDSIZE - tail); + offset += RECORDSIZE - tail; + } + write_if_needed(); +} + +static void append_string(char **p, const char *s) +{ + unsigned int len = strlen(s); + memcpy(*p, s, len); + *p += len; +} + +static void append_char(char **p, char c) +{ + **p = c; + *p += 1; +} + +static void append_path_prefix(char **buffer, struct path_prefix *prefix) +{ + if (!prefix) + return; + append_path_prefix(buffer, prefix->prev); + append_string(buffer, prefix->name); + append_char(buffer, '/'); +} + +static unsigned int path_prefix_len(struct path_prefix *prefix) +{ + if (!prefix) + return 0; + return path_prefix_len(prefix->prev) + strlen(prefix->name) + 1; +} + +static void append_path(char **p, int is_dir, const char *basepath, + struct path_prefix *prefix, const char *path) +{ + if (basepath) { + append_string(p, basepath); + append_char(p, '/'); + } + append_path_prefix(p, prefix); + append_string(p, path); + if (is_dir) + append_char(p, '/'); +} + +static unsigned int path_len(int is_dir, const char *basepath, + struct path_prefix *prefix, const char *path) +{ + unsigned int len = 0; + if (basepath) + len += strlen(basepath) + 1; + len += path_prefix_len(prefix) + strlen(path); + if (is_dir) + len++; + return len; +} + +static void append_extended_header_prefix(char **p, unsigned int size, + const char *keyword) +{ + int len = sprintf(*p, "%u %s=", size, keyword); + *p += len; +} + +static unsigned int extended_header_len(const char *keyword, + unsigned int valuelen) +{ + /* "%u %s=%s\n" */ + unsigned int len = 1 + 1 + strlen(keyword) + 1 + valuelen + 1; + if (len > 9) + len++; + if (len > 99) + len++; + return len; +} + +static void append_extended_header(char **p, const char *keyword, + const char *value, unsigned int len) +{ + unsigned int size = extended_header_len(keyword, len); + append_extended_header_prefix(p, size, keyword); + memcpy(*p, value, len); + *p += len; + append_char(p, '\n'); +} + +static void write_header(const unsigned char *, char, const char *, struct path_prefix *, + const char *, unsigned int, void *, unsigned long); + +/* stores a pax extended header directly in the block buffer */ +static void write_extended_header(const char *headerfilename, int is_dir, + unsigned int flags, const char *basepath, + struct path_prefix *prefix, + const char *path, unsigned int namelen, + void *content, unsigned int contentsize) +{ + char *buffer, *p; + unsigned int pathlen, size, linkpathlen = 0; + + size = pathlen = extended_header_len("path", namelen); + if (flags & EXT_HEADER_LINKPATH) { + linkpathlen = extended_header_len("linkpath", contentsize); + size += linkpathlen; + } + write_header(NULL, TYPEFLAG_EXT_HEADER, NULL, NULL, headerfilename, + 0100600, NULL, size); + + buffer = p = malloc(size); + if (!buffer) + die("git-tar-tree: %s", strerror(errno)); + append_extended_header_prefix(&p, pathlen, "path"); + append_path(&p, is_dir, basepath, prefix, path); + append_char(&p, '\n'); + if (flags & EXT_HEADER_LINKPATH) + append_extended_header(&p, "linkpath", content, contentsize); + write_blocked(buffer, size); + free(buffer); +} + +static void write_global_extended_header(const unsigned char *sha1) +{ + char *p; + unsigned int size; + + size = extended_header_len("comment", 40); + write_header(NULL, TYPEFLAG_GLOBAL_HEADER, NULL, NULL, + "pax_global_header", 0100600, NULL, size); + + p = get_record(); + append_extended_header(&p, "comment", sha1_to_hex(sha1), 40); + write_if_needed(); +} + +/* stores a ustar header directly in the block buffer */ +static void write_header(const unsigned char *sha1, char typeflag, const char *basepath, + struct path_prefix *prefix, const char *path, + unsigned int mode, void *buffer, unsigned long size) +{ + unsigned int namelen; + char *header = NULL; + unsigned int checksum = 0; + int i; + unsigned int ext_header = 0; + + if (typeflag == TYPEFLAG_AUTO) { + if (S_ISDIR(mode)) + typeflag = TYPEFLAG_DIR; + else if (S_ISLNK(mode)) + typeflag = TYPEFLAG_LNK; + else + typeflag = TYPEFLAG_REG; + } + + namelen = path_len(S_ISDIR(mode), basepath, prefix, path); + if (namelen > 100) + ext_header |= EXT_HEADER_PATH; + if (typeflag == TYPEFLAG_LNK && size > 100) + ext_header |= EXT_HEADER_LINKPATH; + + /* the extended header must be written before the normal one */ + if (ext_header) { + char headerfilename[51]; + sprintf(headerfilename, "%s.paxheader", sha1_to_hex(sha1)); + write_extended_header(headerfilename, S_ISDIR(mode), + ext_header, basepath, prefix, path, + namelen, buffer, size); + } + + header = get_record(); + + if (ext_header) { + sprintf(header, "%s.data", sha1_to_hex(sha1)); + } else { + char *p = header; + append_path(&p, S_ISDIR(mode), basepath, prefix, path); + } + + if (typeflag == TYPEFLAG_LNK) { + if (ext_header & EXT_HEADER_LINKPATH) { + sprintf(&header[157], "see %s.paxheader", + sha1_to_hex(sha1)); + } else { + if (buffer) + strncpy(&header[157], buffer, size); + } + } + + if (S_ISDIR(mode)) + mode |= 0755; /* GIT doesn't store permissions of dirs */ + if (S_ISLNK(mode)) + mode |= 0777; /* ... nor of symlinks */ + sprintf(&header[100], "%07o", mode & 07777); + + /* XXX: should we provide more meaningful info here? */ + sprintf(&header[108], "%07o", 0); /* uid */ + sprintf(&header[116], "%07o", 0); /* gid */ + strncpy(&header[265], "git", 31); /* uname */ + strncpy(&header[297], "git", 31); /* gname */ + + if (S_ISDIR(mode) || S_ISLNK(mode)) + size = 0; + sprintf(&header[124], "%011lo", size); + sprintf(&header[136], "%011lo", archive_time); + + header[156] = typeflag; + + memcpy(&header[257], "ustar", 6); + memcpy(&header[263], "00", 2); + + printf(&header[329], "%07o", 0); /* devmajor */ + printf(&header[337], "%07o", 0); /* devminor */ + + memset(&header[148], ' ', 8); + for (i = 0; i < RECORDSIZE; i++) + checksum += header[i]; + sprintf(&header[148], "%07o", checksum & 0x1fffff); + + write_if_needed(); +} + +static void traverse_tree(void *buffer, unsigned long size, + struct path_prefix *prefix) +{ + struct path_prefix this_prefix; + this_prefix.prev = prefix; + + while (size) { + int namelen = strlen(buffer)+1; + void *eltbuf; + char elttype[20]; + unsigned long eltsize; + unsigned char *sha1 = buffer + namelen; + char *path = strchr(buffer, ' ') + 1; + unsigned int mode; + + if (size < namelen + 20 || sscanf(buffer, "%o", &mode) != 1) + die("corrupt 'tree' file"); + buffer = sha1 + 20; + size -= namelen + 20; + + eltbuf = read_sha1_file(sha1, elttype, &eltsize); + if (!eltbuf) + die("cannot read %s", sha1_to_hex(sha1)); + write_header(sha1, TYPEFLAG_AUTO, basedir, prefix, path, + mode, eltbuf, eltsize); + if (!strcmp(elttype, "tree")) { + this_prefix.name = path; + traverse_tree(eltbuf, eltsize, &this_prefix); + } else if (!strcmp(elttype, "blob") && !S_ISLNK(mode)) { + write_blocked(eltbuf, eltsize); + } + free(eltbuf); + } +} + +/* get commit time from committer line of commit object */ +static time_t commit_time(void * buffer, unsigned long size) +{ + time_t result = 0; + char *p = buffer; + + while (size > 0) { + char *endp = memchr(p, '\n', size); + if (!endp || endp == p) + break; + *endp = '\0'; + if (endp - p > 10 && !memcmp(p, "committer ", 10)) { + char *nump = strrchr(p, '>'); + if (!nump) + break; + nump++; + result = strtoul(nump, &endp, 10); + if (*endp != ' ') + result = 0; + break; + } + size -= endp - p - 1; + p = endp + 1; + } + return result; +} + +int main(int argc, char **argv) +{ + unsigned char sha1[20]; + unsigned char commit_sha1[20]; + void *buffer; + unsigned long size; + + switch (argc) { + case 3: + basedir = argv[2]; + /* FALLTHROUGH */ + case 2: + if (get_sha1(argv[1], sha1) < 0) + usage(tar_tree_usage); + break; + default: + usage(tar_tree_usage); + } + + buffer = read_object_with_reference(sha1, "commit", &size, commit_sha1); + if (buffer) { + write_global_extended_header(commit_sha1); + archive_time = commit_time(buffer, size); + free(buffer); + } + buffer = read_object_with_reference(sha1, "tree", &size, NULL); + if (!buffer) + die("not a reference to a tag, commit or tree object: %s", + sha1_to_hex(sha1)); + if (!archive_time) + archive_time = time(NULL); + if (basedir) + write_header((unsigned char *)"0", TYPEFLAG_DIR, NULL, NULL, + basedir, 040755, NULL, 0); + traverse_tree(buffer, size, NULL); + free(buffer); + write_trailer(); + return 0; +} diff --git a/test-date.c b/test-date.c new file mode 100644 index 0000000000..6fe3e28b9d --- /dev/null +++ b/test-date.c @@ -0,0 +1,20 @@ +#include <stdio.h> +#include <time.h> + +#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)); + } + return 0; +} diff --git a/test-delta.c b/test-delta.c new file mode 100644 index 0000000000..37ef86b283 --- /dev/null +++ b/test-delta.c @@ -0,0 +1,81 @@ +/* + * 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 <stdio.h> +#include <unistd.h> +#include <string.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/mman.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]); + 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]); + 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(fd, out_buf, out_size) != out_size) { + perror(argv[4]); + return 1; + } + + return 0; +} diff --git a/tools/Makefile b/tools/Makefile new file mode 100644 index 0000000000..8e7252e592 --- /dev/null +++ b/tools/Makefile @@ -0,0 +1,14 @@ +CC=gcc +CFLAGS=-Wall -O2 +HOME=$(shell echo $$HOME) + +PROGRAMS=mailsplit mailinfo +SCRIPTS=applymbox applypatch + +all: $(PROGRAMS) + +install: $(PROGRAMS) $(SCRIPTS) + cp -f $(PROGRAMS) $(SCRIPTS) $(HOME)/bin/ + +clean: + rm -f $(PROGRAMS) *.o diff --git a/tools/applymbox b/tools/applymbox new file mode 100755 index 0000000000..5ac8d2b4da --- /dev/null +++ b/tools/applymbox @@ -0,0 +1,35 @@ +#!/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" +## +## dotest [ -q ] mail_archive [Signoff_file] +## +rm -rf .dotest +mkdir .dotest +case $1 in + + -q) touch .dotest/.query_apply + shift;; +esac +mailsplit $1 .dotest || exit 1 +for i in .dotest/* +do + mailinfo .dotest/msg .dotest/patch < $i > .dotest/info || exit 1 + git-stripspace < .dotest/msg > .dotest/msg-clean + applypatch .dotest/msg-clean .dotest/patch .dotest/info "$2" + ret=$? + if [ $ret -ne 0 ]; then + # 2 is a special exit code from applypatch to indicate that + # the patch wasn't applied, but continue anyway + [ $ret -ne 2 ] && exit $ret + fi +done +# return to pristine +rm -fr .dotest diff --git a/tools/applypatch b/tools/applypatch new file mode 100755 index 0000000000..5a3a44b0e6 --- /dev/null +++ b/tools/applypatch @@ -0,0 +1,64 @@ +#!/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 +## +signoff="$4" +final=.dotest/final-commit +## +## If this file exists, we ask before applying +## +query_apply=.dotest/.query_apply +MSGFILE=$1 +PATCHFILE=$2 +INFO=$3 +EDIT=${VISUAL:-$EDITOR} +EDIT=${EDIT:-vi} + +export GIT_AUTHOR_NAME="$(sed -n '/^Author/ s/Author: //p' .dotest/info)" +export GIT_AUTHOR_EMAIL="$(sed -n '/^Email/ s/Email: //p' .dotest/info)" +export GIT_AUTHOR_DATE="$(sed -n '/^Date/ s/Date: //p' .dotest/info)" +export SUBJECT="$(sed -n '/^Subject/ s/Subject: //p' .dotest/info)" + +if [ -n "$signoff" -a -f "$signoff" ]; then + cat $signoff >> $MSGFILE +fi + +(echo "[PATCH] $SUBJECT" ; if [ -s $MSGFILE ]; then echo ; cat $MSGFILE; fi ) > $final + +f=0 +[ -f $query_apply ] || f=1 + +while [ $f -eq 0 ]; do + echo "Commit Body is:" + echo "--------------------------" + cat $final + echo "--------------------------" + echo -n "Apply? [y]es/[n]o/[e]dit/[a]ccept all " + read reply + case $reply in + y|Y) f=1;; + n|N) exit 2;; # special value to tell dotest to keep going + e|E) $EDIT $final;; + a|A) rm -f $query_apply + f=1;; + esac +done + +echo +echo Applying "'$SUBJECT'" +echo + +git-apply --index $PATCHFILE || exit 1 +tree=$(git-write-tree) || exit 1 +echo Wrote tree $tree +commit=$(git-commit-tree $tree -p $(cat .git/HEAD) < $final) || exit 1 +echo Committed: $commit +echo $commit > .git/HEAD diff --git a/tools/mailinfo.c b/tools/mailinfo.c new file mode 100644 index 0000000000..ae279bffa5 --- /dev/null +++ b/tools/mailinfo.c @@ -0,0 +1,270 @@ +/* + * Another stupid program, this one parsing the headers of an + * email to figure out authorship and subject + */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +static FILE *cmitmsg, *patchfile; + +static char line[1000]; +static char date[1000]; +static char name[1000]; +static char email[1000]; +static char subject[1000]; + +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 handle_from(char *line) +{ + char *at = strchr(line, '@'); + char *dst; + + if (!at) + return 0; + + /* + * If we already have one email, don't take any confusing lines + */ + if (*email && strchr(at+1, '@')) + return 0; + + while (at > line) { + char c = at[-1]; + if (isspace(c) || c == '<') + break; + at--; + } + dst = email; + for (;;) { + unsigned char c = *at; + if (!c || c == '>' || isspace(c)) + break; + *at++ = ' '; + *dst++ = c; + } + *dst++ = 0; + + at = line + strlen(line); + while (at > line) { + unsigned char c = *--at; + if (isalnum(c)) + break; + *at = 0; + } + + at = line; + for (;;) { + unsigned char c = *at; + if (!c) + break; + if (isalnum(c)) + break; + at++; + } + + at = sanity_check(at, email); + + strcpy(name, at); + return 1; +} + +static void handle_date(char *line) +{ + strcpy(date, line); +} + +static void handle_subject(char *line) +{ + strcpy(subject, line); +} + +static void add_subject_line(char *line) +{ + while (isspace(*line)) + line++; + *--line = ' '; + strcat(subject, line); +} + +static void check_line(char *line, int len) +{ + static int cont = -1; + if (!memcmp(line, "From:", 5) && isspace(line[5])) { + handle_from(line+6); + cont = 0; + return; + } + if (!memcmp(line, "Date:", 5) && isspace(line[5])) { + handle_date(line+6); + cont = 0; + return; + } + if (!memcmp(line, "Subject:", 8) && isspace(line[8])) { + handle_subject(line+9); + cont = 1; + return; + } + if (isspace(*line)) { + switch (cont) { + case 0: + fprintf(stderr, "I don't do 'Date:' or 'From:' line continuations\n"); + break; + case 1: + add_subject_line(line); + return; + default: + break; + } + } + cont = -1; +} + +static char * cleanup_subject(char *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; + } + 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 handle_rest(void) +{ + char *sub = cleanup_subject(subject); + cleanup_space(name); + cleanup_space(date); + cleanup_space(email); + cleanup_space(sub); + printf("Author: %s\nEmail: %s\nSubject: %s\nDate: %s\n\n", name, email, sub, date); + FILE *out = cmitmsg; + + do { + if (!memcmp("diff -", line, 6) || + !memcmp("---", line, 3) || + !memcmp("Index: ", line, 7)) + out = patchfile; + + fputs(line, out); + } while (fgets(line, sizeof(line), stdin) != NULL); + + if (out == cmitmsg) { + fprintf(stderr, "No patch found\n"); + exit(1); + } + + fclose(cmitmsg); + fclose(patchfile); +} + +static int eatspace(char *line) +{ + int len = strlen(line); + while (len > 0 && isspace(line[len-1])) + line[--len] = 0; + return len; +} + +static void handle_body(void) +{ + int has_from = 0; + + /* First line of body can be a From: */ + while (fgets(line, sizeof(line), stdin) != NULL) { + int len = eatspace(line); + if (!len) + continue; + if (!memcmp("From:", line, 5) && isspace(line[5])) { + if (!has_from && handle_from(line+6)) { + has_from = 1; + continue; + } + } + line[len] = '\n'; + handle_rest(); + break; + } +} + +static void usage(void) +{ + fprintf(stderr, "mailinfo msg-file path-file < email\n"); + exit(1); +} + +int main(int argc, char ** argv) +{ + if (argc != 3) + usage(); + cmitmsg = fopen(argv[1], "w"); + if (!cmitmsg) { + perror(argv[1]); + exit(1); + } + patchfile = fopen(argv[2], "w"); + if (!patchfile) { + perror(argv[2]); + exit(1); + } + while (fgets(line, sizeof(line), stdin) != NULL) { + int len = eatspace(line); + if (!len) { + handle_body(); + break; + } + check_line(line, len); + } + return 0; +} diff --git a/tools/mailsplit.c b/tools/mailsplit.c new file mode 100644 index 0000000000..9379fbc5e8 --- /dev/null +++ b/tools/mailsplit.c @@ -0,0 +1,144 @@ +/* + * 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 <unistd.h> +#include <stdlib.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/mman.h> +#include <string.h> +#include <stdio.h> +#include <ctype.h> +#include <assert.h> + +static int usage(void) +{ + fprintf(stderr, "mailsplit <mbox> <directory>\n"); + exit(1); +} + +static int linelen(const char *map, unsigned long size) +{ + int len = 0, c; + + do { + c = *map; + map++; + size--; + len++; + } while (size && c != '\n'); + return len; +} + +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; +} + +static int parse_email(const void *map, unsigned long size) +{ + unsigned long offset; + + if (size < 6 || memcmp("From ", map, 5)) + goto corrupt; + + /* Make sure we don't trigger on this first line */ + map++; size--; offset=1; + + /* + * Search for a line beginning with "From ", and + * having smething that looks like a date format. + */ + do { + int len = linelen(map, size); + if (is_from_line(map, len)) + return offset; + map += len; + size -= len; + offset += len; + } while (size); + return offset; + +corrupt: + fprintf(stderr, "corrupt mailbox\n"); + exit(1); +} + +int main(int argc, char **argv) +{ + int fd, nr; + struct stat st; + unsigned long size; + void *map; + + if (argc != 3) + usage(); + fd = open(argv[1], O_RDONLY); + if (fd < 0) { + perror(argv[1]); + exit(1); + } + if (chdir(argv[2]) < 0) + usage(); + if (fstat(fd, &st) < 0) { + perror("stat"); + exit(1); + } + size = st.st_size; + map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (-1 == (int)(long)map) { + perror("mmap"); + exit(1); + } + close(fd); + nr = 0; + do { + char name[10]; + unsigned long len = parse_email(map, size); + assert(len <= size); + sprintf(name, "%04d", ++nr); + fd = open(name, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + perror(name); + exit(1); + } + if (write(fd, map, len) != len) { + perror("write"); + exit(1); + } + close(fd); + map += len; + size -= len; + } while (size > 0); + return 0; +} diff --git a/tree.c b/tree.c new file mode 100644 index 0000000000..dabc2e7a2c --- /dev/null +++ b/tree.c @@ -0,0 +1,213 @@ +#include "tree.h" +#include "blob.h" +#include "cache.h" +#include <stdlib.h> + +const char *tree_type = "tree"; + +static int read_one_entry(unsigned char *sha1, const char *base, int baselen, const char *pathname, unsigned mode, int stage) +{ + int len = strlen(pathname); + unsigned int size = cache_entry_size(baselen + len); + struct cache_entry *ce = xmalloc(size); + + memset(ce, 0, 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); + memcpy(ce->sha1, sha1, 20); + 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; +} + +static int read_tree_recursive(void *buffer, unsigned long size, + const char *base, int baselen, + int stage, const char **match) +{ + while (size) { + int len = strlen(buffer)+1; + unsigned char *sha1 = buffer + len; + char *path = strchr(buffer, ' ')+1; + unsigned int mode; + + if (size < len + 20 || sscanf(buffer, "%o", &mode) != 1) + return -1; + + buffer = sha1 + 20; + size -= len + 20; + + if (!match_tree_entry(base, baselen, path, mode, match)) + continue; + + if (S_ISDIR(mode)) { + int retval; + int pathlen = strlen(path); + char *newbase; + void *eltbuf; + char elttype[20]; + unsigned long eltsize; + + eltbuf = read_sha1_file(sha1, elttype, &eltsize); + if (!eltbuf || strcmp(elttype, "tree")) { + if (eltbuf) free(eltbuf); + return -1; + } + newbase = xmalloc(baselen + 1 + pathlen); + memcpy(newbase, base, baselen); + memcpy(newbase + baselen, path, pathlen); + newbase[baselen + pathlen] = '/'; + retval = read_tree_recursive(eltbuf, eltsize, + newbase, + baselen + pathlen + 1, + stage, match); + free(eltbuf); + free(newbase); + if (retval) + return -1; + continue; + } + if (read_one_entry(sha1, base, baselen, path, mode, stage) < 0) + return -1; + } + return 0; +} + +int read_tree(void *buffer, unsigned long size, int stage, const char **match) +{ + return read_tree_recursive(buffer, size, "", 0, stage, match); +} + +struct tree *lookup_tree(const unsigned char *sha1) +{ + struct object *obj = lookup_object(sha1); + if (!obj) { + struct tree *ret = xmalloc(sizeof(struct tree)); + memset(ret, 0, sizeof(struct tree)); + created_object(sha1, &ret->object); + ret->object.type = tree_type; + return ret; + } + if (!obj->type) + obj->type = tree_type; + if (obj->type != tree_type) { + error("Object %s is a %s, not a tree", + sha1_to_hex(sha1), obj->type); + return NULL; + } + return (struct tree *) obj; +} + +int parse_tree_buffer(struct tree *item, void *buffer, unsigned long size) +{ + void *bufptr = buffer; + struct tree_entry_list **list_p; + + if (item->object.parsed) + return 0; + item->object.parsed = 1; + list_p = &item->entries; + while (size) { + struct object *obj; + struct tree_entry_list *entry; + int len = 1+strlen(bufptr); + unsigned char *file_sha1 = bufptr + len; + char *path = strchr(bufptr, ' '); + unsigned int mode; + if (size < len + 20 || !path || + sscanf(bufptr, "%o", &mode) != 1) + return -1; + + entry = xmalloc(sizeof(struct tree_entry_list)); + entry->name = strdup(path + 1); + entry->directory = S_ISDIR(mode) != 0; + entry->executable = (mode & S_IXUSR) != 0; + entry->symlink = S_ISLNK(mode) != 0; + entry->mode = mode; + entry->next = NULL; + + bufptr += len + 20; + size -= len + 20; + + if (entry->directory) { + entry->item.tree = lookup_tree(file_sha1); + obj = &entry->item.tree->object; + } else { + entry->item.blob = lookup_blob(file_sha1); + obj = &entry->item.blob->object; + } + if (obj) + add_ref(&item->object, obj); + entry->parent = NULL; /* needs to be filled by the user */ + *list_p = entry; + list_p = &entry->next; + } + return 0; +} + +int parse_tree(struct tree *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, tree_type)) { + free(buffer); + return error("Object %s not a tree", + sha1_to_hex(item->object.sha1)); + } + ret = parse_tree_buffer(item, buffer, size); + free(buffer); + return ret; +} diff --git a/tree.h b/tree.h new file mode 100644 index 0000000000..74fe09d763 --- /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_entry_list { + struct tree_entry_list *next; + unsigned directory : 1; + unsigned executable : 1; + unsigned symlink : 1; + unsigned int mode; + char *name; + union { + struct tree *tree; + struct blob *blob; + } item; + struct tree_entry_list *parent; +}; + +struct tree { + struct object object; + struct tree_entry_list *entries; +}; + +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); + +#endif /* TREE_H */ diff --git a/unpack-file.c b/unpack-file.c new file mode 100644 index 0000000000..d4ac3a5460 --- /dev/null +++ b/unpack-file.c @@ -0,0 +1,34 @@ +#include "cache.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")) + 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(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 || get_sha1(argv[1], sha1)) + usage("git-unpack-file <sha1>"); + + puts(create_temp_file(sha1)); + return 0; +} diff --git a/unpack-objects.c b/unpack-objects.c new file mode 100644 index 0000000000..19874d004a --- /dev/null +++ b/unpack-objects.c @@ -0,0 +1,312 @@ +#include "cache.h" +#include "object.h" +#include "delta.h" +#include "pack.h" + +#include <sys/time.h> + +static int dry_run, quiet; +static const char unpack_usage[] = "git-unpack-objects < pack-file"; + +/* We always read in 4kB chunks. */ +static unsigned char buffer[4096]; +static unsigned long offset, len, eof; +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 (eof) + die("unable to fill input"); + if (min > sizeof(buffer)) + die("cannot fill %d bytes", min); + if (offset) { + SHA1_Update(&ctx, buffer, offset); + memcpy(buffer, buffer + offset, len); + offset = 0; + } + do { + int ret = read(0, buffer + len, sizeof(buffer) - len); + if (ret <= 0) { + if (!ret) + die("early EOF"); + if (errno == EAGAIN || errno == EINTR) + continue; + 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; +} + +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) + die("inflate returned %d\n", ret); + stream.next_in = fill(1); + stream.avail_in = len; + } + return buf; +} + +struct delta_info { + unsigned char base_sha1[20]; + unsigned long size; + void *delta; + struct delta_info *next; +}; + +static struct delta_info *delta_list; + +static void add_delta_to_list(unsigned char *base_sha1, void *delta, unsigned long size) +{ + struct delta_info *info = xmalloc(sizeof(*info)); + + memcpy(info->base_sha1, base_sha1, 20); + info->size = size; + info->delta = delta; + info->next = delta_list; + delta_list = info; +} + +static void added_object(unsigned char *sha1, const char *type, void *data, unsigned long size); + +static void write_object(void *buf, unsigned long size, const char *type) +{ + unsigned char sha1[20]; + if (write_sha1_file(buf, size, type, sha1) < 0) + die("failed to write object"); + added_object(sha1, type, buf, size); +} + +static int resolve_delta(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(result, result_size, type); + free(result); + return 0; +} + +static void added_object(unsigned char *sha1, const char *type, void *data, unsigned long size) +{ + struct delta_info **p = &delta_list; + struct delta_info *info; + + while ((info = *p) != NULL) { + if (!memcmp(info->base_sha1, sha1, 20)) { + *p = info->next; + p = &delta_list; + resolve_delta(type, data, size, info->delta, info->size); + free(info); + continue; + } + p = &info->next; + } +} + +static int unpack_non_delta_entry(enum object_type kind, unsigned long size) +{ + void *buf = get_data(size); + const char *type; + + switch (kind) { + case OBJ_COMMIT: type = "commit"; break; + case OBJ_TREE: type = "tree"; break; + case OBJ_BLOB: type = "blob"; break; + case OBJ_TAG: type = "tag"; break; + default: die("bad type %d", kind); + } + if (!dry_run) + write_object(buf, size, type); + free(buf); + return 0; +} + +static int unpack_delta_entry(unsigned long delta_size) +{ + void *delta_data, *base; + unsigned long base_size; + char type[20]; + unsigned char base_sha1[20]; + + memcpy(base_sha1, fill(20), 20); + use(20); + + delta_data = get_data(delta_size); + if (dry_run) { + free(delta_data); + return 0; + } + + if (!has_sha1_file(base_sha1)) { + add_delta_to_list(base_sha1, delta_data, delta_size); + return 0; + } + base = read_sha1_file(base_sha1, type, &base_size); + if (!base) + die("failed to read delta-pack base object %s", sha1_to_hex(base_sha1)); + return resolve_delta(type, base, base_size, delta_data, delta_size); +} + +static void unpack_one(unsigned nr, unsigned total) +{ + unsigned shift; + unsigned char *pack, c; + unsigned long size; + enum object_type type; + + 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 * 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, total); + } + } + switch (type) { + case OBJ_COMMIT: + case OBJ_TREE: + case OBJ_BLOB: + case OBJ_TAG: + unpack_non_delta_entry(type, size); + return; + case OBJ_DELTA: + unpack_delta_entry(size); + return; + default: + die("bad object type %d", type); + } +} + +/* + * We unpack from the end, older files first. Now, usually + * there are deltas etc, so we'll not actually write the + * objects in that order, but we might as well try.. + */ +static void unpack_all(void) +{ + int i; + struct pack_header *hdr = fill(sizeof(struct pack_header)); + unsigned version = ntohl(hdr->hdr_version); + unsigned nr_objects = ntohl(hdr->hdr_entries); + + if (ntohl(hdr->hdr_signature) != PACK_SIGNATURE) + die("bad pack file"); + if (version != PACK_VERSION) + die("unable to handle pack file version %d", version); + fprintf(stderr, "Unpacking %d objects\n", nr_objects); + + use(sizeof(struct pack_header)); + for (i = 0; i < nr_objects; i++) + unpack_one(i+1, nr_objects); + if (delta_list) + die("unresolved deltas left after unpacking"); +} + +int main(int argc, char **argv) +{ + int i; + unsigned char sha1[20]; + + 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; + } + 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 (memcmp(fill(20), sha1, 20)) + die("final sha1 did not match"); + use(20); + + /* Write the last part of the buffer to stdout */ + while (len) { + int ret = write(1, buffer + offset, len); + if (!ret) + break; + if (ret < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + break; + } + len -= ret; + offset += ret; + } + + /* All done */ + if (!quiet) + fprintf(stderr, "\n"); + return 0; +} diff --git a/update-cache.c b/update-cache.c new file mode 100644 index 0000000000..1fcc59a9c0 --- /dev/null +++ b/update-cache.c @@ -0,0 +1,401 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.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-cache *" and suddenly having all the object + * files be revision controlled. + */ +static int allow_add = 0, allow_remove = 0, allow_replace = 0, not_new = 0, quiet = 0, info_only = 0; +static int force_remove; + +/* Three functions to allow overloaded pointer return; see linux/err.h */ +static inline void *ERR_PTR(long error) +{ + return (void *) error; +} + +static inline long PTR_ERR(const void *ptr) +{ + return (long) ptr; +} + +static inline long IS_ERR(const void *ptr) +{ + return (unsigned long)ptr > (unsigned long)-1000L; +} + +static int add_file_to_cache(char *path) +{ + int size, namelen, option, status; + struct cache_entry *ce; + struct stat st; + int fd; + char *target; + + status = lstat(path, &st); + 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-cache --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) + return remove_file_from_cache(path); + } + return error("open(\"%s\"): %s", path, strerror(errno)); + } + namelen = strlen(path); + size = cache_entry_size(namelen); + ce = xmalloc(size); + memset(ce, 0, size); + memcpy(ce->name, path, namelen); + fill_stat_cache_info(ce, &st); + ce->ce_mode = create_ce_mode(st.st_mode); + ce->ce_flags = htons(namelen); + switch (st.st_mode & S_IFMT) { + case S_IFREG: + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + if (index_fd(ce->sha1, fd, &st, !info_only, NULL) < 0) + return -1; + break; + case S_IFLNK: + target = xmalloc(st.st_size+1); + if (readlink(path, target, st.st_size+1) != st.st_size) { + free(target); + return -1; + } + if (info_only) { + unsigned char hdr[50]; + int hdrlen; + write_sha1_file_prepare(target, st.st_size, "blob", + ce->sha1, hdr, &hdrlen); + } else if (write_sha1_file(target, st.st_size, "blob", ce->sha1)) + return -1; + free(target); + break; + default: + return -1; + } + option = allow_add ? ADD_CACHE_OK_TO_ADD : 0; + option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0; + return add_cache_entry(ce, option); +} + +static int 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 = memcmp(sha1, ce->sha1, 20); + close(fd); + } + return match; +} + +static int 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; +} + +/* + * "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. + */ +static struct cache_entry *refresh_entry(struct cache_entry *ce) +{ + struct stat st; + struct cache_entry *updated; + int changed, size; + + if (lstat(ce->name, &st) < 0) + return ERR_PTR(-errno); + + changed = ce_match_stat(ce, &st); + if (!changed) + return ce; + + /* + * 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 ERR_PTR(-EINVAL); + + switch (st.st_mode & S_IFMT) { + case S_IFREG: + if (compare_data(ce, &st)) + return ERR_PTR(-EINVAL); + break; + case S_IFLNK: + if (compare_link(ce, st.st_size)) + return ERR_PTR(-EINVAL); + break; + default: + return ERR_PTR(-EINVAL); + } + + size = ce_size(ce); + updated = xmalloc(size); + memcpy(updated, ce, size); + fill_stat_cache_info(updated, &st); + return updated; +} + +static int refresh_cache(void) +{ + int i; + int has_errors = 0; + + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce, *new; + ce = active_cache[i]; + if (ce_stage(ce)) { + printf("%s: needs merge\n", ce->name); + has_errors = 1; + while ((i < active_nr) && + ! strcmp(active_cache[i]->name, ce->name)) + i++; + i--; + continue; + } + + new = refresh_entry(ce); + if (IS_ERR(new)) { + if (not_new && PTR_ERR(new) == -ENOENT) + continue; + 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; +} + +/* + * 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; +} + +static int verify_path(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++; + } +} + +static int add_cacheinfo(char *arg1, char *arg2, char *arg3) +{ + int size, len, option; + unsigned int mode; + unsigned char sha1[20]; + struct cache_entry *ce; + + if (sscanf(arg1, "%o", &mode) != 1) + return -1; + if (get_sha1_hex(arg2, sha1)) + return -1; + if (!verify_path(arg3)) + return -1; + + len = strlen(arg3); + size = cache_entry_size(len); + ce = xmalloc(size); + memset(ce, 0, size); + + memcpy(ce->sha1, sha1, 20); + memcpy(ce->name, arg3, len); + ce->ce_flags = htons(len); + ce->ce_mode = create_ce_mode(mode); + option = allow_add ? ADD_CACHE_OK_TO_ADD : 0; + option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0; + return add_cache_entry(ce, option); +} + +static struct cache_file cache_file; + +int main(int argc, char **argv) +{ + int i, newfd, entries, has_errors = 0; + int allow_options = 1; + + newfd = hold_index_file_for_update(&cache_file, get_index_file()); + if (newfd < 0) + die("unable to create new cachefile"); + + entries = read_cache(); + if (entries < 0) + die("cache corrupted"); + + for (i = 1 ; i < argc; i++) { + char *path = argv[i]; + + if (allow_options && *path == '-') { + if (!strcmp(path, "--")) { + allow_options = 0; + continue; + } + if (!strcmp(path, "-q")) { + quiet = 1; + 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, "--refresh")) { + has_errors |= refresh_cache(); + continue; + } + if (!strcmp(path, "--cacheinfo")) { + if (i+3 >= argc) + die("git-update-cache: --cacheinfo <mode> <sha1> <path>"); + if (add_cacheinfo(argv[i+1], argv[i+2], argv[i+3])) + die("git-update-cache: --cacheinfo cannot add %s", argv[i+3]); + i += 3; + continue; + } + if (!strcmp(path, "--info-only")) { + info_only = 1; + continue; + } + if (!strcmp(path, "--force-remove")) { + force_remove = 1; + continue; + } + + if (!strcmp(path, "--ignore-missing")) { + not_new = 1; + continue; + } + die("unknown option %s", path); + } + if (!verify_path(path)) { + fprintf(stderr, "Ignoring path %s\n", argv[i]); + continue; + } + if (force_remove) { + if (remove_file_from_cache(path)) + die("git-update-cache: --force-remove cannot remove %s", path); + continue; + } + if (add_file_to_cache(path)) + die("Unable to add %s to database", path); + } + if (write_cache(newfd, active_cache, active_nr) || + commit_index_file(&cache_file)) + die("Unable to write new cachefile"); + + return has_errors ? 1 : 0; +} diff --git a/upload-pack.c b/upload-pack.c new file mode 100644 index 0000000000..95d3812051 --- /dev/null +++ b/upload-pack.c @@ -0,0 +1,185 @@ +#include "cache.h" +#include "refs.h" +#include "pkt-line.h" + +static const char upload_pack_usage[] = "git-upload-pack <dir>"; + +#define MAX_HAS (16) +#define MAX_NEEDS (256) +static int nr_has = 0, nr_needs = 0; +static unsigned char has_sha1[MAX_HAS][20]; +static unsigned char needs_sha1[MAX_NEEDS][20]; + +static int strip(char *line, int len) +{ + if (len && line[len-1] == '\n') + line[--len] = 0; + return len; +} + +static void create_pack_file(void) +{ + int fd[2]; + pid_t pid; + + if (pipe(fd) < 0) + die("git-upload-pack: unable to create pipe"); + pid = fork(); + if (pid < 0) + die("git-upload-pack: unable to fork git-rev-list"); + + if (!pid) { + int i; + int args = nr_has + nr_needs + 5; + char **argv = xmalloc(args * sizeof(char *)); + char *buf = xmalloc(args * 45); + char **p = argv; + + dup2(fd[1], 1); + close(0); + close(fd[0]); + close(fd[1]); + *p++ = "git-rev-list"; + *p++ = "--objects"; + for (i = 0; i < nr_needs; i++) { + *p++ = buf; + memcpy(buf, sha1_to_hex(needs_sha1[i]), 41); + buf += 41; + } + for (i = 0; i < nr_has; i++) { + *p++ = buf; + *buf++ = '^'; + memcpy(buf, sha1_to_hex(has_sha1[i]), 41); + buf += 41; + } + *p++ = NULL; + execvp("git-rev-list", argv); + die("git-upload-pack: unable to exec git-rev-list"); + } + dup2(fd[0], 0); + close(fd[0]); + close(fd[1]); + execlp("git-pack-objects", "git-pack-objects", "--stdout", NULL); + die("git-upload-pack: unable to exec git-pack-objects"); +} + +static int got_sha1(char *hex, unsigned char *sha1) +{ + int nr; + if (get_sha1_hex(hex, sha1)) + die("git-upload-pack: expected SHA1 object, got '%s'", hex); + if (!has_sha1_file(sha1)) + return 0; + nr = nr_has; + if (nr < MAX_HAS) { + memcpy(has_sha1[nr], sha1, 20); + nr_has = nr+1; + } + return 1; +} + +static int get_common_commits(void) +{ + static char line[1000]; + unsigned char sha1[20]; + int len; + + for(;;) { + len = packet_read_line(0, line, sizeof(line)); + + if (!len) { + packet_write(1, "NAK\n"); + continue; + } + len = strip(line, len); + if (!strncmp(line, "have ", 5)) { + if (got_sha1(line+5, sha1)) { + packet_write(1, "ACK %s\n", sha1_to_hex(sha1)); + break; + } + continue; + } + if (!strcmp(line, "done")) { + packet_write(1, "NAK\n"); + return -1; + } + die("git-upload-pack: expected SHA1 list, got '%s'", line); + } + + for (;;) { + len = packet_read_line(0, line, sizeof(line)); + if (!len) + continue; + len = strip(line, len); + if (!strncmp(line, "have ", 5)) { + got_sha1(line+5, sha1); + continue; + } + if (!strcmp(line, "done")) + break; + die("git-upload-pack: expected SHA1 list, got '%s'", line); + } + return 0; +} + +static int receive_needs(void) +{ + static char line[1000]; + int len, needs; + + needs = 0; + for (;;) { + len = packet_read_line(0, line, sizeof(line)); + if (!len) + return needs; + + /* + * This is purely theoretical right now: git-fetch-pack only + * ever asks for a single HEAD + */ + if (needs >= MAX_NEEDS) + die("I'm only doing a max of %d requests", MAX_NEEDS); + if (strncmp("want ", line, 5) || get_sha1_hex(line+5, needs_sha1[needs])) + die("git-upload-pack: protocol error, expected to get sha, not '%s'", line); + needs++; + } +} + +static int send_ref(const char *refname, const unsigned char *sha1) +{ + packet_write(1, "%s %s\n", sha1_to_hex(sha1), refname); + return 0; +} + +static int upload_pack(void) +{ + head_ref(send_ref); + for_each_ref(send_ref); + packet_flush(1); + nr_needs = receive_needs(); + if (!nr_needs) + return 0; + get_common_commits(); + create_pack_file(); + return 0; +} + +int main(int argc, char **argv) +{ + const char *dir; + if (argc != 2) + usage(upload_pack_usage); + dir = argv[1]; + + /* chdir to the directory. If that fails, try appending ".git" */ + if (chdir(dir) < 0) { + if (chdir(mkpath("%s.git", dir)) < 0) + die("git-upload-pack unable to chdir to %s", dir); + } + chdir(".git"); + if (access("objects", X_OK) || access("refs", X_OK)) + die("git-upload-pack: %s doesn't seem to be a git archive", dir); + setenv("GIT_DIR", ".", 1); + upload_pack(); + return 0; +} diff --git a/usage.c b/usage.c new file mode 100644 index 0000000000..86211c9141 --- /dev/null +++ b/usage.c @@ -0,0 +1,39 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" + +static void report(const char *prefix, const char *err, va_list params) +{ + fputs(prefix, stderr); + vfprintf(stderr, err, params); + fputs("\n", stderr); +} + +void usage(const char *err) +{ + fprintf(stderr, "usage: %s\n", err); + exit(1); +} + +void die(const char *err, ...) +{ + va_list params; + + va_start(params, err); + report("fatal: ", err, params); + va_end(params); + exit(1); +} + +int error(const char *err, ...) +{ + va_list params; + + va_start(params, err); + report("error: ", err, params); + va_end(params); + return -1; +} @@ -0,0 +1,65 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Eric Biederman, 2005 + */ +#include "cache.h" +#include <stdio.h> +#include <errno.h> +#include <string.h> + +static char *var_usage = "git-var [-l | <variable>]"; + +struct git_var { + const char *name; + char *(*read)(void); +}; +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()); + } +} + +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(); + break; + } + } + return val; +} + +int main(int argc, char **argv) +{ + const char *val; + if (argc != 2) { + usage(var_usage); + } + setup_ident(); + val = NULL; + + if (strcmp(argv[1], "-l") == 0) { + list_vars(); + return 0; + } + val = read_var(argv[1]); + if (!val) + usage(var_usage); + + printf("%s\n", val); + + return 0; +} diff --git a/verify-pack.c b/verify-pack.c new file mode 100644 index 0000000000..30c40feebd --- /dev/null +++ b/verify-pack.c @@ -0,0 +1,57 @@ +#include "cache.h" +#include "pack.h" + +static int verify_one_pack(char *arg, int verbose) +{ + int len = strlen(arg); + struct packed_git *g; + + while (1) { + /* Should name foo.idx, but foo.pack may be named; + * convert it to foo.idx + */ + if (!strcmp(arg + len - 5, ".pack")) { + strcpy(arg + len - 5, ".idx"); + len--; + } + /* Should name foo.idx now */ + if ((g = add_packed_git(arg, len))) + break; + /* No? did you name just foo? */ + strcpy(arg + len, ".idx"); + len += 4; + if ((g = add_packed_git(arg, len))) + break; + return error("packfile %s not found.", arg); + } + return verify_pack(g, verbose); +} + +static const char *verify_pack_usage = "git-verify-pack [-v] <pack>..."; + +int main(int ac, char **av) +{ + int errs = 0; + int verbose = 0; + int no_more_options = 0; + + while (1 < ac) { + char path[PATH_MAX]; + + if (!no_more_options && av[1][0] == '-') { + if (!strcmp("-v", av[1])) + verbose = 1; + else if (!strcmp("--", av[1])) + no_more_options = 1; + else + usage(verify_pack_usage); + } + else { + strcpy(path, av[1]); + if (verify_one_pack(path, verbose)) + errs++; + } + ac--; av++; + } + return !!errs; +} diff --git a/write-tree.c b/write-tree.c new file mode 100644 index 0000000000..561a101fa9 --- /dev/null +++ b/write-tree.c @@ -0,0 +1,152 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" + +static int missing_ok = 0; + +static int check_valid_sha1(unsigned char *sha1) +{ + int ret; + + /* If we were anal, we'd check that the sha1 of the contents actually matches */ + ret = has_sha1_file(sha1); + if (ret == 0) + perror(sha1_file_name(sha1)); + return ret ? 0 : -1; +} + +static int write_tree(struct cache_entry **cachep, int maxentries, const char *base, int baselen, unsigned char *returnsha1) +{ + unsigned char subdir_sha1[20]; + unsigned long size, offset; + char *buffer; + int nr; + + /* Guess at some random initial size */ + size = 8192; + buffer = xmalloc(size); + offset = 0; + + nr = 0; + while (nr < maxentries) { + struct cache_entry *ce = cachep[nr]; + const char *pathname = ce->name, *filename, *dirname; + int pathlen = ce_namelen(ce), entrylen; + unsigned char *sha1; + unsigned int mode; + + /* Did we hit the end of the directory? Return how many we wrote */ + if (baselen >= pathlen || memcmp(base, pathname, baselen)) + break; + + sha1 = ce->sha1; + mode = ntohl(ce->ce_mode); + + /* Do we have _further_ subdirectories? */ + filename = pathname + baselen; + dirname = strchr(filename, '/'); + if (dirname) { + int subdir_written; + + subdir_written = write_tree(cachep + nr, maxentries - nr, pathname, dirname-pathname+1, subdir_sha1); + nr += subdir_written; + + /* Now we need to write out the directory entry into this tree.. */ + mode = S_IFDIR; + pathlen = dirname - pathname; + + /* ..but the directory entry doesn't count towards the total count */ + nr--; + sha1 = subdir_sha1; + } + + if (!missing_ok && check_valid_sha1(sha1) < 0) + exit(1); + + entrylen = pathlen - baselen; + if (offset + entrylen + 100 > size) { + size = alloc_nr(offset + entrylen + 100); + buffer = xrealloc(buffer, size); + } + offset += sprintf(buffer + offset, "%o %.*s", mode, entrylen, filename); + buffer[offset++] = 0; + memcpy(buffer + offset, sha1, 20); + offset += 20; + nr++; + } + + write_sha1_file(buffer, offset, "tree", returnsha1); + free(buffer); + return nr; +} + +int main(int argc, char **argv) +{ + int i, funny; + int entries = read_cache(); + unsigned char sha1[20]; + + if (argc==2) { + if (!strcmp(argv[1], "--missing-ok")) + missing_ok = 1; + else + die("unknown option %s", argv[1]); + } + + if (argc>2) + die("too many options"); + + if (entries < 0) + die("git-write-tree: error reading cache"); + + /* Verify that the tree is merged */ + funny = 0; + for (i = 0; i < entries; i++) { + struct cache_entry *ce = active_cache[i]; + if (ntohs(ce->ce_flags) & ~CE_NAMEMASK) { + if (10 < ++funny) { + fprintf(stderr, "...\n"); + break; + } + fprintf(stderr, "%s: unmerged (%s)\n", ce->name, sha1_to_hex(ce->sha1)); + } + } + if (funny) + die("git-write-tree: not able to write tree"); + + /* Also verify that the cache does not have path and path/file + * at the same time. At this point we know the cache has only + * stage 0 entries. + */ + funny = 0; + for (i = 0; i < entries - 1; i++) { + /* path/file always comes after path because of the way + * the cache is sorted. Also path can appear only once, + * which means conflicting one would immediately follow. + */ + const char *this_name = active_cache[i]->name; + const char *next_name = active_cache[i+1]->name; + int this_len = strlen(this_name); + if (this_len < strlen(next_name) && + strncmp(this_name, next_name, this_len) == 0 && + next_name[this_len] == '/') { + if (10 < ++funny) { + fprintf(stderr, "...\n"); + break; + } + fprintf(stderr, "You have both %s and %s\n", + this_name, next_name); + } + } + if (funny) + die("git-write-tree: not able to write tree"); + + /* Ok, write it out */ + if (write_tree(active_cache, entries, "", 0, sha1) != entries) + die("git-write-tree: internal error"); + printf("%s\n", sha1_to_hex(sha1)); + return 0; +} |