├── .gitignore ├── .gitmodules ├── COPYING ├── Makefile ├── README ├── cache.c ├── cache.h ├── cgit.c ├── cgit.css ├── cgit.h ├── cgit.png ├── cgitrc.5.txt ├── cmd.c ├── cmd.h ├── configfile.c ├── configfile.h ├── gen-version.sh ├── html.c ├── html.h ├── parsing.c ├── scan-tree.c ├── scan-tree.h ├── shared.c ├── tests ├── .gitignore ├── Makefile ├── setup.sh ├── t0010-validate-html.sh ├── t0020-validate-cache.sh ├── t0101-index.sh ├── t0102-summary.sh ├── t0103-log.sh ├── t0104-tree.sh ├── t0105-commit.sh ├── t0106-diff.sh ├── t0107-snapshot.sh └── t0108-patch.sh ├── ui-atom.c ├── ui-atom.h ├── ui-blob.c ├── ui-blob.h ├── ui-clone.c ├── ui-clone.h ├── ui-commit.c ├── ui-commit.h ├── ui-diff.c ├── ui-diff.h ├── ui-log.c ├── ui-log.h ├── ui-patch.c ├── ui-patch.h ├── ui-plain.c ├── ui-plain.h ├── ui-refs.c ├── ui-refs.h ├── ui-repolist.c ├── ui-repolist.h ├── ui-shared.c ├── ui-shared.h ├── ui-snapshot.c ├── ui-snapshot.h ├── ui-summary.c ├── ui-summary.h ├── ui-tag.c ├── ui-tag.h ├── ui-tree.c └── ui-tree.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Files I don't care to see in git-status/commit 2 | cgit 3 | cgit.conf 4 | VERSION 5 | *.o 6 | *.d 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "git"] 2 | url = git://git.kernel.org/pub/scm/git/git.git 3 | path = git 4 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CGIT_VERSION = v0.8.1 2 | CGIT_SCRIPT_NAME = cgit.cgi 3 | CGIT_SCRIPT_PATH = /var/www/htdocs/cgit 4 | CGIT_CONFIG = /etc/cgitrc 5 | CACHE_ROOT = /var/cache/cgit 6 | SHA1_HEADER = 7 | GIT_VER = 1.6.0.3 8 | GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2 9 | 10 | # 11 | # Let the user override the above settings. 12 | # 13 | -include cgit.conf 14 | 15 | # 16 | # Define a way to invoke make in subdirs quietly, shamelessly ripped 17 | # from git.git 18 | # 19 | QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir 20 | QUIET_SUBDIR1 = 21 | 22 | ifneq ($(findstring $(MAKEFLAGS),w),w) 23 | PRINT_DIR = --no-print-directory 24 | else # "make -w" 25 | NO_SUBDIR = : 26 | endif 27 | 28 | ifndef V 29 | QUIET_CC = @echo ' ' CC $@; 30 | QUIET_MM = @echo ' ' MM $@; 31 | QUIET_SUBDIR0 = +@subdir= 32 | QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \ 33 | $(MAKE) $(PRINT_DIR) -C $$subdir 34 | endif 35 | 36 | # 37 | # Define a pattern rule for automatic dependency building 38 | # 39 | %.d: %.c 40 | $(QUIET_MM)$(CC) $(CFLAGS) -MM $< | sed -e 's/\($*\)\.o:/\1.o $@:/g' >$@ 41 | 42 | # 43 | # Define a pattern rule for silent object building 44 | # 45 | %.o: %.c 46 | $(QUIET_CC)$(CC) -o $*.o -c $(CFLAGS) $< 47 | 48 | 49 | EXTLIBS = git/libgit.a git/xdiff/lib.a -lz -lcrypto 50 | OBJECTS = 51 | OBJECTS += cache.o 52 | OBJECTS += cgit.o 53 | OBJECTS += cmd.o 54 | OBJECTS += configfile.o 55 | OBJECTS += html.o 56 | OBJECTS += parsing.o 57 | OBJECTS += scan-tree.o 58 | OBJECTS += shared.o 59 | OBJECTS += ui-atom.o 60 | OBJECTS += ui-blob.o 61 | OBJECTS += ui-clone.o 62 | OBJECTS += ui-commit.o 63 | OBJECTS += ui-diff.o 64 | OBJECTS += ui-log.o 65 | OBJECTS += ui-patch.o 66 | OBJECTS += ui-plain.o 67 | OBJECTS += ui-refs.o 68 | OBJECTS += ui-repolist.o 69 | OBJECTS += ui-shared.o 70 | OBJECTS += ui-snapshot.o 71 | OBJECTS += ui-summary.o 72 | OBJECTS += ui-tag.o 73 | OBJECTS += ui-tree.o 74 | 75 | ifdef NEEDS_LIBICONV 76 | EXTLIBS += -liconv 77 | endif 78 | 79 | 80 | .PHONY: all libgit test install uninstall clean force-version get-git 81 | 82 | all: cgit 83 | 84 | VERSION: force-version 85 | @./gen-version.sh "$(CGIT_VERSION)" 86 | -include VERSION 87 | 88 | 89 | CFLAGS += -g -Wall -Igit 90 | CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)' 91 | CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"' 92 | CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"' 93 | CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"' 94 | CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"' 95 | 96 | ifdef NO_ICONV 97 | CFLAGS += -DNO_ICONV 98 | endif 99 | 100 | cgit: $(OBJECTS) libgit 101 | $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS) 102 | 103 | cgit.o: VERSION 104 | 105 | -include $(OBJECTS:.o=.d) 106 | 107 | libgit: 108 | $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) libgit.a 109 | $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) xdiff/lib.a 110 | 111 | test: all 112 | $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all 113 | 114 | install: all 115 | mkdir -p $(DESTDIR)$(CGIT_SCRIPT_PATH) 116 | install cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 117 | install -m 0644 cgit.css $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.css 118 | install -m 0644 cgit.png $(DESTDIR)$(CGIT_SCRIPT_PATH)/cgit.png 119 | 120 | uninstall: 121 | rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 122 | rm -f $(CGIT_SCRIPT_PATH)/cgit.css 123 | rm -f $(CGIT_SCRIPT_PATH)/cgit.png 124 | 125 | clean: 126 | rm -f cgit VERSION *.o *.d 127 | 128 | get-git: 129 | curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git 130 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | cgit - cgi for git 3 | 4 | 5 | This is an attempt to create a fast web interface for the git scm, using a 6 | builtin cache to decrease server io-pressure. 7 | 8 | 9 | Installation 10 | 11 | Building cgit involves building a proper version of git. How to do this 12 | depends on how you obtained the cgit sources: 13 | 14 | a) If you're working in a cloned cgit repository, you first need to 15 | initialize and update the git submodule: 16 | 17 | $ git submodule init # register the git submodule in .git/config 18 | $ $EDITOR .git/config # if you want to specify a different url for git 19 | $ git submodule update # clone/fetch and checkout correct git version 20 | 21 | b) If you're building from a cgit tarball, you can download a proper git 22 | version like this: 23 | 24 | $ make get-git 25 | 26 | 27 | When either a) or b) has been performed, you can build and install cgit like 28 | this: 29 | 30 | $ make 31 | $ sudo make install 32 | 33 | This will install cgit.cgi and cgit.css into "/var/www/htdocs/cgit". You can 34 | configure this location (and a few other things) by providing a "cgit.conf" 35 | file (see the Makefile for details). 36 | 37 | 38 | Dependencies: 39 | -git 1.5.3 40 | -zip lib 41 | -crypto lib 42 | -openssl lib 43 | 44 | 45 | Apache configuration 46 | 47 | A new Directory-section must probably be added for cgit, possibly something 48 | like this: 49 | 50 | 51 | AllowOverride None 52 | Options ExecCGI 53 | Order allow,deny 54 | Allow from all 55 | 56 | 57 | 58 | Runtime configuration 59 | 60 | The file /etc/cgitrc is read by cgit before handling a request. In addition 61 | to runtime parameters, this file also contains a list of the repositories 62 | displayed by cgit. 63 | 64 | A template cgitrc is shipped with the sources, and all parameters and default 65 | values are documented in this file. 66 | 67 | 68 | The cache 69 | 70 | When cgit is invoked it looks for a cachefile matching the request and 71 | returns it to the client. If no such cachefile exist (or if it has expired), 72 | the content for the request is written into the proper cachefile before the 73 | file is returned. 74 | 75 | If the cachefile has expired but cgit is unable to obtain a lock for it, the 76 | stale cachefile is returned to the client. This is done to favour page 77 | throughput over page freshness. 78 | 79 | The generated content contains the complete response to the client, including 80 | the http-headers "Modified" and "Expires". 81 | 82 | 83 | The missing features 84 | 85 | * Submodule links in the directory listing page have a fixed format per 86 | repository. This should probably be extended to a generic map between 87 | submodule path and url. 88 | 89 | * Branch- and tag-lists in the summary page can get very long, they should 90 | probably only show something like the ten "latest modified" branches and 91 | a similar number of "most recent" tags. 92 | 93 | * There should be a new page for browsing refs/heads and refs/tags, with links 94 | from the summary page whenever the branch/tag lists overflow. 95 | 96 | * The log-page should have more/better search options (author, committer, 97 | pickaxe, paths) and possibly support arbitrary revision specifiers. 98 | 99 | * A set of test-scripts is required before cgit-1.0 can be released. 100 | 101 | Patches/bugreports/suggestions/comments are always welcome, please feel free 102 | to contact the author: hjemli@gmail.com 103 | -------------------------------------------------------------------------------- /cache.c: -------------------------------------------------------------------------------- 1 | /* cache.c: cache management 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | * 8 | * 9 | * The cache is just a directory structure where each file is a cache slot, 10 | * and each filename is based on the hash of some key (e.g. the cgit url). 11 | * Each file contains the full key followed by the cached content for that 12 | * key. 13 | * 14 | */ 15 | 16 | #include "cgit.h" 17 | #include "cache.h" 18 | 19 | #define CACHE_BUFSIZE (1024 * 4) 20 | 21 | struct cache_slot { 22 | const char *key; 23 | int keylen; 24 | int ttl; 25 | cache_fill_fn fn; 26 | void *cbdata; 27 | int cache_fd; 28 | int lock_fd; 29 | const char *cache_name; 30 | const char *lock_name; 31 | int match; 32 | struct stat cache_st; 33 | struct stat lock_st; 34 | int bufsize; 35 | char buf[CACHE_BUFSIZE]; 36 | }; 37 | 38 | /* Open an existing cache slot and fill the cache buffer with 39 | * (part of) the content of the cache file. Return 0 on success 40 | * and errno otherwise. 41 | */ 42 | static int open_slot(struct cache_slot *slot) 43 | { 44 | char *bufz; 45 | int bufkeylen = -1; 46 | 47 | slot->cache_fd = open(slot->cache_name, O_RDONLY); 48 | if (slot->cache_fd == -1) 49 | return errno; 50 | 51 | if (fstat(slot->cache_fd, &slot->cache_st)) 52 | return errno; 53 | 54 | slot->bufsize = xread(slot->cache_fd, slot->buf, sizeof(slot->buf)); 55 | if (slot->bufsize < 0) 56 | return errno; 57 | 58 | bufz = memchr(slot->buf, 0, slot->bufsize); 59 | if (bufz) 60 | bufkeylen = bufz - slot->buf; 61 | 62 | slot->match = bufkeylen == slot->keylen && 63 | !memcmp(slot->key, slot->buf, bufkeylen + 1); 64 | 65 | return 0; 66 | } 67 | 68 | /* Close the active cache slot */ 69 | static int close_slot(struct cache_slot *slot) 70 | { 71 | int err = 0; 72 | if (slot->cache_fd > 0) { 73 | if (close(slot->cache_fd)) 74 | err = errno; 75 | else 76 | slot->cache_fd = -1; 77 | } 78 | return err; 79 | } 80 | 81 | /* Print the content of the active cache slot (but skip the key). */ 82 | static int print_slot(struct cache_slot *slot) 83 | { 84 | ssize_t i, j; 85 | 86 | i = lseek(slot->cache_fd, slot->keylen + 1, SEEK_SET); 87 | if (i != slot->keylen + 1) 88 | return errno; 89 | 90 | do { 91 | i = j = xread(slot->cache_fd, slot->buf, sizeof(slot->buf)); 92 | if (i > 0) 93 | j = xwrite(STDOUT_FILENO, slot->buf, i); 94 | } while (i > 0 && j == i); 95 | 96 | if (i < 0 || j != i) 97 | return errno; 98 | else 99 | return 0; 100 | } 101 | 102 | /* Check if the slot has expired */ 103 | static int is_expired(struct cache_slot *slot) 104 | { 105 | if (slot->ttl < 0) 106 | return 0; 107 | else 108 | return slot->cache_st.st_mtime + slot->ttl*60 < time(NULL); 109 | } 110 | 111 | /* Check if the slot has been modified since we opened it. 112 | * NB: If stat() fails, we pretend the file is modified. 113 | */ 114 | static int is_modified(struct cache_slot *slot) 115 | { 116 | struct stat st; 117 | 118 | if (stat(slot->cache_name, &st)) 119 | return 1; 120 | return (st.st_ino != slot->cache_st.st_ino || 121 | st.st_mtime != slot->cache_st.st_mtime || 122 | st.st_size != slot->cache_st.st_size); 123 | } 124 | 125 | /* Close an open lockfile */ 126 | static int close_lock(struct cache_slot *slot) 127 | { 128 | int err = 0; 129 | if (slot->lock_fd > 0) { 130 | if (close(slot->lock_fd)) 131 | err = errno; 132 | else 133 | slot->lock_fd = -1; 134 | } 135 | return err; 136 | } 137 | 138 | /* Create a lockfile used to store the generated content for a cache 139 | * slot, and write the slot key + \0 into it. 140 | * Returns 0 on success and errno otherwise. 141 | */ 142 | static int lock_slot(struct cache_slot *slot) 143 | { 144 | slot->lock_fd = open(slot->lock_name, O_RDWR|O_CREAT|O_EXCL, 145 | S_IRUSR|S_IWUSR); 146 | if (slot->lock_fd == -1) 147 | return errno; 148 | if (xwrite(slot->lock_fd, slot->key, slot->keylen + 1) < 0) 149 | return errno; 150 | return 0; 151 | } 152 | 153 | /* Release the current lockfile. If `replace_old_slot` is set the 154 | * lockfile replaces the old cache slot, otherwise the lockfile is 155 | * just deleted. 156 | */ 157 | static int unlock_slot(struct cache_slot *slot, int replace_old_slot) 158 | { 159 | int err; 160 | 161 | if (replace_old_slot) 162 | err = rename(slot->lock_name, slot->cache_name); 163 | else 164 | err = unlink(slot->lock_name); 165 | 166 | if (err) 167 | return errno; 168 | 169 | return 0; 170 | } 171 | 172 | /* Generate the content for the current cache slot by redirecting 173 | * stdout to the lock-fd and invoking the callback function 174 | */ 175 | static int fill_slot(struct cache_slot *slot) 176 | { 177 | int tmp; 178 | 179 | /* Preserve stdout */ 180 | tmp = dup(STDOUT_FILENO); 181 | if (tmp == -1) 182 | return errno; 183 | 184 | /* Redirect stdout to lockfile */ 185 | if (dup2(slot->lock_fd, STDOUT_FILENO) == -1) 186 | return errno; 187 | 188 | /* Generate cache content */ 189 | slot->fn(slot->cbdata); 190 | 191 | /* Restore stdout */ 192 | if (dup2(tmp, STDOUT_FILENO) == -1) 193 | return errno; 194 | 195 | /* Close the temporary filedescriptor */ 196 | if (close(tmp)) 197 | return errno; 198 | 199 | return 0; 200 | } 201 | 202 | /* Crude implementation of 32-bit FNV-1 hash algorithm, 203 | * see http://www.isthe.com/chongo/tech/comp/fnv/ for details 204 | * about the magic numbers. 205 | */ 206 | #define FNV_OFFSET 0x811c9dc5 207 | #define FNV_PRIME 0x01000193 208 | 209 | unsigned long hash_str(const char *str) 210 | { 211 | unsigned long h = FNV_OFFSET; 212 | unsigned char *s = (unsigned char *)str; 213 | 214 | if (!s) 215 | return h; 216 | 217 | while(*s) { 218 | h *= FNV_PRIME; 219 | h ^= *s++; 220 | } 221 | return h; 222 | } 223 | 224 | static int process_slot(struct cache_slot *slot) 225 | { 226 | int err; 227 | 228 | err = open_slot(slot); 229 | if (!err && slot->match) { 230 | if (is_expired(slot)) { 231 | if (!lock_slot(slot)) { 232 | /* If the cachefile has been replaced between 233 | * `open_slot` and `lock_slot`, we'll just 234 | * serve the stale content from the original 235 | * cachefile. This way we avoid pruning the 236 | * newly generated slot. The same code-path 237 | * is chosen if fill_slot() fails for some 238 | * reason. 239 | * 240 | * TODO? check if the new slot contains the 241 | * same key as the old one, since we would 242 | * prefer to serve the newest content. 243 | * This will require us to open yet another 244 | * file-descriptor and read and compare the 245 | * key from the new file, so for now we're 246 | * lazy and just ignore the new file. 247 | */ 248 | if (is_modified(slot) || fill_slot(slot)) { 249 | unlock_slot(slot, 0); 250 | close_lock(slot); 251 | } else { 252 | close_slot(slot); 253 | unlock_slot(slot, 1); 254 | slot->cache_fd = slot->lock_fd; 255 | } 256 | } 257 | } 258 | if ((err = print_slot(slot)) != 0) { 259 | cache_log("[cgit] error printing cache %s: %s (%d)\n", 260 | slot->cache_name, 261 | strerror(err), 262 | err); 263 | } 264 | close_slot(slot); 265 | return err; 266 | } 267 | 268 | /* If the cache slot does not exist (or its key doesn't match the 269 | * current key), lets try to create a new cache slot for this 270 | * request. If this fails (for whatever reason), lets just generate 271 | * the content without caching it and fool the caller to belive 272 | * everything worked out (but print a warning on stdout). 273 | */ 274 | 275 | close_slot(slot); 276 | if ((err = lock_slot(slot)) != 0) { 277 | cache_log("[cgit] Unable to lock slot %s: %s (%d)\n", 278 | slot->lock_name, strerror(err), err); 279 | slot->fn(slot->cbdata); 280 | return 0; 281 | } 282 | 283 | if ((err = fill_slot(slot)) != 0) { 284 | cache_log("[cgit] Unable to fill slot %s: %s (%d)\n", 285 | slot->lock_name, strerror(err), err); 286 | unlock_slot(slot, 0); 287 | close_lock(slot); 288 | slot->fn(slot->cbdata); 289 | return 0; 290 | } 291 | // We've got a valid cache slot in the lock file, which 292 | // is about to replace the old cache slot. But if we 293 | // release the lockfile and then try to open the new cache 294 | // slot, we might get a race condition with a concurrent 295 | // writer for the same cache slot (with a different key). 296 | // Lets avoid such a race by just printing the content of 297 | // the lock file. 298 | slot->cache_fd = slot->lock_fd; 299 | unlock_slot(slot, 1); 300 | if ((err = print_slot(slot)) != 0) { 301 | cache_log("[cgit] error printing cache %s: %s (%d)\n", 302 | slot->cache_name, 303 | strerror(err), 304 | err); 305 | } 306 | close_slot(slot); 307 | return err; 308 | } 309 | 310 | /* Print cached content to stdout, generate the content if necessary. */ 311 | int cache_process(int size, const char *path, const char *key, int ttl, 312 | cache_fill_fn fn, void *cbdata) 313 | { 314 | unsigned long hash; 315 | int len, i; 316 | char filename[1024]; 317 | char lockname[1024 + 5]; /* 5 = ".lock" */ 318 | struct cache_slot slot; 319 | 320 | /* If the cache is disabled, just generate the content */ 321 | if (size <= 0) { 322 | fn(cbdata); 323 | return 0; 324 | } 325 | 326 | /* Verify input, calculate filenames */ 327 | if (!path) { 328 | cache_log("[cgit] Cache path not specified, caching is disabled\n"); 329 | fn(cbdata); 330 | return 0; 331 | } 332 | len = strlen(path); 333 | if (len > sizeof(filename) - 10) { /* 10 = "/01234567\0" */ 334 | cache_log("[cgit] Cache path too long, caching is disabled: %s\n", 335 | path); 336 | fn(cbdata); 337 | return 0; 338 | } 339 | if (!key) 340 | key = ""; 341 | hash = hash_str(key) % size; 342 | strcpy(filename, path); 343 | if (filename[len - 1] != '/') 344 | filename[len++] = '/'; 345 | for(i = 0; i < 8; i++) { 346 | sprintf(filename + len++, "%x", 347 | (unsigned char)(hash & 0xf)); 348 | hash >>= 4; 349 | } 350 | filename[len] = '\0'; 351 | strcpy(lockname, filename); 352 | strcpy(lockname + len, ".lock"); 353 | slot.fn = fn; 354 | slot.cbdata = cbdata; 355 | slot.ttl = ttl; 356 | slot.cache_name = filename; 357 | slot.lock_name = lockname; 358 | slot.key = key; 359 | slot.keylen = strlen(key); 360 | return process_slot(&slot); 361 | } 362 | 363 | /* Return a strftime formatted date/time 364 | * NB: the result from this function is to shared memory 365 | */ 366 | char *sprintftime(const char *format, time_t time) 367 | { 368 | static char buf[64]; 369 | struct tm *tm; 370 | 371 | if (!time) 372 | return NULL; 373 | tm = gmtime(&time); 374 | strftime(buf, sizeof(buf)-1, format, tm); 375 | return buf; 376 | } 377 | 378 | int cache_ls(const char *path) 379 | { 380 | DIR *dir; 381 | struct dirent *ent; 382 | int err = 0; 383 | struct cache_slot slot; 384 | char fullname[1024]; 385 | char *name; 386 | 387 | if (!path) { 388 | cache_log("[cgit] cache path not specified\n"); 389 | return -1; 390 | } 391 | if (strlen(path) > 1024 - 10) { 392 | cache_log("[cgit] cache path too long: %s\n", 393 | path); 394 | return -1; 395 | } 396 | dir = opendir(path); 397 | if (!dir) { 398 | err = errno; 399 | cache_log("[cgit] unable to open path %s: %s (%d)\n", 400 | path, strerror(err), err); 401 | return err; 402 | } 403 | strcpy(fullname, path); 404 | name = fullname + strlen(path); 405 | if (*(name - 1) != '/') { 406 | *name++ = '/'; 407 | *name = '\0'; 408 | } 409 | slot.cache_name = fullname; 410 | while((ent = readdir(dir)) != NULL) { 411 | if (strlen(ent->d_name) != 8) 412 | continue; 413 | strcpy(name, ent->d_name); 414 | if ((err = open_slot(&slot)) != 0) { 415 | cache_log("[cgit] unable to open path %s: %s (%d)\n", 416 | fullname, strerror(err), err); 417 | continue; 418 | } 419 | printf("%s %s %10"PRIuMAX" %s\n", 420 | name, 421 | sprintftime("%Y-%m-%d %H:%M:%S", 422 | slot.cache_st.st_mtime), 423 | (uintmax_t)slot.cache_st.st_size, 424 | slot.buf); 425 | close_slot(&slot); 426 | } 427 | closedir(dir); 428 | return 0; 429 | } 430 | 431 | /* Print a message to stdout */ 432 | void cache_log(const char *format, ...) 433 | { 434 | va_list args; 435 | va_start(args, format); 436 | vfprintf(stderr, format, args); 437 | va_end(args); 438 | } 439 | 440 | -------------------------------------------------------------------------------- /cache.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Since git has it's own cache.h which we include, 3 | * lets test on CGIT_CACHE_H to avoid confusion 4 | */ 5 | 6 | #ifndef CGIT_CACHE_H 7 | #define CGIT_CACHE_H 8 | 9 | typedef void (*cache_fill_fn)(void *cbdata); 10 | 11 | 12 | /* Print cached content to stdout, generate the content if necessary. 13 | * 14 | * Parameters 15 | * size max number of cache files 16 | * path directory used to store cache files 17 | * key the key used to lookup cache files 18 | * ttl max cache time in seconds for this key 19 | * fn content generator function for this key 20 | * cbdata user-supplied data to the content generator function 21 | * 22 | * Return value 23 | * 0 indicates success, everyting else is an error 24 | */ 25 | extern int cache_process(int size, const char *path, const char *key, int ttl, 26 | cache_fill_fn fn, void *cbdata); 27 | 28 | 29 | /* List info about all cache entries on stdout */ 30 | extern int cache_ls(const char *path); 31 | 32 | /* Print a message to stdout */ 33 | extern void cache_log(const char *format, ...); 34 | 35 | #endif /* CGIT_CACHE_H */ 36 | -------------------------------------------------------------------------------- /cgit.c: -------------------------------------------------------------------------------- 1 | /* cgit.c: cgi for the git scm 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "cache.h" 11 | #include "cmd.h" 12 | #include "configfile.h" 13 | #include "html.h" 14 | #include "ui-shared.h" 15 | #include "scan-tree.h" 16 | 17 | const char *cgit_version = CGIT_VERSION; 18 | 19 | void config_cb(const char *name, const char *value) 20 | { 21 | if (!strcmp(name, "root-title")) 22 | ctx.cfg.root_title = xstrdup(value); 23 | else if (!strcmp(name, "root-desc")) 24 | ctx.cfg.root_desc = xstrdup(value); 25 | else if (!strcmp(name, "root-readme")) 26 | ctx.cfg.root_readme = xstrdup(value); 27 | else if (!strcmp(name, "css")) 28 | ctx.cfg.css = xstrdup(value); 29 | else if (!strcmp(name, "favicon")) 30 | ctx.cfg.favicon = xstrdup(value); 31 | else if (!strcmp(name, "footer")) 32 | ctx.cfg.footer = xstrdup(value); 33 | else if (!strcmp(name, "logo")) 34 | ctx.cfg.logo = xstrdup(value); 35 | else if (!strcmp(name, "index-header")) 36 | ctx.cfg.index_header = xstrdup(value); 37 | else if (!strcmp(name, "index-info")) 38 | ctx.cfg.index_info = xstrdup(value); 39 | else if (!strcmp(name, "logo-link")) 40 | ctx.cfg.logo_link = xstrdup(value); 41 | else if (!strcmp(name, "module-link")) 42 | ctx.cfg.module_link = xstrdup(value); 43 | else if (!strcmp(name, "virtual-root")) { 44 | ctx.cfg.virtual_root = trim_end(value, '/'); 45 | if (!ctx.cfg.virtual_root && (!strcmp(value, "/"))) 46 | ctx.cfg.virtual_root = ""; 47 | } else if (!strcmp(name, "nocache")) 48 | ctx.cfg.nocache = atoi(value); 49 | else if (!strcmp(name, "snapshots")) 50 | ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); 51 | else if (!strcmp(name, "enable-index-links")) 52 | ctx.cfg.enable_index_links = atoi(value); 53 | else if (!strcmp(name, "enable-log-filecount")) 54 | ctx.cfg.enable_log_filecount = atoi(value); 55 | else if (!strcmp(name, "enable-log-linecount")) 56 | ctx.cfg.enable_log_linecount = atoi(value); 57 | else if (!strcmp(name, "cache-size")) 58 | ctx.cfg.cache_size = atoi(value); 59 | else if (!strcmp(name, "cache-root")) 60 | ctx.cfg.cache_root = xstrdup(value); 61 | else if (!strcmp(name, "cache-root-ttl")) 62 | ctx.cfg.cache_root_ttl = atoi(value); 63 | else if (!strcmp(name, "cache-repo-ttl")) 64 | ctx.cfg.cache_repo_ttl = atoi(value); 65 | else if (!strcmp(name, "cache-static-ttl")) 66 | ctx.cfg.cache_static_ttl = atoi(value); 67 | else if (!strcmp(name, "cache-dynamic-ttl")) 68 | ctx.cfg.cache_dynamic_ttl = atoi(value); 69 | else if (!strcmp(name, "max-message-length")) 70 | ctx.cfg.max_msg_len = atoi(value); 71 | else if (!strcmp(name, "max-repodesc-length")) 72 | ctx.cfg.max_repodesc_len = atoi(value); 73 | else if (!strcmp(name, "max-repo-count")) 74 | ctx.cfg.max_repo_count = atoi(value); 75 | else if (!strcmp(name, "max-commit-count")) 76 | ctx.cfg.max_commit_count = atoi(value); 77 | else if (!strcmp(name, "summary-log")) 78 | ctx.cfg.summary_log = atoi(value); 79 | else if (!strcmp(name, "summary-branches")) 80 | ctx.cfg.summary_branches = atoi(value); 81 | else if (!strcmp(name, "summary-tags")) 82 | ctx.cfg.summary_tags = atoi(value); 83 | else if (!strcmp(name, "agefile")) 84 | ctx.cfg.agefile = xstrdup(value); 85 | else if (!strcmp(name, "renamelimit")) 86 | ctx.cfg.renamelimit = atoi(value); 87 | else if (!strcmp(name, "robots")) 88 | ctx.cfg.robots = xstrdup(value); 89 | else if (!strcmp(name, "clone-prefix")) 90 | ctx.cfg.clone_prefix = xstrdup(value); 91 | else if (!strcmp(name, "local-time")) 92 | ctx.cfg.local_time = atoi(value); 93 | else if (!strcmp(name, "repo.group")) 94 | ctx.cfg.repo_group = xstrdup(value); 95 | else if (!strcmp(name, "repo.url")) 96 | ctx.repo = cgit_add_repo(value); 97 | else if (!strcmp(name, "repo.name")) 98 | ctx.repo->name = xstrdup(value); 99 | else if (ctx.repo && !strcmp(name, "repo.path")) 100 | ctx.repo->path = trim_end(value, '/'); 101 | else if (ctx.repo && !strcmp(name, "repo.clone-url")) 102 | ctx.repo->clone_url = xstrdup(value); 103 | else if (ctx.repo && !strcmp(name, "repo.desc")) 104 | ctx.repo->desc = xstrdup(value); 105 | else if (ctx.repo && !strcmp(name, "repo.owner")) 106 | ctx.repo->owner = xstrdup(value); 107 | else if (ctx.repo && !strcmp(name, "repo.defbranch")) 108 | ctx.repo->defbranch = xstrdup(value); 109 | else if (ctx.repo && !strcmp(name, "repo.snapshots")) 110 | ctx.repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); /* XXX: &? */ 111 | else if (ctx.repo && !strcmp(name, "repo.enable-log-filecount")) 112 | ctx.repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value); 113 | else if (ctx.repo && !strcmp(name, "repo.enable-log-linecount")) 114 | ctx.repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value); 115 | else if (ctx.repo && !strcmp(name, "repo.module-link")) 116 | ctx.repo->module_link= xstrdup(value); 117 | else if (ctx.repo && !strcmp(name, "repo.readme") && value != NULL) { 118 | if (*value == '/') 119 | ctx.repo->readme = xstrdup(value); 120 | else 121 | ctx.repo->readme = xstrdup(fmt("%s/%s", ctx.repo->path, value)); 122 | } else if (!strcmp(name, "include")) 123 | parse_configfile(value, config_cb); 124 | } 125 | 126 | static void querystring_cb(const char *name, const char *value) 127 | { 128 | if (!strcmp(name,"r")) { 129 | ctx.qry.repo = xstrdup(value); 130 | ctx.repo = cgit_get_repoinfo(value); 131 | } else if (!strcmp(name, "p")) { 132 | ctx.qry.page = xstrdup(value); 133 | } else if (!strcmp(name, "url")) { 134 | ctx.qry.url = xstrdup(value); 135 | cgit_parse_url(value); 136 | } else if (!strcmp(name, "qt")) { 137 | ctx.qry.grep = xstrdup(value); 138 | } else if (!strcmp(name, "q")) { 139 | ctx.qry.search = xstrdup(value); 140 | } else if (!strcmp(name, "h")) { 141 | ctx.qry.head = xstrdup(value); 142 | ctx.qry.has_symref = 1; 143 | } else if (!strcmp(name, "id")) { 144 | ctx.qry.sha1 = xstrdup(value); 145 | ctx.qry.has_sha1 = 1; 146 | } else if (!strcmp(name, "id2")) { 147 | ctx.qry.sha2 = xstrdup(value); 148 | ctx.qry.has_sha1 = 1; 149 | } else if (!strcmp(name, "ofs")) { 150 | ctx.qry.ofs = atoi(value); 151 | } else if (!strcmp(name, "path")) { 152 | ctx.qry.path = trim_end(value, '/'); 153 | } else if (!strcmp(name, "name")) { 154 | ctx.qry.name = xstrdup(value); 155 | } else if (!strcmp(name, "mimetype")) { 156 | ctx.qry.mimetype = xstrdup(value); 157 | } 158 | } 159 | 160 | static void prepare_context(struct cgit_context *ctx) 161 | { 162 | memset(ctx, 0, sizeof(ctx)); 163 | ctx->cfg.agefile = "info/web/last-modified"; 164 | ctx->cfg.nocache = 0; 165 | ctx->cfg.cache_size = 0; 166 | ctx->cfg.cache_dynamic_ttl = 5; 167 | ctx->cfg.cache_max_create_time = 5; 168 | ctx->cfg.cache_repo_ttl = 5; 169 | ctx->cfg.cache_root = CGIT_CACHE_ROOT; 170 | ctx->cfg.cache_root_ttl = 5; 171 | ctx->cfg.cache_static_ttl = -1; 172 | ctx->cfg.css = "/cgit.css"; 173 | ctx->cfg.logo = "/git-logo.png"; 174 | ctx->cfg.local_time = 0; 175 | ctx->cfg.max_repo_count = 50; 176 | ctx->cfg.max_commit_count = 50; 177 | ctx->cfg.max_lock_attempts = 5; 178 | ctx->cfg.max_msg_len = 80; 179 | ctx->cfg.max_repodesc_len = 80; 180 | ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; 181 | ctx->cfg.renamelimit = -1; 182 | ctx->cfg.robots = "index, nofollow"; 183 | ctx->cfg.root_title = "Git repository browser"; 184 | ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 185 | ctx->cfg.script_name = CGIT_SCRIPT_NAME; 186 | ctx->cfg.summary_branches = 10; 187 | ctx->cfg.summary_log = 10; 188 | ctx->cfg.summary_tags = 10; 189 | ctx->page.mimetype = "text/html"; 190 | ctx->page.charset = PAGE_ENCODING; 191 | ctx->page.filename = NULL; 192 | ctx->page.size = 0; 193 | ctx->page.modified = time(NULL); 194 | ctx->page.expires = ctx->page.modified; 195 | } 196 | 197 | struct refmatch { 198 | char *req_ref; 199 | char *first_ref; 200 | int match; 201 | }; 202 | 203 | int find_current_ref(const char *refname, const unsigned char *sha1, 204 | int flags, void *cb_data) 205 | { 206 | struct refmatch *info; 207 | 208 | info = (struct refmatch *)cb_data; 209 | if (!strcmp(refname, info->req_ref)) 210 | info->match = 1; 211 | if (!info->first_ref) 212 | info->first_ref = xstrdup(refname); 213 | return info->match; 214 | } 215 | 216 | char *find_default_branch(struct cgit_repo *repo) 217 | { 218 | struct refmatch info; 219 | char *ref; 220 | 221 | info.req_ref = repo->defbranch; 222 | info.first_ref = NULL; 223 | info.match = 0; 224 | for_each_branch_ref(find_current_ref, &info); 225 | if (info.match) 226 | ref = info.req_ref; 227 | else 228 | ref = info.first_ref; 229 | if (ref) 230 | ref = xstrdup(ref); 231 | return ref; 232 | } 233 | 234 | static int prepare_repo_cmd(struct cgit_context *ctx) 235 | { 236 | char *tmp; 237 | unsigned char sha1[20]; 238 | int nongit = 0; 239 | 240 | setenv("GIT_DIR", ctx->repo->path, 1); 241 | setup_git_directory_gently(&nongit); 242 | if (nongit) { 243 | ctx->page.title = fmt("%s - %s", ctx->cfg.root_title, 244 | "config error"); 245 | tmp = fmt("Not a git repository: '%s'", ctx->repo->path); 246 | ctx->repo = NULL; 247 | cgit_print_http_headers(ctx); 248 | cgit_print_docstart(ctx); 249 | cgit_print_pageheader(ctx); 250 | cgit_print_error(tmp); 251 | cgit_print_docend(); 252 | return 1; 253 | } 254 | ctx->page.title = fmt("%s - %s", ctx->repo->name, ctx->repo->desc); 255 | 256 | if (!ctx->qry.head) { 257 | ctx->qry.nohead = 1; 258 | ctx->qry.head = find_default_branch(ctx->repo); 259 | ctx->repo->defbranch = ctx->qry.head; 260 | } 261 | 262 | if (!ctx->qry.head) { 263 | cgit_print_http_headers(ctx); 264 | cgit_print_docstart(ctx); 265 | cgit_print_pageheader(ctx); 266 | cgit_print_error("Repository seems to be empty"); 267 | cgit_print_docend(); 268 | return 1; 269 | } 270 | 271 | if (get_sha1(ctx->qry.head, sha1)) { 272 | tmp = xstrdup(ctx->qry.head); 273 | ctx->qry.head = ctx->repo->defbranch; 274 | cgit_print_http_headers(ctx); 275 | cgit_print_docstart(ctx); 276 | cgit_print_pageheader(ctx); 277 | cgit_print_error(fmt("Invalid branch: %s", tmp)); 278 | cgit_print_docend(); 279 | return 1; 280 | } 281 | return 0; 282 | } 283 | 284 | static void process_request(void *cbdata) 285 | { 286 | struct cgit_context *ctx = cbdata; 287 | struct cgit_cmd *cmd; 288 | 289 | cmd = cgit_get_cmd(ctx); 290 | if (!cmd) { 291 | ctx->page.title = "cgit error"; 292 | ctx->repo = NULL; 293 | cgit_print_http_headers(ctx); 294 | cgit_print_docstart(ctx); 295 | cgit_print_pageheader(ctx); 296 | cgit_print_error("Invalid request"); 297 | cgit_print_docend(); 298 | return; 299 | } 300 | 301 | if (cmd->want_repo && !ctx->repo) { 302 | cgit_print_http_headers(ctx); 303 | cgit_print_docstart(ctx); 304 | cgit_print_pageheader(ctx); 305 | cgit_print_error(fmt("No repository selected")); 306 | cgit_print_docend(); 307 | return; 308 | } 309 | 310 | if (ctx->repo && prepare_repo_cmd(ctx)) 311 | return; 312 | 313 | if (cmd->want_layout) { 314 | cgit_print_http_headers(ctx); 315 | cgit_print_docstart(ctx); 316 | cgit_print_pageheader(ctx); 317 | } 318 | 319 | cmd->fn(ctx); 320 | 321 | if (cmd->want_layout) 322 | cgit_print_docend(); 323 | } 324 | 325 | int cmp_repos(const void *a, const void *b) 326 | { 327 | const struct cgit_repo *ra = a, *rb = b; 328 | return strcmp(ra->url, rb->url); 329 | } 330 | 331 | void print_repo(struct cgit_repo *repo) 332 | { 333 | printf("repo.url=%s\n", repo->url); 334 | printf("repo.name=%s\n", repo->name); 335 | printf("repo.path=%s\n", repo->path); 336 | if (repo->owner) 337 | printf("repo.owner=%s\n", repo->owner); 338 | if (repo->desc) 339 | printf("repo.desc=%s\n", repo->desc); 340 | if (repo->readme) 341 | printf("repo.readme=%s\n", repo->readme); 342 | printf("\n"); 343 | } 344 | 345 | void print_repolist(struct cgit_repolist *list) 346 | { 347 | int i; 348 | 349 | for(i = 0; i < list->count; i++) 350 | print_repo(&list->repos[i]); 351 | } 352 | 353 | 354 | static void cgit_parse_args(int argc, const char **argv) 355 | { 356 | int i; 357 | int scan = 0; 358 | 359 | for (i = 1; i < argc; i++) { 360 | if (!strncmp(argv[i], "--cache=", 8)) { 361 | ctx.cfg.cache_root = xstrdup(argv[i]+8); 362 | } 363 | if (!strcmp(argv[i], "--nocache")) { 364 | ctx.cfg.nocache = 1; 365 | } 366 | if (!strncmp(argv[i], "--query=", 8)) { 367 | ctx.qry.raw = xstrdup(argv[i]+8); 368 | } 369 | if (!strncmp(argv[i], "--repo=", 7)) { 370 | ctx.qry.repo = xstrdup(argv[i]+7); 371 | } 372 | if (!strncmp(argv[i], "--page=", 7)) { 373 | ctx.qry.page = xstrdup(argv[i]+7); 374 | } 375 | if (!strncmp(argv[i], "--head=", 7)) { 376 | ctx.qry.head = xstrdup(argv[i]+7); 377 | ctx.qry.has_symref = 1; 378 | } 379 | if (!strncmp(argv[i], "--sha1=", 7)) { 380 | ctx.qry.sha1 = xstrdup(argv[i]+7); 381 | ctx.qry.has_sha1 = 1; 382 | } 383 | if (!strncmp(argv[i], "--ofs=", 6)) { 384 | ctx.qry.ofs = atoi(argv[i]+6); 385 | } 386 | if (!strncmp(argv[i], "--scan-tree=", 12)) { 387 | scan++; 388 | scan_tree(argv[i] + 12); 389 | } 390 | } 391 | if (scan) { 392 | qsort(cgit_repolist.repos, cgit_repolist.count, 393 | sizeof(struct cgit_repo), cmp_repos); 394 | print_repolist(&cgit_repolist); 395 | exit(0); 396 | } 397 | } 398 | 399 | static int calc_ttl() 400 | { 401 | if (!ctx.repo) 402 | return ctx.cfg.cache_root_ttl; 403 | 404 | if (!ctx.qry.page) 405 | return ctx.cfg.cache_repo_ttl; 406 | 407 | if (ctx.qry.has_symref) 408 | return ctx.cfg.cache_dynamic_ttl; 409 | 410 | if (ctx.qry.has_sha1) 411 | return ctx.cfg.cache_static_ttl; 412 | 413 | return ctx.cfg.cache_repo_ttl; 414 | } 415 | 416 | int main(int argc, const char **argv) 417 | { 418 | const char *cgit_config_env = getenv("CGIT_CONFIG"); 419 | const char *path; 420 | char *qry; 421 | int err, ttl; 422 | 423 | prepare_context(&ctx); 424 | cgit_repolist.length = 0; 425 | cgit_repolist.count = 0; 426 | cgit_repolist.repos = NULL; 427 | 428 | if (getenv("SCRIPT_NAME")) 429 | ctx.cfg.script_name = xstrdup(getenv("SCRIPT_NAME")); 430 | if (getenv("QUERY_STRING")) 431 | ctx.qry.raw = xstrdup(getenv("QUERY_STRING")); 432 | cgit_parse_args(argc, argv); 433 | parse_configfile(cgit_config_env ? cgit_config_env : CGIT_CONFIG, 434 | config_cb); 435 | ctx.repo = NULL; 436 | http_parse_querystring(ctx.qry.raw, querystring_cb); 437 | 438 | /* If no url parameter is specified in the query string, let's 439 | * pretend that virtual-root is SCRIPT_NAME (if undefined) and 440 | * use PATH_INFO as the url. This allows cgit to work with 441 | * pretty urls without the need for rewrite rules in the web 442 | * server. 443 | */ 444 | if (!ctx.qry.url) { 445 | if (!ctx.cfg.virtual_root) 446 | ctx.cfg.virtual_root = ctx.cfg.script_name; 447 | 448 | path = getenv("PATH_INFO"); 449 | if (path) { 450 | if (path[0] == '/') 451 | path++; 452 | ctx.qry.url = xstrdup(path); 453 | if (ctx.qry.raw) { 454 | qry = ctx.qry.raw; 455 | ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 456 | free(qry); 457 | } else 458 | ctx.qry.raw = ctx.qry.url; 459 | cgit_parse_url(ctx.qry.url); 460 | } 461 | } 462 | 463 | ttl = calc_ttl(); 464 | ctx.page.expires += ttl*60; 465 | if (ctx.cfg.nocache) 466 | ctx.cfg.cache_size = 0; 467 | err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 468 | ctx.qry.raw, ttl, process_request, &ctx); 469 | if (err) 470 | cgit_print_error(fmt("Error processing page: %s (%d)", 471 | strerror(err), err)); 472 | return err; 473 | } 474 | -------------------------------------------------------------------------------- /cgit.css: -------------------------------------------------------------------------------- 1 | body, table, form { 2 | padding: 0em; 3 | margin: 0em; 4 | } 5 | 6 | body { 7 | font-family: sans-serif; 8 | font-size: 10pt; 9 | color: #333; 10 | background: white; 11 | padding: 4px; 12 | } 13 | 14 | a { 15 | color: blue; 16 | text-decoration: none; 17 | } 18 | 19 | a:hover { 20 | text-decoration: underline; 21 | } 22 | 23 | table { 24 | border-collapse: collapse; 25 | } 26 | 27 | table#header { 28 | width: 100%; 29 | margin-bottom: 1em; 30 | } 31 | 32 | table#header td.logo { 33 | width: 96px; 34 | } 35 | 36 | table#header td.main { 37 | font-size: 250%; 38 | padding-left: 10px; 39 | white-space: nowrap; 40 | } 41 | 42 | table#header td.main a { 43 | color: #000; 44 | } 45 | 46 | table#header td.form { 47 | text-align: right; 48 | vertical-align: bottom; 49 | padding-right: 1em; 50 | padding-bottom: 2px; 51 | white-space: nowrap; 52 | } 53 | 54 | table#header td.form form, 55 | table#header td.form input, 56 | table#header td.form select { 57 | font-size: 90%; 58 | } 59 | 60 | table#header td.sub { 61 | color: #777; 62 | border-top: solid 1px #ccc; 63 | padding-left: 10px; 64 | } 65 | 66 | table.tabs { 67 | /* border-bottom: solid 2px #ccc; */ 68 | border-collapse: collapse; 69 | margin-top: 2em; 70 | margin-bottom: 0px; 71 | width: 100%; 72 | } 73 | 74 | table.tabs td { 75 | padding: 0px 1em; 76 | vertical-align: bottom; 77 | } 78 | 79 | table.tabs td a { 80 | padding: 2px 0.75em; 81 | color: #777; 82 | font-size: 110%; 83 | } 84 | 85 | table.tabs td a.active { 86 | color: #000; 87 | background-color: #ccc; 88 | } 89 | 90 | table.tabs td.form { 91 | text-align: right; 92 | } 93 | 94 | table.tabs td.form form { 95 | padding-bottom: 2px; 96 | font-size: 90%; 97 | white-space: nowrap; 98 | } 99 | 100 | table.tabs td.form input, 101 | table.tabs td.form select { 102 | font-size: 90%; 103 | } 104 | 105 | div.content { 106 | margin: 0px; 107 | padding: 2em; 108 | border-top: solid 3px #ccc; 109 | border-bottom: solid 3px #ccc; 110 | } 111 | 112 | 113 | table.list { 114 | width: 100%; 115 | border: none; 116 | border-collapse: collapse; 117 | } 118 | 119 | table.list tr { 120 | background: white; 121 | } 122 | 123 | table.list tr:hover { 124 | background: #eee; 125 | } 126 | 127 | table.list tr.nohover:hover { 128 | background: white; 129 | } 130 | 131 | table.list th { 132 | font-weight: bold; 133 | /* color: #888; 134 | border-top: dashed 1px #888; 135 | border-bottom: dashed 1px #888; 136 | */ 137 | padding: 0.1em 0.5em 0.05em 0.5em; 138 | vertical-align: baseline; 139 | } 140 | 141 | table.list td { 142 | border: none; 143 | padding: 0.1em 0.5em 0.1em 0.5em; 144 | } 145 | 146 | table.list td a { 147 | color: black; 148 | } 149 | 150 | table.list td a:hover { 151 | color: #00f; 152 | } 153 | 154 | img { 155 | border: none; 156 | } 157 | 158 | input#switch-btn { 159 | margin: 2px 0px 0px 0px; 160 | } 161 | 162 | td#sidebar input.txt { 163 | width: 100%; 164 | margin: 2px 0px 0px 0px; 165 | } 166 | 167 | table#grid { 168 | margin: 0px; 169 | } 170 | 171 | td#content { 172 | vertical-align: top; 173 | padding: 1em 2em 1em 1em; 174 | border: none; 175 | } 176 | 177 | div#summary { 178 | vertical-align: top; 179 | margin-bottom: 1em; 180 | } 181 | 182 | table#downloads { 183 | float: right; 184 | border-collapse: collapse; 185 | border: solid 1px #777; 186 | margin-left: 0.5em; 187 | margin-bottom: 0.5em; 188 | } 189 | 190 | table#downloads th { 191 | background-color: #ccc; 192 | } 193 | 194 | div#blob { 195 | border: solid 1px black; 196 | } 197 | 198 | div.error { 199 | color: red; 200 | font-weight: bold; 201 | margin: 1em 2em; 202 | } 203 | 204 | a.ls-blob, a.ls-dir, a.ls-mod { 205 | font-family: monospace; 206 | } 207 | 208 | td.ls-size { 209 | text-align: right; 210 | font-family: monospace; 211 | width: 10em; 212 | } 213 | 214 | td.ls-mode { 215 | font-family: monospace; 216 | width: 10em; 217 | } 218 | 219 | table.blob { 220 | margin-top: 0.5em; 221 | border-top: solid 1px black; 222 | } 223 | 224 | table.blob td.no { 225 | border-right: solid 1px black; 226 | color: black; 227 | background-color: #eee; 228 | text-align: right; 229 | } 230 | 231 | table.blob td.no a { 232 | color: black; 233 | } 234 | 235 | table.blob td.no a:hover { 236 | color: black; 237 | text-decoration: none; 238 | } 239 | 240 | table.blob td.txt { 241 | white-space: pre; 242 | font-family: monospace; 243 | padding-left: 0.5em; 244 | } 245 | 246 | table.nowrap td { 247 | white-space: nowrap; 248 | } 249 | 250 | table.commit-info { 251 | border-collapse: collapse; 252 | margin-top: 1.5em; 253 | } 254 | 255 | table.commit-info th { 256 | text-align: left; 257 | font-weight: normal; 258 | padding: 0.1em 1em 0.1em 0.1em; 259 | vertical-align: top; 260 | } 261 | 262 | table.commit-info td { 263 | font-weight: normal; 264 | padding: 0.1em 1em 0.1em 0.1em; 265 | } 266 | 267 | div.commit-subject { 268 | font-weight: bold; 269 | font-size: 125%; 270 | margin: 1.5em 0em 0.5em 0em; 271 | padding: 0em; 272 | } 273 | 274 | div.commit-msg { 275 | white-space: pre; 276 | font-family: monospace; 277 | } 278 | 279 | div.diffstat-header { 280 | font-weight: bold; 281 | padding-top: 1.5em; 282 | } 283 | 284 | table.diffstat { 285 | border-collapse: collapse; 286 | border: solid 1px #aaa; 287 | background-color: #eee; 288 | } 289 | 290 | table.diffstat th { 291 | font-weight: normal; 292 | text-align: left; 293 | text-decoration: underline; 294 | padding: 0.1em 1em 0.1em 0.1em; 295 | font-size: 100%; 296 | } 297 | 298 | table.diffstat td { 299 | padding: 0.2em 0.2em 0.1em 0.1em; 300 | font-size: 100%; 301 | border: none; 302 | } 303 | 304 | table.diffstat td.mode { 305 | white-space: nowrap; 306 | } 307 | 308 | table.diffstat td span.modechange { 309 | padding-left: 1em; 310 | color: red; 311 | } 312 | 313 | table.diffstat td.add a { 314 | color: green; 315 | } 316 | 317 | table.diffstat td.del a { 318 | color: red; 319 | } 320 | 321 | table.diffstat td.upd a { 322 | color: blue; 323 | } 324 | 325 | table.diffstat td.graph { 326 | width: 500px; 327 | vertical-align: middle; 328 | } 329 | 330 | table.diffstat td.graph table { 331 | border: none; 332 | } 333 | 334 | table.diffstat td.graph td { 335 | padding: 0px; 336 | border: 0px; 337 | height: 7pt; 338 | } 339 | 340 | table.diffstat td.graph td.add { 341 | background-color: #5c5; 342 | } 343 | 344 | table.diffstat td.graph td.rem { 345 | background-color: #c55; 346 | } 347 | 348 | div.diffstat-summary { 349 | color: #888; 350 | padding-top: 0.5em; 351 | } 352 | 353 | table.diff { 354 | width: 100%; 355 | } 356 | 357 | table.diff td { 358 | font-family: monospace; 359 | white-space: pre; 360 | } 361 | 362 | table.diff td div.head { 363 | font-weight: bold; 364 | margin-top: 1em; 365 | color: black; 366 | } 367 | 368 | table.diff td div.hunk { 369 | color: #009; 370 | } 371 | 372 | table.diff td div.add { 373 | color: green; 374 | } 375 | 376 | table.diff td div.del { 377 | color: red; 378 | } 379 | 380 | .sha1 { 381 | font-family: monospace; 382 | font-size: 90%; 383 | } 384 | 385 | .left { 386 | text-align: left; 387 | } 388 | 389 | .right { 390 | text-align: right; 391 | } 392 | 393 | table.list td.repogroup { 394 | font-style: italic; 395 | color: #888; 396 | } 397 | 398 | a.button { 399 | font-size: 80%; 400 | padding: 0em 0.5em; 401 | } 402 | 403 | a.primary { 404 | font-size: 100%; 405 | } 406 | 407 | a.secondary { 408 | font-size: 90%; 409 | } 410 | 411 | td.toplevel-repo { 412 | 413 | } 414 | 415 | table.list td.sublevel-repo { 416 | padding-left: 1.5em; 417 | } 418 | 419 | div.pager { 420 | text-align: center; 421 | margin: 1em 0em 0em 0em; 422 | } 423 | 424 | div.pager a { 425 | color: #777; 426 | margin: 0em 0.5em; 427 | } 428 | 429 | span.age-mins { 430 | font-weight: bold; 431 | color: #080; 432 | } 433 | 434 | span.age-hours { 435 | color: #080; 436 | } 437 | 438 | span.age-days { 439 | color: #040; 440 | } 441 | 442 | span.age-weeks { 443 | color: #444; 444 | } 445 | 446 | span.age-months { 447 | color: #888; 448 | } 449 | 450 | span.age-years { 451 | color: #bbb; 452 | } 453 | div.footer { 454 | margin-top: 0.5em; 455 | text-align: center; 456 | font-size: 80%; 457 | color: #ccc; 458 | } 459 | -------------------------------------------------------------------------------- /cgit.h: -------------------------------------------------------------------------------- 1 | #ifndef CGIT_H 2 | #define CGIT_H 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | 22 | /* 23 | * Dateformats used on misc. pages 24 | */ 25 | #define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)" 26 | #define FMT_SHORTDATE "%Y-%m-%d" 27 | #define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ" 28 | 29 | 30 | /* 31 | * Limits used for relative dates 32 | */ 33 | #define TM_MIN 60 34 | #define TM_HOUR (TM_MIN * 60) 35 | #define TM_DAY (TM_HOUR * 24) 36 | #define TM_WEEK (TM_DAY * 7) 37 | #define TM_YEAR (TM_DAY * 365) 38 | #define TM_MONTH (TM_YEAR / 12.0) 39 | 40 | 41 | /* 42 | * Default encoding 43 | */ 44 | #define PAGE_ENCODING "UTF-8" 45 | 46 | typedef void (*configfn)(const char *name, const char *value); 47 | typedef void (*filepair_fn)(struct diff_filepair *pair); 48 | typedef void (*linediff_fn)(char *line, int len); 49 | 50 | struct cgit_repo { 51 | char *url; 52 | char *name; 53 | char *path; 54 | char *desc; 55 | char *owner; 56 | char *defbranch; 57 | char *group; 58 | char *module_link; 59 | char *readme; 60 | char *clone_url; 61 | int snapshots; 62 | int enable_log_filecount; 63 | int enable_log_linecount; 64 | }; 65 | 66 | struct cgit_repolist { 67 | int length; 68 | int count; 69 | struct cgit_repo *repos; 70 | }; 71 | 72 | struct commitinfo { 73 | struct commit *commit; 74 | char *author; 75 | char *author_email; 76 | unsigned long author_date; 77 | char *committer; 78 | char *committer_email; 79 | unsigned long committer_date; 80 | char *subject; 81 | char *msg; 82 | char *msg_encoding; 83 | }; 84 | 85 | struct taginfo { 86 | char *tagger; 87 | char *tagger_email; 88 | unsigned long tagger_date; 89 | char *msg; 90 | }; 91 | 92 | struct refinfo { 93 | const char *refname; 94 | struct object *object; 95 | union { 96 | struct taginfo *tag; 97 | struct commitinfo *commit; 98 | }; 99 | }; 100 | 101 | struct reflist { 102 | struct refinfo **refs; 103 | int alloc; 104 | int count; 105 | }; 106 | 107 | struct cgit_query { 108 | int has_symref; 109 | int has_sha1; 110 | char *raw; 111 | char *repo; 112 | char *page; 113 | char *search; 114 | char *grep; 115 | char *head; 116 | char *sha1; 117 | char *sha2; 118 | char *path; 119 | char *name; 120 | char *mimetype; 121 | char *url; 122 | int ofs; 123 | int nohead; 124 | }; 125 | 126 | struct cgit_config { 127 | char *agefile; 128 | char *cache_root; 129 | char *clone_prefix; 130 | char *css; 131 | char *favicon; 132 | char *footer; 133 | char *index_header; 134 | char *index_info; 135 | char *logo; 136 | char *logo_link; 137 | char *module_link; 138 | char *repo_group; 139 | char *robots; 140 | char *root_title; 141 | char *root_desc; 142 | char *root_readme; 143 | char *script_name; 144 | char *virtual_root; 145 | int cache_size; 146 | int cache_dynamic_ttl; 147 | int cache_max_create_time; 148 | int cache_repo_ttl; 149 | int cache_root_ttl; 150 | int cache_static_ttl; 151 | int enable_index_links; 152 | int enable_log_filecount; 153 | int enable_log_linecount; 154 | int local_time; 155 | int max_repo_count; 156 | int max_commit_count; 157 | int max_lock_attempts; 158 | int max_msg_len; 159 | int max_repodesc_len; 160 | int nocache; 161 | int renamelimit; 162 | int snapshots; 163 | int summary_branches; 164 | int summary_log; 165 | int summary_tags; 166 | }; 167 | 168 | struct cgit_page { 169 | time_t modified; 170 | time_t expires; 171 | size_t size; 172 | char *mimetype; 173 | char *charset; 174 | char *filename; 175 | char *title; 176 | }; 177 | 178 | struct cgit_context { 179 | struct cgit_query qry; 180 | struct cgit_config cfg; 181 | struct cgit_repo *repo; 182 | struct cgit_page page; 183 | }; 184 | 185 | struct cgit_snapshot_format { 186 | const char *suffix; 187 | const char *mimetype; 188 | write_archive_fn_t write_func; 189 | int bit; 190 | }; 191 | 192 | extern const char *cgit_version; 193 | 194 | extern struct cgit_repolist cgit_repolist; 195 | extern struct cgit_context ctx; 196 | extern const struct cgit_snapshot_format cgit_snapshot_formats[]; 197 | 198 | extern struct cgit_repo *cgit_add_repo(const char *url); 199 | extern struct cgit_repo *cgit_get_repoinfo(const char *url); 200 | extern void cgit_repo_config_cb(const char *name, const char *value); 201 | 202 | extern int chk_zero(int result, char *msg); 203 | extern int chk_positive(int result, char *msg); 204 | extern int chk_non_negative(int result, char *msg); 205 | 206 | extern char *trim_end(const char *str, char c); 207 | extern char *strlpart(char *txt, int maxlen); 208 | extern char *strrpart(char *txt, int maxlen); 209 | 210 | extern void cgit_add_ref(struct reflist *list, struct refinfo *ref); 211 | extern int cgit_refs_cb(const char *refname, const unsigned char *sha1, 212 | int flags, void *cb_data); 213 | 214 | extern void *cgit_free_commitinfo(struct commitinfo *info); 215 | 216 | extern int cgit_diff_files(const unsigned char *old_sha1, 217 | const unsigned char *new_sha1, 218 | linediff_fn fn); 219 | 220 | extern void cgit_diff_tree(const unsigned char *old_sha1, 221 | const unsigned char *new_sha1, 222 | filepair_fn fn, const char *prefix); 223 | 224 | extern void cgit_diff_commit(struct commit *commit, filepair_fn fn); 225 | 226 | extern char *fmt(const char *format,...); 227 | 228 | extern struct commitinfo *cgit_parse_commit(struct commit *commit); 229 | extern struct taginfo *cgit_parse_tag(struct tag *tag); 230 | extern void cgit_parse_url(const char *url); 231 | 232 | extern const char *cgit_repobasename(const char *reponame); 233 | 234 | extern int cgit_parse_snapshots_mask(const char *str); 235 | 236 | /* libgit.a either links against or compiles its own implementation of 237 | * strcasestr(), and we'd like to reuse it. Simply re-declaring it 238 | * seems to do the trick. 239 | */ 240 | extern char *strcasestr(const char *haystack, const char *needle); 241 | 242 | 243 | #endif /* CGIT_H */ 244 | -------------------------------------------------------------------------------- /cgit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metajack/cgit/824e26c821e12a4de3d15343f929b4ed9f4317a6/cgit.png -------------------------------------------------------------------------------- /cgitrc.5.txt: -------------------------------------------------------------------------------- 1 | CGITRC 2 | ====== 3 | 4 | 5 | NAME 6 | ---- 7 | cgitrc - runtime configuration for cgit 8 | 9 | 10 | DESCRIPTION 11 | ----------- 12 | Cgitrc contains all runtime settings for cgit, including the list of git 13 | repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank 14 | lines, and lines starting with '#', are ignored. 15 | 16 | 17 | GLOBAL SETTINGS 18 | --------------- 19 | agefile 20 | Specifies a path, relative to each repository path, which can be used 21 | to specify the date and time of the youngest commit in the repository. 22 | The first line in the file is used as input to the "parse_date" 23 | function in libgit. Recommended timestamp-format is "yyyy-mm-dd 24 | hh:mm:ss". Default value: "info/web/last-modified". 25 | 26 | cache-root 27 | Path used to store the cgit cache entries. Default value: 28 | "/var/cache/cgit". 29 | 30 | cache-dynamic-ttl 31 | Number which specifies the time-to-live, in minutes, for the cached 32 | version of repository pages accessed without a fixed SHA1. Default 33 | value: "5". 34 | 35 | cache-repo-ttl 36 | Number which specifies the time-to-live, in minutes, for the cached 37 | version of the repository summary page. Default value: "5". 38 | 39 | cache-root-ttl 40 | Number which specifies the time-to-live, in minutes, for the cached 41 | version of the repository index page. Default value: "5". 42 | 43 | cache-size 44 | The maximum number of entries in the cgit cache. Default value: "0" 45 | (i.e. caching is disabled). 46 | 47 | cache-static-ttl 48 | Number which specifies the time-to-live, in minutes, for the cached 49 | version of repository pages accessed with a fixed SHA1. Default value: 50 | "5". 51 | 52 | clone-prefix 53 | Space-separated list of common prefixes which, when combined with a 54 | repository url, generates valid clone urls for the repository. This 55 | setting is only used if `repo.clone-url` is unspecified. Default value: 56 | none. 57 | 58 | css 59 | Url which specifies the css document to include in all cgit pages. 60 | Default value: "/cgit.css". 61 | 62 | enable-index-links 63 | Flag which, when set to "1", will make cgit generate extra links for 64 | each repo in the repository index (specifically, to the "summary", 65 | "commit" and "tree" pages). Default value: "0". 66 | 67 | enable-log-filecount 68 | Flag which, when set to "1", will make cgit print the number of 69 | modified files for each commit on the repository log page. Default 70 | value: "0". 71 | 72 | enable-log-linecount 73 | Flag which, when set to "1", will make cgit print the number of added 74 | and removed lines for each commit on the repository log page. Default 75 | value: "0". 76 | 77 | favicon 78 | Url used as link to a shortcut icon for cgit. If specified, it is 79 | suggested to use the value "/favicon.ico" since certain browsers will 80 | ignore other values. Default value: none. 81 | 82 | footer 83 | The content of the file specified with this option will be included 84 | verbatim at the bottom of all pages (i.e. it replaces the standard 85 | "generated by..." message. Default value: none. 86 | 87 | include 88 | Name of a configfile to include before the rest of the current config- 89 | file is parsed. Default value: none. 90 | 91 | index-header 92 | The content of the file specified with this option will be included 93 | verbatim above the repository index. This setting is deprecated, and 94 | will not be supported by cgit-1.0 (use root-readme instead). Default 95 | value: none. 96 | 97 | index-info 98 | The content of the file specified with this option will be included 99 | verbatim below the heading on the repository index page. This setting 100 | is deprecated, and will not be supported by cgit-1.0 (use root-desc 101 | instead). Default value: none. 102 | 103 | local-time 104 | Flag which, if set to "1", makes cgit print commit and tag times in the 105 | servers timezone. Default value: "0". 106 | 107 | logo 108 | Url which specifies the source of an image which will be used as a logo 109 | on all cgit pages. 110 | 111 | logo-link 112 | Url loaded when clicking on the cgit logo image. If unspecified the 113 | calculated url of the repository index page will be used. Default 114 | value: none. 115 | 116 | max-commit-count 117 | Specifies the number of entries to list per page in "log" view. Default 118 | value: "50". 119 | 120 | max-message-length 121 | Specifies the maximum number of commit message characters to display in 122 | "log" view. Default value: "80". 123 | 124 | max-repo-count 125 | Specifies the number of entries to list per page on the repository 126 | index page. Default value: "50". 127 | 128 | max-repodesc-length 129 | Specifies the maximum number of repo description characters to display 130 | on the repository index page. Default value: "80". 131 | 132 | module-link 133 | Text which will be used as the formatstring for a hyperlink when a 134 | submodule is printed in a directory listing. The arguments for the 135 | formatstring are the path and SHA1 of the submodule commit. Default 136 | value: "./?repo=%s&page=commit&id=%s" 137 | 138 | nocache 139 | If set to the value "1" caching will be disabled. This settings is 140 | deprecated, and will not be honored starting with cgit-1.0. Default 141 | value: "0". 142 | 143 | renamelimit 144 | Maximum number of files to consider when detecting renames. The value 145 | "-1" uses the compiletime value in git (for further info, look at 146 | `man git-diff`). Default value: "-1". 147 | 148 | repo.group 149 | A value for the current repository group, which all repositories 150 | specified after this setting will inherit. Default value: none. 151 | 152 | robots 153 | Text used as content for the "robots" meta-tag. Default value: 154 | "index, nofollow". 155 | 156 | root-desc 157 | Text printed below the heading on the repository index page. Default 158 | value: "a fast webinterface for the git dscm". 159 | 160 | root-readme: 161 | The content of the file specified with this option will be included 162 | verbatim below the "about" link on the repository index page. Default 163 | value: none. 164 | 165 | root-title 166 | Text printed as heading on the repository index page. Default value: 167 | "Git Repository Browser". 168 | 169 | snapshots 170 | Text which specifies the default (and allowed) set of snapshot formats 171 | supported by cgit. The value is a space-separated list of zero or more 172 | of the following values: 173 | "tar" uncompressed tar-file 174 | "tar.gz" gzip-compressed tar-file 175 | "tar.bz2" bzip-compressed tar-file 176 | "zip" zip-file 177 | Default value: none. 178 | 179 | summary-branches 180 | Specifies the number of branches to display in the repository "summary" 181 | view. Default value: "10". 182 | 183 | summary-log 184 | Specifies the number of log entries to display in the repository 185 | "summary" view. Default value: "10". 186 | 187 | summary-tags 188 | Specifies the number of tags to display in the repository "summary" 189 | view. Default value: "10". 190 | 191 | virtual-root 192 | Url which, if specified, will be used as root for all cgit links. It 193 | will also cause cgit to generate 'virtual urls', i.e. urls like 194 | '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default 195 | value: none. 196 | NOTE: cgit has recently learned how to use PATH_INFO to achieve the 197 | same kind of virtual urls, so this option will probably be deprecated. 198 | 199 | REPOSITORY SETTINGS 200 | ------------------- 201 | repo.clone-url 202 | A list of space-separated urls which can be used to clone this repo. 203 | Default value: none. 204 | 205 | repo.defbranch 206 | The name of the default branch for this repository. If no such branch 207 | exists in the repository, the first branch name (when sorted) is used 208 | as default instead. Default value: "master". 209 | 210 | repo.desc 211 | The value to show as repository description. Default value: none. 212 | 213 | repo.enable-log-filecount 214 | A flag which can be used to disable the global setting 215 | `enable-log-filecount'. Default value: none. 216 | 217 | repo.enable-log-linecount 218 | A flag which can be used to disable the global setting 219 | `enable-log-linecount'. Default value: none. 220 | 221 | repo.name 222 | The value to show as repository name. Default value: . 223 | 224 | repo.owner 225 | A value used to identify the owner of the repository. Default value: 226 | none. 227 | 228 | repo.path 229 | An absolute path to the repository directory. For non-bare repositories 230 | this is the .git-directory. Default value: none. 231 | 232 | repo.readme 233 | A path (relative to ) which specifies a file to include 234 | verbatim as the "About" page for this repo. Default value: none. 235 | 236 | repo.snapshots 237 | A mask of allowed snapshot-formats for this repo, restricted by the 238 | "snapshots" global setting. Default value: . 239 | 240 | repo.url 241 | The relative url used to access the repository. This must be the first 242 | setting specified for each repo. Default value: none. 243 | 244 | 245 | EXAMPLE CGITRC FILE 246 | ------------------- 247 | 248 | # Enable caching of up to 1000 output entriess 249 | cache-size=1000 250 | 251 | 252 | # Specify some default clone prefixes 253 | clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git 254 | 255 | # Specify the css url 256 | css=/css/cgit.css 257 | 258 | 259 | # Show extra links for each repository on the index page 260 | enable-index-links=1 261 | 262 | 263 | # Show number of affected files per commit on the log pages 264 | enable-log-filecount=1 265 | 266 | 267 | # Show number of added/removed lines per commit on the log pages 268 | enable-log-linecount=1 269 | 270 | 271 | # Add a cgit favicon 272 | favicon=/favicon.ico 273 | 274 | 275 | # Use a custom logo 276 | logo=/img/mylogo.png 277 | 278 | 279 | # Set the title and heading of the repository index page 280 | root-title=foobar.com git repositories 281 | 282 | 283 | # Set a subheading for the repository index page 284 | root-desc=tracking the foobar development 285 | 286 | 287 | # Include some more info about foobar.com on the index page 288 | root-readme=/var/www/htdocs/about.html 289 | 290 | 291 | # Allow download of tar.gz, tar.bz and zip-files 292 | snapshots=tar.gz tar.bz zip 293 | 294 | 295 | ## 296 | ## List of repositories. 297 | ## PS: Any repositories listed when repo.group is unset will not be 298 | ## displayed under a group heading 299 | ## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos') 300 | ## and included like this: 301 | ## include=/etc/cgitrepos 302 | ## 303 | 304 | 305 | repo.url=foo 306 | repo.path=/pub/git/foo.git 307 | repo.desc=the master foo repository 308 | repo.owner=fooman@foobar.com 309 | repo.readme=info/web/about.html 310 | 311 | 312 | repo.url=bar 313 | repo.path=/pub/git/bar.git 314 | repo.desc=the bars for your foo 315 | repo.owner=barman@foobar.com 316 | repo.readme=info/web/about.html 317 | 318 | 319 | # The next repositories will be displayed under the 'extras' heading 320 | repo.group=extras 321 | 322 | 323 | repo.url=baz 324 | repo.path=/pub/git/baz.git 325 | repo.desc=a set of extensions for bar users 326 | 327 | repo.url=wiz 328 | repo.path=/pub/git/wiz.git 329 | repo.desc=the wizard of foo 330 | 331 | 332 | # Add some mirrored repositories 333 | repo.group=mirrors 334 | 335 | 336 | repo.url=git 337 | repo.path=/pub/git/git.git 338 | repo.desc=the dscm 339 | 340 | 341 | repo.url=linux 342 | repo.path=/pub/git/linux.git 343 | repo.desc=the kernel 344 | 345 | # Disable adhoc downloads of this repo 346 | repo.snapshots=0 347 | 348 | # Disable line-counts for this repo 349 | repo.enable-log-linecount=0 350 | 351 | 352 | BUGS 353 | ---- 354 | Comments currently cannot appear on the same line as a setting; the comment 355 | will be included as part of the value. E.g. this line: 356 | 357 | robots=index # allow indexing 358 | 359 | will generate the following html element: 360 | 361 | 362 | 363 | 364 | 365 | AUTHOR 366 | ------ 367 | Lars Hjemli 368 | -------------------------------------------------------------------------------- /cmd.c: -------------------------------------------------------------------------------- 1 | /* cmd.c: the cgit command dispatcher 2 | * 3 | * Copyright (C) 2008 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "cmd.h" 11 | #include "cache.h" 12 | #include "ui-shared.h" 13 | #include "ui-atom.h" 14 | #include "ui-blob.h" 15 | #include "ui-clone.h" 16 | #include "ui-commit.h" 17 | #include "ui-diff.h" 18 | #include "ui-log.h" 19 | #include "ui-patch.h" 20 | #include "ui-plain.h" 21 | #include "ui-refs.h" 22 | #include "ui-repolist.h" 23 | #include "ui-snapshot.h" 24 | #include "ui-summary.h" 25 | #include "ui-tag.h" 26 | #include "ui-tree.h" 27 | 28 | static void HEAD_fn(struct cgit_context *ctx) 29 | { 30 | cgit_clone_head(ctx); 31 | } 32 | 33 | static void atom_fn(struct cgit_context *ctx) 34 | { 35 | cgit_print_atom(ctx->qry.head, ctx->qry.path, 10); 36 | } 37 | 38 | static void about_fn(struct cgit_context *ctx) 39 | { 40 | if (ctx->repo) 41 | cgit_print_repo_readme(); 42 | else 43 | cgit_print_site_readme(); 44 | } 45 | 46 | static void blob_fn(struct cgit_context *ctx) 47 | { 48 | cgit_print_blob(ctx->qry.sha1, ctx->qry.path, ctx->qry.head); 49 | } 50 | 51 | static void commit_fn(struct cgit_context *ctx) 52 | { 53 | cgit_print_commit(ctx->qry.sha1); 54 | } 55 | 56 | static void diff_fn(struct cgit_context *ctx) 57 | { 58 | cgit_print_diff(ctx->qry.sha1, ctx->qry.sha2, ctx->qry.path); 59 | } 60 | 61 | static void info_fn(struct cgit_context *ctx) 62 | { 63 | cgit_clone_info(ctx); 64 | } 65 | 66 | static void log_fn(struct cgit_context *ctx) 67 | { 68 | cgit_print_log(ctx->qry.sha1, ctx->qry.ofs, ctx->cfg.max_commit_count, 69 | ctx->qry.grep, ctx->qry.search, ctx->qry.path, 1); 70 | } 71 | 72 | static void ls_cache_fn(struct cgit_context *ctx) 73 | { 74 | ctx->page.mimetype = "text/plain"; 75 | ctx->page.filename = "ls-cache.txt"; 76 | cgit_print_http_headers(ctx); 77 | cache_ls(ctx->cfg.cache_root); 78 | } 79 | 80 | static void objects_fn(struct cgit_context *ctx) 81 | { 82 | cgit_clone_objects(ctx); 83 | } 84 | 85 | static void repolist_fn(struct cgit_context *ctx) 86 | { 87 | cgit_print_repolist(); 88 | } 89 | 90 | static void patch_fn(struct cgit_context *ctx) 91 | { 92 | cgit_print_patch(ctx->qry.sha1); 93 | } 94 | 95 | static void plain_fn(struct cgit_context *ctx) 96 | { 97 | cgit_print_plain(ctx); 98 | } 99 | 100 | static void refs_fn(struct cgit_context *ctx) 101 | { 102 | cgit_print_refs(); 103 | } 104 | 105 | static void snapshot_fn(struct cgit_context *ctx) 106 | { 107 | cgit_print_snapshot(ctx->qry.head, ctx->qry.sha1, 108 | cgit_repobasename(ctx->repo->url), ctx->qry.path, 109 | ctx->repo->snapshots, ctx->qry.nohead); 110 | } 111 | 112 | static void summary_fn(struct cgit_context *ctx) 113 | { 114 | cgit_print_summary(); 115 | } 116 | 117 | static void tag_fn(struct cgit_context *ctx) 118 | { 119 | cgit_print_tag(ctx->qry.sha1); 120 | } 121 | 122 | static void tree_fn(struct cgit_context *ctx) 123 | { 124 | cgit_print_tree(ctx->qry.sha1, ctx->qry.path); 125 | } 126 | 127 | #define def_cmd(name, want_repo, want_layout) \ 128 | {#name, name##_fn, want_repo, want_layout} 129 | 130 | struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx) 131 | { 132 | static struct cgit_cmd cmds[] = { 133 | def_cmd(HEAD, 1, 0), 134 | def_cmd(atom, 1, 0), 135 | def_cmd(about, 0, 1), 136 | def_cmd(blob, 1, 0), 137 | def_cmd(commit, 1, 1), 138 | def_cmd(diff, 1, 1), 139 | def_cmd(info, 1, 0), 140 | def_cmd(log, 1, 1), 141 | def_cmd(ls_cache, 0, 0), 142 | def_cmd(objects, 1, 0), 143 | def_cmd(patch, 1, 0), 144 | def_cmd(plain, 1, 0), 145 | def_cmd(refs, 1, 1), 146 | def_cmd(repolist, 0, 0), 147 | def_cmd(snapshot, 1, 0), 148 | def_cmd(summary, 1, 1), 149 | def_cmd(tag, 1, 1), 150 | def_cmd(tree, 1, 1), 151 | }; 152 | int i; 153 | 154 | if (ctx->qry.page == NULL) { 155 | if (ctx->repo) 156 | ctx->qry.page = "summary"; 157 | else 158 | ctx->qry.page = "repolist"; 159 | } 160 | 161 | for(i = 0; i < sizeof(cmds)/sizeof(*cmds); i++) 162 | if (!strcmp(ctx->qry.page, cmds[i].name)) 163 | return &cmds[i]; 164 | return NULL; 165 | } 166 | -------------------------------------------------------------------------------- /cmd.h: -------------------------------------------------------------------------------- 1 | #ifndef CMD_H 2 | #define CMD_H 3 | 4 | typedef void (*cgit_cmd_fn)(struct cgit_context *ctx); 5 | 6 | struct cgit_cmd { 7 | const char *name; 8 | cgit_cmd_fn fn; 9 | unsigned int want_repo:1, 10 | want_layout:1; 11 | }; 12 | 13 | extern struct cgit_cmd *cgit_get_cmd(struct cgit_context *ctx); 14 | 15 | #endif /* CMD_H */ 16 | -------------------------------------------------------------------------------- /configfile.c: -------------------------------------------------------------------------------- 1 | /* configfile.c: parsing of config files 2 | * 3 | * Copyright (C) 2008 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include 10 | #include 11 | #include "configfile.h" 12 | 13 | int next_char(FILE *f) 14 | { 15 | int c = fgetc(f); 16 | if (c=='\r') { 17 | c = fgetc(f); 18 | if (c!='\n') { 19 | ungetc(c, f); 20 | c = '\r'; 21 | } 22 | } 23 | return c; 24 | } 25 | 26 | void skip_line(FILE *f) 27 | { 28 | int c; 29 | 30 | while((c=next_char(f)) && c!='\n' && c!=EOF) 31 | ; 32 | } 33 | 34 | int read_config_line(FILE *f, char *line, const char **value, int bufsize) 35 | { 36 | int i = 0, isname = 0; 37 | 38 | *value = NULL; 39 | while(i 8) 77 | return -1; 78 | if (!(f = fopen(filename, "r"))) 79 | return -1; 80 | nesting++; 81 | while((len = read_config_line(f, line, &value, sizeof(line))) > 0) 82 | fn(line, value); 83 | nesting--; 84 | fclose(f); 85 | return 0; 86 | } 87 | 88 | -------------------------------------------------------------------------------- /configfile.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIGFILE_H 2 | #define CONFIGFILE_H 3 | 4 | typedef void (*configfile_value_fn)(const char *name, const char *value); 5 | 6 | extern int parse_configfile(const char *filename, configfile_value_fn fn); 7 | 8 | #endif /* CONFIGFILE_H */ 9 | -------------------------------------------------------------------------------- /gen-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Get version-info specified in Makefile 4 | V=$1 5 | 6 | # Use `git describe` to get current version if we're inside a git repo 7 | if test -d .git 8 | then 9 | V=$(git describe --abbrev=4 HEAD 2>/dev/null) 10 | fi 11 | 12 | new="CGIT_VERSION = $V" 13 | old=$(cat VERSION 2>/dev/null) 14 | 15 | # Exit if VERSION is uptodate 16 | test "$old" = "$new" && exit 0 17 | 18 | # Update VERSION with new version-info 19 | echo "$new" > VERSION 20 | cat VERSION 21 | -------------------------------------------------------------------------------- /html.c: -------------------------------------------------------------------------------- 1 | /* html.c: helper functions for html output 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | int htmlfd = STDOUT_FILENO; 17 | 18 | char *fmt(const char *format, ...) 19 | { 20 | static char buf[8][1024]; 21 | static int bufidx; 22 | int len; 23 | va_list args; 24 | 25 | bufidx++; 26 | bufidx &= 7; 27 | 28 | va_start(args, format); 29 | len = vsnprintf(buf[bufidx], sizeof(buf[bufidx]), format, args); 30 | va_end(args); 31 | if (len>sizeof(buf[bufidx])) { 32 | fprintf(stderr, "[html.c] string truncated: %s\n", format); 33 | exit(1); 34 | } 35 | return buf[bufidx]; 36 | } 37 | 38 | void html_raw(const char *data, size_t size) 39 | { 40 | write(htmlfd, data, size); 41 | } 42 | 43 | void html(const char *txt) 44 | { 45 | write(htmlfd, txt, strlen(txt)); 46 | } 47 | 48 | void htmlf(const char *format, ...) 49 | { 50 | static char buf[65536]; 51 | va_list args; 52 | 53 | va_start(args, format); 54 | vsnprintf(buf, sizeof(buf), format, args); 55 | va_end(args); 56 | html(buf); 57 | } 58 | 59 | void html_status(int code, const char *msg, int more_headers) 60 | { 61 | htmlf("Status: %d %s\n", code, msg); 62 | if (!more_headers) 63 | html("\n"); 64 | } 65 | 66 | void html_txt(char *txt) 67 | { 68 | char *t = txt; 69 | while(t && *t){ 70 | int c = *t; 71 | if (c=='<' || c=='>' || c=='&') { 72 | write(htmlfd, txt, t - txt); 73 | if (c=='>') 74 | html(">"); 75 | else if (c=='<') 76 | html("<"); 77 | else if (c=='&') 78 | html("&"); 79 | txt = t+1; 80 | } 81 | t++; 82 | } 83 | if (t!=txt) 84 | html(txt); 85 | } 86 | 87 | void html_ntxt(int len, char *txt) 88 | { 89 | char *t = txt; 90 | while(t && *t && len--){ 91 | int c = *t; 92 | if (c=='<' || c=='>' || c=='&') { 93 | write(htmlfd, txt, t - txt); 94 | if (c=='>') 95 | html(">"); 96 | else if (c=='<') 97 | html("<"); 98 | else if (c=='&') 99 | html("&"); 100 | txt = t+1; 101 | } 102 | t++; 103 | } 104 | if (t!=txt) 105 | write(htmlfd, txt, t - txt); 106 | if (len<0) 107 | html("..."); 108 | } 109 | 110 | void html_attr(char *txt) 111 | { 112 | char *t = txt; 113 | while(t && *t){ 114 | int c = *t; 115 | if (c=='<' || c=='>' || c=='\'') { 116 | write(htmlfd, txt, t - txt); 117 | if (c=='>') 118 | html(">"); 119 | else if (c=='<') 120 | html("<"); 121 | else if (c=='\'') 122 | html(""e;"); 123 | txt = t+1; 124 | } 125 | t++; 126 | } 127 | if (t!=txt) 128 | html(txt); 129 | } 130 | 131 | void html_url_path(char *txt) 132 | { 133 | char *t = txt; 134 | while(t && *t){ 135 | int c = *t; 136 | if (c=='"' || c=='#' || c=='\'' || c=='?') { 137 | write(htmlfd, txt, t - txt); 138 | write(htmlfd, fmt("%%%2x", c), 3); 139 | txt = t+1; 140 | } 141 | t++; 142 | } 143 | if (t!=txt) 144 | html(txt); 145 | } 146 | 147 | void html_url_arg(char *txt) 148 | { 149 | char *t = txt; 150 | while(t && *t){ 151 | int c = *t; 152 | if (c=='"' || c=='#' || c=='%' || c=='&' || c=='\'' || c=='+' || c=='?') { 153 | write(htmlfd, txt, t - txt); 154 | write(htmlfd, fmt("%%%2x", c), 3); 155 | txt = t+1; 156 | } 157 | t++; 158 | } 159 | if (t!=txt) 160 | html(txt); 161 | } 162 | 163 | void html_hidden(char *name, char *value) 164 | { 165 | html(""); 170 | } 171 | 172 | void html_option(char *value, char *text, char *selected_value) 173 | { 174 | html("\n"); 182 | } 183 | 184 | void html_link_open(char *url, char *title, char *class) 185 | { 186 | html(""); 197 | } 198 | 199 | void html_link_close(void) 200 | { 201 | html(""); 202 | } 203 | 204 | void html_fileperm(unsigned short mode) 205 | { 206 | htmlf("%c%c%c", (mode & 4 ? 'r' : '-'), 207 | (mode & 2 ? 'w' : '-'), (mode & 1 ? 'x' : '-')); 208 | } 209 | 210 | int html_include(const char *filename) 211 | { 212 | FILE *f; 213 | char buf[4096]; 214 | size_t len; 215 | 216 | if (!(f = fopen(filename, "r"))) { 217 | fprintf(stderr, "[cgit] Failed to include file %s: %s (%d).\n", 218 | filename, strerror(errno), errno); 219 | return -1; 220 | } 221 | while((len = fread(buf, 1, 4096, f)) > 0) 222 | write(htmlfd, buf, len); 223 | fclose(f); 224 | return 0; 225 | } 226 | 227 | int hextoint(char c) 228 | { 229 | if (c >= 'a' && c <= 'f') 230 | return 10 + c - 'a'; 231 | else if (c >= 'A' && c <= 'F') 232 | return 10 + c - 'A'; 233 | else if (c >= '0' && c <= '9') 234 | return c - '0'; 235 | else 236 | return -1; 237 | } 238 | 239 | char *convert_query_hexchar(char *txt) 240 | { 241 | int d1, d2; 242 | if (strlen(txt) < 3) { 243 | *txt = '\0'; 244 | return txt-1; 245 | } 246 | d1 = hextoint(*(txt+1)); 247 | d2 = hextoint(*(txt+2)); 248 | if (d1<0 || d2<0) { 249 | strcpy(txt, txt+3); 250 | return txt-1; 251 | } else { 252 | *txt = d1 * 16 + d2; 253 | strcpy(txt+1, txt+3); 254 | return txt; 255 | } 256 | } 257 | 258 | int http_parse_querystring(char *txt, void (*fn)(const char *name, const char *value)) 259 | { 260 | char *t, *value = NULL, c; 261 | 262 | if (!txt) 263 | return 0; 264 | 265 | t = txt = strdup(txt); 266 | if (t == NULL) { 267 | printf("Out of memory\n"); 268 | exit(1); 269 | } 270 | while((c=*t) != '\0') { 271 | if (c=='=') { 272 | *t = '\0'; 273 | value = t+1; 274 | } else if (c=='+') { 275 | *t = ' '; 276 | } else if (c=='%') { 277 | t = convert_query_hexchar(t); 278 | } else if (c=='&') { 279 | *t = '\0'; 280 | (*fn)(txt, value); 281 | txt = t+1; 282 | value = NULL; 283 | } 284 | t++; 285 | } 286 | if (t!=txt) 287 | (*fn)(txt, value); 288 | return 0; 289 | } 290 | -------------------------------------------------------------------------------- /html.h: -------------------------------------------------------------------------------- 1 | #ifndef HTML_H 2 | #define HTML_H 3 | 4 | extern int htmlfd; 5 | 6 | extern void html_raw(const char *txt, size_t size); 7 | extern void html(const char *txt); 8 | extern void htmlf(const char *format,...); 9 | extern void html_status(int code, const char *msg, int more_headers); 10 | extern void html_txt(char *txt); 11 | extern void html_ntxt(int len, char *txt); 12 | extern void html_attr(char *txt); 13 | extern void html_url_path(char *txt); 14 | extern void html_url_arg(char *txt); 15 | extern void html_hidden(char *name, char *value); 16 | extern void html_option(char *value, char *text, char *selected_value); 17 | extern void html_link_open(char *url, char *title, char *class); 18 | extern void html_link_close(void); 19 | extern void html_fileperm(unsigned short mode); 20 | extern int html_include(const char *filename); 21 | 22 | extern int http_parse_querystring(char *txt, void (*fn)(const char *name, const char *value)); 23 | 24 | #endif /* HTML_H */ 25 | -------------------------------------------------------------------------------- /parsing.c: -------------------------------------------------------------------------------- 1 | /* config.c: parsing of config files 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | 11 | /* 12 | * url syntax: [repo ['/' cmd [ '/' path]]] 13 | * repo: any valid repo url, may contain '/' 14 | * cmd: log | commit | diff | tree | view | blob | snapshot 15 | * path: any valid path, may contain '/' 16 | * 17 | */ 18 | void cgit_parse_url(const char *url) 19 | { 20 | char *cmd, *p; 21 | 22 | ctx.repo = NULL; 23 | if (!url || url[0] == '\0') 24 | return; 25 | 26 | ctx.repo = cgit_get_repoinfo(url); 27 | if (ctx.repo) { 28 | ctx.qry.repo = ctx.repo->url; 29 | return; 30 | } 31 | 32 | cmd = strchr(url, '/'); 33 | while (!ctx.repo && cmd) { 34 | cmd[0] = '\0'; 35 | ctx.repo = cgit_get_repoinfo(url); 36 | if (ctx.repo == NULL) { 37 | cmd[0] = '/'; 38 | cmd = strchr(cmd + 1, '/'); 39 | continue; 40 | } 41 | 42 | ctx.qry.repo = ctx.repo->url; 43 | p = strchr(cmd + 1, '/'); 44 | if (p) { 45 | p[0] = '\0'; 46 | if (p[1]) 47 | ctx.qry.path = trim_end(p + 1, '/'); 48 | } 49 | if (cmd[1]) 50 | ctx.qry.page = xstrdup(cmd + 1); 51 | return; 52 | } 53 | } 54 | 55 | char *substr(const char *head, const char *tail) 56 | { 57 | char *buf; 58 | 59 | buf = xmalloc(tail - head + 1); 60 | strncpy(buf, head, tail - head); 61 | buf[tail - head] = '\0'; 62 | return buf; 63 | } 64 | 65 | char *parse_user(char *t, char **name, char **email, unsigned long *date) 66 | { 67 | char *p = t; 68 | int mode = 1; 69 | 70 | while (p && *p) { 71 | if (mode == 1 && *p == '<') { 72 | *name = substr(t, p - 1); 73 | t = p; 74 | mode++; 75 | } else if (mode == 1 && *p == '\n') { 76 | *name = substr(t, p); 77 | p++; 78 | break; 79 | } else if (mode == 2 && *p == '>') { 80 | *email = substr(t, p + 1); 81 | t = p; 82 | mode++; 83 | } else if (mode == 2 && *p == '\n') { 84 | *email = substr(t, p); 85 | p++; 86 | break; 87 | } else if (mode == 3 && isdigit(*p)) { 88 | *date = atol(p); 89 | mode++; 90 | } else if (*p == '\n') { 91 | p++; 92 | break; 93 | } 94 | p++; 95 | } 96 | return p; 97 | } 98 | 99 | const char *reencode(char **txt, const char *src_enc, const char *dst_enc) 100 | { 101 | char *tmp; 102 | 103 | if (!txt || !*txt || !src_enc || !dst_enc) 104 | return *txt; 105 | 106 | tmp = reencode_string(*txt, src_enc, dst_enc); 107 | if (tmp) { 108 | free(*txt); 109 | *txt = tmp; 110 | } 111 | return *txt; 112 | } 113 | 114 | struct commitinfo *cgit_parse_commit(struct commit *commit) 115 | { 116 | struct commitinfo *ret; 117 | char *p = commit->buffer, *t = commit->buffer; 118 | 119 | ret = xmalloc(sizeof(*ret)); 120 | ret->commit = commit; 121 | ret->author = NULL; 122 | ret->author_email = NULL; 123 | ret->committer = NULL; 124 | ret->committer_email = NULL; 125 | ret->subject = NULL; 126 | ret->msg = NULL; 127 | ret->msg_encoding = NULL; 128 | 129 | if (p == NULL) 130 | return ret; 131 | 132 | if (strncmp(p, "tree ", 5)) 133 | die("Bad commit: %s", sha1_to_hex(commit->object.sha1)); 134 | else 135 | p += 46; // "tree " + hex[40] + "\n" 136 | 137 | while (!strncmp(p, "parent ", 7)) 138 | p += 48; // "parent " + hex[40] + "\n" 139 | 140 | if (p && !strncmp(p, "author ", 7)) { 141 | p = parse_user(p + 7, &ret->author, &ret->author_email, 142 | &ret->author_date); 143 | } 144 | 145 | if (p && !strncmp(p, "committer ", 9)) { 146 | p = parse_user(p + 9, &ret->committer, &ret->committer_email, 147 | &ret->committer_date); 148 | } 149 | 150 | if (p && !strncmp(p, "encoding ", 9)) { 151 | p += 9; 152 | t = strchr(p, '\n'); 153 | if (t) { 154 | ret->msg_encoding = substr(p, t + 1); 155 | p = t + 1; 156 | } 157 | } 158 | 159 | // skip unknown header fields 160 | while (p && *p && (*p != '\n')) { 161 | p = strchr(p, '\n'); 162 | if (p) 163 | p++; 164 | } 165 | 166 | // skip empty lines between headers and message 167 | while (p && *p == '\n') 168 | p++; 169 | 170 | if (!p) 171 | return ret; 172 | 173 | t = strchr(p, '\n'); 174 | if (t) { 175 | ret->subject = substr(p, t); 176 | p = t + 1; 177 | 178 | while (p && *p == '\n') { 179 | p = strchr(p, '\n'); 180 | if (p) 181 | p++; 182 | } 183 | if (p) 184 | ret->msg = xstrdup(p); 185 | } else 186 | ret->subject = xstrdup(p); 187 | 188 | if (ret->msg_encoding) { 189 | reencode(&ret->subject, PAGE_ENCODING, ret->msg_encoding); 190 | reencode(&ret->msg, PAGE_ENCODING, ret->msg_encoding); 191 | } 192 | 193 | return ret; 194 | } 195 | 196 | 197 | struct taginfo *cgit_parse_tag(struct tag *tag) 198 | { 199 | void *data; 200 | enum object_type type; 201 | unsigned long size; 202 | char *p; 203 | struct taginfo *ret; 204 | 205 | data = read_sha1_file(tag->object.sha1, &type, &size); 206 | if (!data || type != OBJ_TAG) { 207 | free(data); 208 | return 0; 209 | } 210 | 211 | ret = xmalloc(sizeof(*ret)); 212 | ret->tagger = NULL; 213 | ret->tagger_email = NULL; 214 | ret->tagger_date = 0; 215 | ret->msg = NULL; 216 | 217 | p = data; 218 | 219 | while (p && *p) { 220 | if (*p == '\n') 221 | break; 222 | 223 | if (!strncmp(p, "tagger ", 7)) { 224 | p = parse_user(p + 7, &ret->tagger, &ret->tagger_email, 225 | &ret->tagger_date); 226 | } else { 227 | p = strchr(p, '\n'); 228 | if (p) 229 | p++; 230 | } 231 | } 232 | 233 | // skip empty lines between headers and message 234 | while (p && *p == '\n') 235 | p++; 236 | 237 | if (p && *p) 238 | ret->msg = xstrdup(p); 239 | free(data); 240 | return ret; 241 | } 242 | -------------------------------------------------------------------------------- /scan-tree.c: -------------------------------------------------------------------------------- 1 | #include "cgit.h" 2 | #include "html.h" 3 | 4 | #define MAX_PATH 4096 5 | 6 | /* return 1 if path contains a objects/ directory and a HEAD file */ 7 | static int is_git_dir(const char *path) 8 | { 9 | struct stat st; 10 | static char buf[MAX_PATH]; 11 | 12 | if (snprintf(buf, MAX_PATH, "%s/objects", path) >= MAX_PATH) { 13 | fprintf(stderr, "Insanely long path: %s\n", path); 14 | return 0; 15 | } 16 | if (stat(buf, &st)) { 17 | if (errno != ENOENT) 18 | fprintf(stderr, "Error checking path %s: %s (%d)\n", 19 | path, strerror(errno), errno); 20 | return 0; 21 | } 22 | if (!S_ISDIR(st.st_mode)) 23 | return 0; 24 | 25 | sprintf(buf, "%s/HEAD", path); 26 | if (stat(buf, &st)) { 27 | if (errno != ENOENT) 28 | fprintf(stderr, "Error checking path %s: %s (%d)\n", 29 | path, strerror(errno), errno); 30 | return 0; 31 | } 32 | if (!S_ISREG(st.st_mode)) 33 | return 0; 34 | 35 | return 1; 36 | } 37 | 38 | char *readfile(const char *path) 39 | { 40 | FILE *f; 41 | static char buf[MAX_PATH]; 42 | 43 | if (!(f = fopen(path, "r"))) 44 | return NULL; 45 | fgets(buf, MAX_PATH, f); 46 | fclose(f); 47 | return buf; 48 | } 49 | 50 | static void add_repo(const char *base, const char *path) 51 | { 52 | struct cgit_repo *repo; 53 | struct stat st; 54 | struct passwd *pwd; 55 | char *p; 56 | 57 | if (stat(path, &st)) { 58 | fprintf(stderr, "Error accessing %s: %s (%d)\n", 59 | path, strerror(errno), errno); 60 | return; 61 | } 62 | if ((pwd = getpwuid(st.st_uid)) == NULL) { 63 | fprintf(stderr, "Error reading owner-info for %s: %s (%d)\n", 64 | path, strerror(errno), errno); 65 | return; 66 | } 67 | if (base == path) 68 | p = fmt("%s", path); 69 | else 70 | p = fmt("%s", path + strlen(base) + 1); 71 | 72 | if (!strcmp(p + strlen(p) - 5, "/.git")) 73 | p[strlen(p) - 5] = '\0'; 74 | 75 | repo = cgit_add_repo(xstrdup(p)); 76 | repo->name = repo->url; 77 | repo->path = xstrdup(path); 78 | repo->owner = (pwd ? xstrdup(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name) : ""); 79 | 80 | p = fmt("%s/description", path); 81 | if (!stat(p, &st)) 82 | repo->desc = xstrdup(readfile(p)); 83 | 84 | p = fmt("%s/README.html", path); 85 | if (!stat(p, &st)) 86 | repo->readme = "README.html"; 87 | } 88 | 89 | static void scan_path(const char *base, const char *path) 90 | { 91 | DIR *dir; 92 | struct dirent *ent; 93 | char *buf; 94 | struct stat st; 95 | 96 | if (is_git_dir(path)) { 97 | add_repo(base, path); 98 | return; 99 | } 100 | dir = opendir(path); 101 | if (!dir) { 102 | fprintf(stderr, "Error opening directory %s: %s (%d)\n", 103 | path, strerror(errno), errno); 104 | return; 105 | } 106 | while((ent = readdir(dir)) != NULL) { 107 | if (ent->d_name[0] == '.') { 108 | if (ent->d_name[1] == '\0') 109 | continue; 110 | if (ent->d_name[1] == '.' && ent->d_name[2] == '\0') 111 | continue; 112 | } 113 | buf = malloc(strlen(path) + strlen(ent->d_name) + 2); 114 | if (!buf) { 115 | fprintf(stderr, "Alloc error on %s: %s (%d)\n", 116 | path, strerror(errno), errno); 117 | exit(1); 118 | } 119 | sprintf(buf, "%s/%s", path, ent->d_name); 120 | if (stat(buf, &st)) { 121 | fprintf(stderr, "Error checking path %s: %s (%d)\n", 122 | buf, strerror(errno), errno); 123 | free(buf); 124 | continue; 125 | } 126 | if (S_ISDIR(st.st_mode)) 127 | scan_path(base, buf); 128 | free(buf); 129 | } 130 | closedir(dir); 131 | } 132 | 133 | void scan_tree(const char *path) 134 | { 135 | scan_path(path, path); 136 | } 137 | -------------------------------------------------------------------------------- /scan-tree.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | extern void scan_tree(const char *path); 4 | -------------------------------------------------------------------------------- /shared.c: -------------------------------------------------------------------------------- 1 | /* shared.c: global vars + some callback functions 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | 11 | struct cgit_repolist cgit_repolist; 12 | struct cgit_context ctx; 13 | int cgit_cmd; 14 | 15 | int chk_zero(int result, char *msg) 16 | { 17 | if (result != 0) 18 | die("%s: %s", msg, strerror(errno)); 19 | return result; 20 | } 21 | 22 | int chk_positive(int result, char *msg) 23 | { 24 | if (result <= 0) 25 | die("%s: %s", msg, strerror(errno)); 26 | return result; 27 | } 28 | 29 | int chk_non_negative(int result, char *msg) 30 | { 31 | if (result < 0) 32 | die("%s: %s",msg, strerror(errno)); 33 | return result; 34 | } 35 | 36 | struct cgit_repo *cgit_add_repo(const char *url) 37 | { 38 | struct cgit_repo *ret; 39 | 40 | if (++cgit_repolist.count > cgit_repolist.length) { 41 | if (cgit_repolist.length == 0) 42 | cgit_repolist.length = 8; 43 | else 44 | cgit_repolist.length *= 2; 45 | cgit_repolist.repos = xrealloc(cgit_repolist.repos, 46 | cgit_repolist.length * 47 | sizeof(struct cgit_repo)); 48 | } 49 | 50 | ret = &cgit_repolist.repos[cgit_repolist.count-1]; 51 | ret->url = trim_end(url, '/'); 52 | ret->name = ret->url; 53 | ret->path = NULL; 54 | ret->desc = "[no description]"; 55 | ret->owner = NULL; 56 | ret->group = ctx.cfg.repo_group; 57 | ret->defbranch = "master"; 58 | ret->snapshots = ctx.cfg.snapshots; 59 | ret->enable_log_filecount = ctx.cfg.enable_log_filecount; 60 | ret->enable_log_linecount = ctx.cfg.enable_log_linecount; 61 | ret->module_link = ctx.cfg.module_link; 62 | ret->readme = NULL; 63 | return ret; 64 | } 65 | 66 | struct cgit_repo *cgit_get_repoinfo(const char *url) 67 | { 68 | int i; 69 | struct cgit_repo *repo; 70 | 71 | for (i=0; iurl, url)) 74 | return repo; 75 | } 76 | return NULL; 77 | } 78 | 79 | void *cgit_free_commitinfo(struct commitinfo *info) 80 | { 81 | free(info->author); 82 | free(info->author_email); 83 | free(info->committer); 84 | free(info->committer_email); 85 | free(info->subject); 86 | free(info->msg); 87 | free(info->msg_encoding); 88 | free(info); 89 | return NULL; 90 | } 91 | 92 | char *trim_end(const char *str, char c) 93 | { 94 | int len; 95 | char *s, *t; 96 | 97 | if (str == NULL) 98 | return NULL; 99 | t = (char *)str; 100 | len = strlen(t); 101 | while(len > 0 && t[len - 1] == c) 102 | len--; 103 | 104 | if (len == 0) 105 | return NULL; 106 | 107 | c = t[len]; 108 | t[len] = '\0'; 109 | s = xstrdup(t); 110 | t[len] = c; 111 | return s; 112 | } 113 | 114 | char *strlpart(char *txt, int maxlen) 115 | { 116 | char *result; 117 | 118 | if (!txt) 119 | return txt; 120 | 121 | if (strlen(txt) <= maxlen) 122 | return txt; 123 | result = xmalloc(maxlen + 1); 124 | memcpy(result, txt, maxlen - 3); 125 | result[maxlen-1] = result[maxlen-2] = result[maxlen-3] = '.'; 126 | result[maxlen] = '\0'; 127 | return result; 128 | } 129 | 130 | char *strrpart(char *txt, int maxlen) 131 | { 132 | char *result; 133 | 134 | if (!txt) 135 | return txt; 136 | 137 | if (strlen(txt) <= maxlen) 138 | return txt; 139 | result = xmalloc(maxlen + 1); 140 | memcpy(result + 3, txt + strlen(txt) - maxlen + 4, maxlen - 3); 141 | result[0] = result[1] = result[2] = '.'; 142 | return result; 143 | } 144 | 145 | void cgit_add_ref(struct reflist *list, struct refinfo *ref) 146 | { 147 | size_t size; 148 | 149 | if (list->count >= list->alloc) { 150 | list->alloc += (list->alloc ? list->alloc : 4); 151 | size = list->alloc * sizeof(struct refinfo *); 152 | list->refs = xrealloc(list->refs, size); 153 | } 154 | list->refs[list->count++] = ref; 155 | } 156 | 157 | struct refinfo *cgit_mk_refinfo(const char *refname, const unsigned char *sha1) 158 | { 159 | struct refinfo *ref; 160 | 161 | ref = xmalloc(sizeof (struct refinfo)); 162 | ref->refname = xstrdup(refname); 163 | ref->object = parse_object(sha1); 164 | switch (ref->object->type) { 165 | case OBJ_TAG: 166 | ref->tag = cgit_parse_tag((struct tag *)ref->object); 167 | break; 168 | case OBJ_COMMIT: 169 | ref->commit = cgit_parse_commit((struct commit *)ref->object); 170 | break; 171 | } 172 | return ref; 173 | } 174 | 175 | int cgit_refs_cb(const char *refname, const unsigned char *sha1, int flags, 176 | void *cb_data) 177 | { 178 | struct reflist *list = (struct reflist *)cb_data; 179 | struct refinfo *info = cgit_mk_refinfo(refname, sha1); 180 | 181 | if (info) 182 | cgit_add_ref(list, info); 183 | return 0; 184 | } 185 | 186 | void cgit_diff_tree_cb(struct diff_queue_struct *q, 187 | struct diff_options *options, void *data) 188 | { 189 | int i; 190 | 191 | for (i = 0; i < q->nr; i++) { 192 | if (q->queue[i]->status == 'U') 193 | continue; 194 | ((filepair_fn)data)(q->queue[i]); 195 | } 196 | } 197 | 198 | static int load_mmfile(mmfile_t *file, const unsigned char *sha1) 199 | { 200 | enum object_type type; 201 | 202 | if (is_null_sha1(sha1)) { 203 | file->ptr = (char *)""; 204 | file->size = 0; 205 | } else { 206 | file->ptr = read_sha1_file(sha1, &type, 207 | (unsigned long *)&file->size); 208 | } 209 | return 1; 210 | } 211 | 212 | /* 213 | * Receive diff-buffers from xdiff and concatenate them as 214 | * needed across multiple callbacks. 215 | * 216 | * This is basically a copy of xdiff-interface.c/xdiff_outf(), 217 | * ripped from git and modified to use globals instead of 218 | * a special callback-struct. 219 | */ 220 | char *diffbuf = NULL; 221 | int buflen = 0; 222 | 223 | int filediff_cb(void *priv, mmbuffer_t *mb, int nbuf) 224 | { 225 | int i; 226 | 227 | for (i = 0; i < nbuf; i++) { 228 | if (mb[i].ptr[mb[i].size-1] != '\n') { 229 | /* Incomplete line */ 230 | diffbuf = xrealloc(diffbuf, buflen + mb[i].size); 231 | memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size); 232 | buflen += mb[i].size; 233 | continue; 234 | } 235 | 236 | /* we have a complete line */ 237 | if (!diffbuf) { 238 | ((linediff_fn)priv)(mb[i].ptr, mb[i].size); 239 | continue; 240 | } 241 | diffbuf = xrealloc(diffbuf, buflen + mb[i].size); 242 | memcpy(diffbuf + buflen, mb[i].ptr, mb[i].size); 243 | ((linediff_fn)priv)(diffbuf, buflen + mb[i].size); 244 | free(diffbuf); 245 | diffbuf = NULL; 246 | buflen = 0; 247 | } 248 | if (diffbuf) { 249 | ((linediff_fn)priv)(diffbuf, buflen); 250 | free(diffbuf); 251 | diffbuf = NULL; 252 | buflen = 0; 253 | } 254 | return 0; 255 | } 256 | 257 | int cgit_diff_files(const unsigned char *old_sha1, 258 | const unsigned char *new_sha1, 259 | linediff_fn fn) 260 | { 261 | mmfile_t file1, file2; 262 | xpparam_t diff_params; 263 | xdemitconf_t emit_params; 264 | xdemitcb_t emit_cb; 265 | 266 | if (!load_mmfile(&file1, old_sha1) || !load_mmfile(&file2, new_sha1)) 267 | return 1; 268 | 269 | diff_params.flags = XDF_NEED_MINIMAL; 270 | emit_params.ctxlen = 3; 271 | emit_params.flags = XDL_EMIT_FUNCNAMES; 272 | emit_params.find_func = NULL; 273 | emit_cb.outf = filediff_cb; 274 | emit_cb.priv = fn; 275 | xdl_diff(&file1, &file2, &diff_params, &emit_params, &emit_cb); 276 | return 0; 277 | } 278 | 279 | void cgit_diff_tree(const unsigned char *old_sha1, 280 | const unsigned char *new_sha1, 281 | filepair_fn fn, const char *prefix) 282 | { 283 | struct diff_options opt; 284 | int ret; 285 | int prefixlen; 286 | 287 | diff_setup(&opt); 288 | opt.output_format = DIFF_FORMAT_CALLBACK; 289 | opt.detect_rename = 1; 290 | opt.rename_limit = ctx.cfg.renamelimit; 291 | DIFF_OPT_SET(&opt, RECURSIVE); 292 | opt.format_callback = cgit_diff_tree_cb; 293 | opt.format_callback_data = fn; 294 | if (prefix) { 295 | opt.nr_paths = 1; 296 | opt.paths = &prefix; 297 | prefixlen = strlen(prefix); 298 | opt.pathlens = &prefixlen; 299 | } 300 | diff_setup_done(&opt); 301 | 302 | if (old_sha1 && !is_null_sha1(old_sha1)) 303 | ret = diff_tree_sha1(old_sha1, new_sha1, "", &opt); 304 | else 305 | ret = diff_root_tree_sha1(new_sha1, "", &opt); 306 | diffcore_std(&opt); 307 | diff_flush(&opt); 308 | } 309 | 310 | void cgit_diff_commit(struct commit *commit, filepair_fn fn) 311 | { 312 | unsigned char *old_sha1 = NULL; 313 | 314 | if (commit->parents) 315 | old_sha1 = commit->parents->item->object.sha1; 316 | cgit_diff_tree(old_sha1, commit->object.sha1, fn, NULL); 317 | } 318 | 319 | int cgit_parse_snapshots_mask(const char *str) 320 | { 321 | const struct cgit_snapshot_format *f; 322 | static const char *delim = " \t,:/|;"; 323 | int tl, sl, rv = 0; 324 | 325 | /* favor legacy setting */ 326 | if(atoi(str)) 327 | return 1; 328 | for(;;) { 329 | str += strspn(str,delim); 330 | tl = strcspn(str,delim); 331 | if (!tl) 332 | break; 333 | for (f = cgit_snapshot_formats; f->suffix; f++) { 334 | sl = strlen(f->suffix); 335 | if((tl == sl && !strncmp(f->suffix, str, tl)) || 336 | (tl == sl-1 && !strncmp(f->suffix+1, str, tl-1))) { 337 | rv |= f->bit; 338 | break; 339 | } 340 | } 341 | str += tl; 342 | } 343 | return rv; 344 | } 345 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | trash 2 | test-output.log 3 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh) 4 | 5 | all: $(T) 6 | 7 | $(T): 8 | @./$@ 9 | 10 | clean: 11 | $(RM) -rf trash 12 | 13 | .PHONY: $(T) clean 14 | -------------------------------------------------------------------------------- /tests/setup.sh: -------------------------------------------------------------------------------- 1 | # This file should be sourced by all test-scripts 2 | # 3 | # Main functions: 4 | # prepare_tests(description) - setup for testing, i.e. create repos+config 5 | # run_test(description, script) - run one test, i.e. eval script 6 | # 7 | # Helper functions 8 | # cgit_query(querystring) - call cgit with the specified querystring 9 | # cgit_url(url) - call cgit with the specified virtual url 10 | # 11 | # Example script: 12 | # 13 | # . setup.sh 14 | # prepare_tests "html validation" 15 | # run_test 'repo index' 'cgit_url "/" | tidy -e' 16 | # run_test 'repo summary' 'cgit_url "/foo" | tidy -e' 17 | 18 | 19 | mkrepo() { 20 | name=$1 21 | count=$2 22 | dir=$PWD 23 | test -d $name && return 24 | printf "Creating testrepo %s\n" $name 25 | mkdir -p $name 26 | cd $name 27 | git init 28 | n=1 29 | while test $n -le $count 30 | do 31 | echo $n >file-$n 32 | git add file-$n 33 | git commit -m "commit $n" 34 | n=$(expr $n + 1) 35 | done 36 | if test "$3" = "testplus" 37 | then 38 | echo "hello" >a+b 39 | git add a+b 40 | git commit -m "add a+b" 41 | git branch "1+2" 42 | fi 43 | cd $dir 44 | } 45 | 46 | setup_repos() 47 | { 48 | rm -rf trash/cache 49 | mkdir -p trash/cache 50 | mkrepo trash/repos/foo 5 >/dev/null 51 | mkrepo trash/repos/bar 50 >/dev/null 52 | mkrepo trash/repos/foo+bar 10 testplus >/dev/null 53 | cat >trash/cgitrc </dev/null 85 | test_count=0 86 | test_failed=0 87 | echo "[$0]" "$@" >test-output.log 88 | echo "$@" "($0)" 89 | } 90 | 91 | tests_done() 92 | { 93 | printf "\n" 94 | if test $test_failed -gt 0 95 | then 96 | printf "test: *** %s failure(s), logfile=%s\n" \ 97 | $test_failed "$(pwd)/test-output.log" 98 | false 99 | fi 100 | } 101 | 102 | run_test() 103 | { 104 | desc=$1 105 | script=$2 106 | test_count=$(expr $test_count + 1) 107 | printf "\ntest %d: name='%s'\n" $test_count "$desc" >>test-output.log 108 | printf "test %d: eval='%s'\n" $test_count "$2" >>test-output.log 109 | eval "$2" >>test-output.log 2>>test-output.log 110 | res=$? 111 | printf "test %d: exitcode=%d\n" $test_count $res >>test-output.log 112 | if test $res = 0 113 | then 114 | printf " %2d) %-60s [ok]\n" $test_count "$desc" 115 | else 116 | ((test_failed++)) 117 | printf " %2d) %-60s [failed]\n" $test_count "$desc" 118 | fi 119 | } 120 | 121 | cgit_query() 122 | { 123 | CGIT_CONFIG="$PWD/trash/cgitrc" QUERY_STRING="$1" "$PWD/../cgit" 124 | } 125 | 126 | cgit_url() 127 | { 128 | CGIT_CONFIG="$PWD/trash/cgitrc" QUERY_STRING="url=$1" "$PWD/../cgit" 129 | } 130 | -------------------------------------------------------------------------------- /tests/t0010-validate-html.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | 6 | test_url() 7 | { 8 | tidy_opt="-eq" 9 | test -z "$NO_TIDY_WARNINGS" || tidy_opt+=" --show-warnings no" 10 | cgit_url "$1" >trash/tidy-$test_count || return 11 | sed -ie "1,4d" trash/tidy-$test_count || return 12 | tidy $tidy_opt trash/tidy-$test_count 13 | rc=$? 14 | 15 | # tidy returns with exitcode 1 on warnings, 2 on error 16 | if test $rc = 2 17 | then 18 | false 19 | else 20 | : 21 | fi 22 | } 23 | 24 | prepare_tests 'Validate html with tidy' 25 | 26 | run_test 'index page' 'test_url ""' 27 | run_test 'foo' 'test_url "foo"' 28 | run_test 'foo/log' 'test_url "foo/log"' 29 | run_test 'foo/tree' 'test_url "foo/tree"' 30 | run_test 'foo/tree/file-1' 'test_url "foo/tree/file-1"' 31 | run_test 'foo/commit' 'test_url "foo/commit"' 32 | run_test 'foo/diff' 'test_url "foo/diff"' 33 | 34 | tests_done 35 | -------------------------------------------------------------------------------- /tests/t0020-validate-cache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | prepare_tests 'Validate cache' 6 | 7 | run_test 'verify cache-size=0' ' 8 | 9 | rm -f trash/cache/* && 10 | sed -i -e "s/cache-size=1021$/cache-size=0/" trash/cgitrc && 11 | cgit_url "" && 12 | cgit_url "foo" && 13 | cgit_url "foo/refs" && 14 | cgit_url "foo/tree" && 15 | cgit_url "foo/log" && 16 | cgit_url "foo/diff" && 17 | cgit_url "foo/patch" && 18 | cgit_url "bar" && 19 | cgit_url "bar/refs" && 20 | cgit_url "bar/tree" && 21 | cgit_url "bar/log" && 22 | cgit_url "bar/diff" && 23 | cgit_url "bar/patch" && 24 | test 0 -eq $(ls trash/cache | wc -l) 25 | ' 26 | 27 | run_test 'verify cache-size=1' ' 28 | 29 | rm -f trash/cache/* && 30 | sed -i -e "s/cache-size=0$/cache-size=1/" trash/cgitrc && 31 | cgit_url "" && 32 | cgit_url "foo" && 33 | cgit_url "foo/refs" && 34 | cgit_url "foo/tree" && 35 | cgit_url "foo/log" && 36 | cgit_url "foo/diff" && 37 | cgit_url "foo/patch" && 38 | cgit_url "bar" && 39 | cgit_url "bar/refs" && 40 | cgit_url "bar/tree" && 41 | cgit_url "bar/log" && 42 | cgit_url "bar/diff" && 43 | cgit_url "bar/patch" && 44 | test 1 -eq $(ls trash/cache | wc -l) 45 | ' 46 | 47 | run_test 'verify cache-size=1021' ' 48 | 49 | rm -f trash/cache/* && 50 | sed -i -e "s/cache-size=1$/cache-size=1021/" trash/cgitrc && 51 | cgit_url "" && 52 | cgit_url "foo" && 53 | cgit_url "foo/refs" && 54 | cgit_url "foo/tree" && 55 | cgit_url "foo/log" && 56 | cgit_url "foo/diff" && 57 | cgit_url "foo/patch" && 58 | cgit_url "bar" && 59 | cgit_url "bar/refs" && 60 | cgit_url "bar/tree" && 61 | cgit_url "bar/log" && 62 | cgit_url "bar/diff" && 63 | cgit_url "bar/patch" && 64 | test 13 -eq $(ls trash/cache | wc -l) 65 | ' 66 | 67 | tests_done 68 | -------------------------------------------------------------------------------- /tests/t0101-index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | prepare_tests "Check content on index page" 6 | 7 | run_test 'generate index page' 'cgit_url "" >trash/tmp' 8 | run_test 'find foo repo' 'grep -e "foo" trash/tmp' 9 | run_test 'find foo description' 'grep -e "\[no description\]" trash/tmp' 10 | run_test 'find bar repo' 'grep -e "bar" trash/tmp' 11 | run_test 'find bar description' 'grep -e "the bar repo" trash/tmp' 12 | run_test 'find foo+bar repo' 'grep -e ">foo+bar<" trash/tmp' 13 | run_test 'verify foo+bar link' 'grep -e "/foo+bar/" trash/tmp' 14 | run_test 'no tree-link' '! grep -e "foo/tree" trash/tmp' 15 | run_test 'no log-link' '! grep -e "foo/log" trash/tmp' 16 | 17 | tests_done 18 | -------------------------------------------------------------------------------- /tests/t0102-summary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | prepare_tests "Check content on summary page" 6 | 7 | run_test 'generate foo summary' 'cgit_url "foo" >trash/tmp' 8 | run_test 'find commit 1' 'grep -e "commit 1" trash/tmp' 9 | run_test 'find commit 5' 'grep -e "commit 5" trash/tmp' 10 | run_test 'find branch master' 'grep -e "master" trash/tmp' 11 | run_test 'no tags' '! grep -e "tags" trash/tmp' 12 | 13 | run_test 'generate bar summary' 'cgit_url "bar" >trash/tmp' 14 | run_test 'no commit 45' '! grep -e "commit 45" trash/tmp' 15 | run_test 'find commit 46' 'grep -e "commit 46" trash/tmp' 16 | run_test 'find commit 50' 'grep -e "commit 50" trash/tmp' 17 | run_test 'find branch master' 'grep -e "master" trash/tmp' 18 | run_test 'no tags' '! grep -e "tags" trash/tmp' 19 | 20 | tests_done 21 | -------------------------------------------------------------------------------- /tests/t0103-log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | prepare_tests "Check content on log page" 6 | 7 | run_test 'generate foo/log' 'cgit_url "foo/log" >trash/tmp' 8 | run_test 'find commit 1' 'grep -e "commit 1" trash/tmp' 9 | run_test 'find commit 5' 'grep -e "commit 5" trash/tmp' 10 | 11 | run_test 'generate bar/log' 'cgit_url "bar/log" >trash/tmp' 12 | run_test 'find commit 1' 'grep -e "commit 1" trash/tmp' 13 | run_test 'find commit 50' 'grep -e "commit 50" trash/tmp' 14 | 15 | tests_done 16 | -------------------------------------------------------------------------------- /tests/t0104-tree.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | prepare_tests "Check content on tree page" 6 | 7 | run_test 'generate bar/tree' 'cgit_url "bar/tree" >trash/tmp' 8 | run_test 'find file-1' 'grep -e "file-1" trash/tmp' 9 | run_test 'find file-50' 'grep -e "file-50" trash/tmp' 10 | 11 | run_test 'generate bar/tree/file-50' 'cgit_url "bar/tree/file-50" >trash/tmp' 12 | 13 | run_test 'find line 1' ' 14 | grep -e "1" trash/tmp 15 | ' 16 | 17 | run_test 'no line 2' ' 18 | grep -e "2" trash/tmp 19 | ' 20 | 21 | run_test 'generate foo+bar/tree' 'cgit_url "foo%2bbar/tree" >trash/tmp' 22 | 23 | run_test 'verify a+b link' ' 24 | grep -e "/foo+bar/tree/a+b" trash/tmp 25 | ' 26 | 27 | run_test 'generate foo+bar/tree?h=1+2' 'cgit_url "foo%2bbar/tree&h=1%2b2" >trash/tmp' 28 | 29 | run_test 'verify a+b?h=1+2 link' ' 30 | grep -e "/foo+bar/tree/a+b?h=1%2b2" trash/tmp 31 | ' 32 | 33 | tests_done 34 | -------------------------------------------------------------------------------- /tests/t0105-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | prepare_tests "Check content on commit page" 6 | 7 | run_test 'generate foo/commit' 'cgit_url "foo/commit" >trash/tmp' 8 | run_test 'find tree link' 'grep -e "" trash/tmp' 9 | run_test 'find parent link' 'grep -E "" trash/tmp' 10 | 11 | run_test 'find commit subject' ' 12 | grep -e "
commit 5
" trash/tmp 13 | ' 14 | 15 | run_test 'find commit msg' 'grep -e "
" trash/tmp' 16 | run_test 'find diffstat' 'grep -e "" trash/tmp' 17 | 18 | run_test 'find diff summary' ' 19 | grep -e "1 files changed, 1 insertions, 0 deletions" trash/tmp 20 | ' 21 | 22 | run_test 'get root commit' ' 23 | root=$(cd trash/repos/foo && git rev-list --reverse HEAD | head -1) && 24 | cgit_url "foo/commit&id=$root" >trash/tmp && 25 | grep "" trash/tmp 26 | ' 27 | 28 | run_test 'root commit contains diffstat' ' 29 | grep "file-1" trash/tmp 30 | ' 31 | 32 | run_test 'root commit contains diff' ' 33 | grep ">diff --git a/file-1 b/file-1<" trash/tmp && 34 | grep -e "
+1
" trash/tmp 35 | ' 36 | 37 | tests_done 38 | -------------------------------------------------------------------------------- /tests/t0106-diff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | prepare_tests "Check content on diff page" 6 | 7 | run_test 'generate foo/diff' 'cgit_url "foo/diff" >trash/tmp' 8 | run_test 'find diff header' 'grep -e "a/file-5 b/file-5" trash/tmp' 9 | run_test 'find blob link' 'grep -e "@@ -0,0 +1 @@" trash/tmp 14 | ' 15 | 16 | run_test 'find added line' ' 17 | grep -e "
+5
" trash/tmp 18 | ' 19 | 20 | tests_done 21 | -------------------------------------------------------------------------------- /tests/t0107-snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | prepare_tests "Verify snapshot" 6 | 7 | run_test 'get foo/snapshot/test.tar.gz' ' 8 | cgit_url "foo/snapshot/test.tar.gz" >trash/tmp 9 | ' 10 | 11 | run_test 'check html headers' ' 12 | head -n 1 trash/tmp | 13 | grep -e "Content-Type: application/x-tar" && 14 | 15 | head -n 2 trash/tmp | 16 | grep -e "Content-Disposition: inline; filename=.test.tar.gz." 17 | ' 18 | 19 | run_test 'strip off the header lines' ' 20 | tail -n +6 trash/tmp > trash/test.tar.gz 21 | ' 22 | 23 | run_test 'verify gzip format' 'gunzip --test trash/test.tar.gz' 24 | run_test 'untar' ' 25 | rm -rf trash/foo && 26 | tar -xf trash/test.tar.gz -C trash 27 | ' 28 | 29 | run_test 'count files' ' 30 | c=$(ls -1 trash/foo/ | wc -l) && 31 | test $c = 5 32 | ' 33 | 34 | run_test 'verify untarred file-5' ' 35 | grep -e "^5$" trash/foo/file-5 && 36 | test $(cat trash/foo/file-5 | wc -l) = 1 37 | ' 38 | 39 | tests_done 40 | -------------------------------------------------------------------------------- /tests/t0108-patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . ./setup.sh 4 | 5 | prepare_tests "Check content on patch page" 6 | 7 | run_test 'generate foo/patch' ' 8 | cgit_query "url=foo/patch" >trash/tmp 9 | ' 10 | 11 | run_test 'find `From:` line' ' 12 | grep -e "^From: " trash/tmp 13 | ' 14 | 15 | run_test 'find `Date:` line' ' 16 | grep -e "^Date: " trash/tmp 17 | ' 18 | 19 | run_test 'find `Subject:` line' ' 20 | grep -e "^Subject: commit 5" trash/tmp 21 | ' 22 | 23 | run_test 'find `cgit` signature' ' 24 | tail -1 trash/tmp | grep -e "^cgit" 25 | ' 26 | 27 | run_test 'find initial commit' ' 28 | root=$(git --git-dir=$PWD/trash/repos/foo/.git rev-list HEAD | tail -1) 29 | ' 30 | 31 | run_test 'generate patch for initial commit' ' 32 | cgit_query "url=foo/patch&id=$root" >trash/tmp 33 | ' 34 | 35 | run_test 'find `cgit` signature' ' 36 | tail -1 trash/tmp | grep -e "^cgit" 37 | ' 38 | -------------------------------------------------------------------------------- /ui-atom.c: -------------------------------------------------------------------------------- 1 | /* ui-atom.c: functions for atom feeds 2 | * 3 | * Copyright (C) 2008 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | void add_entry(struct commit *commit, char *host) 14 | { 15 | char delim = '&'; 16 | char *hex; 17 | char *mail, *t, *t2; 18 | struct commitinfo *info; 19 | 20 | info = cgit_parse_commit(commit); 21 | hex = sha1_to_hex(commit->object.sha1); 22 | html("\n"); 23 | html(""); 24 | html_txt(info->subject); 25 | html("\n"); 26 | html(""); 27 | cgit_print_date(info->author_date, FMT_ATOMDATE, ctx.cfg.local_time); 28 | html("\n"); 29 | html("\n"); 30 | if (info->author) { 31 | html(""); 32 | html_txt(info->author); 33 | html("\n"); 34 | } 35 | if (info->author_email) { 36 | mail = xstrdup(info->author_email); 37 | t = strchr(mail, '<'); 38 | if (t) 39 | t++; 40 | else 41 | t = mail; 42 | t2 = strchr(t, '>'); 43 | if (t2) 44 | *t2 = '\0'; 45 | html(""); 46 | html_txt(t); 47 | html("\n"); 48 | free(mail); 49 | } 50 | html("\n"); 51 | html(""); 52 | cgit_print_date(info->author_date, FMT_ATOMDATE, ctx.cfg.local_time); 53 | html("\n"); 54 | if (host) { 55 | html("\n"); 62 | } 63 | htmlf("%s\n", hex); 64 | html("\n"); 65 | html_txt(info->msg); 66 | html("\n"); 67 | html("\n"); 68 | html("
\n"); 69 | html("
\n");
 70 | 	html_txt(info->msg);
 71 | 	html("
\n"); 72 | html("
\n"); 73 | html("
\n"); 74 | html("
\n"); 75 | cgit_free_commitinfo(info); 76 | } 77 | 78 | 79 | void cgit_print_atom(char *tip, char *path, int max_count) 80 | { 81 | char *host; 82 | const char *argv[] = {NULL, tip, NULL, NULL, NULL}; 83 | struct commit *commit; 84 | struct rev_info rev; 85 | int argc = 2; 86 | 87 | if (!tip) 88 | argv[1] = ctx.qry.head; 89 | 90 | if (path) { 91 | argv[argc++] = "--"; 92 | argv[argc++] = path; 93 | } 94 | 95 | init_revisions(&rev, NULL); 96 | rev.abbrev = DEFAULT_ABBREV; 97 | rev.commit_format = CMIT_FMT_DEFAULT; 98 | rev.verbose_header = 1; 99 | rev.show_root_diff = 0; 100 | rev.max_count = max_count; 101 | setup_revisions(argc, argv, &rev, NULL); 102 | prepare_revision_walk(&rev); 103 | 104 | host = cgit_hosturl(); 105 | ctx.page.mimetype = "text/xml"; 106 | ctx.page.charset = "utf-8"; 107 | cgit_print_http_headers(&ctx); 108 | html("\n"); 109 | html(""); 110 | html_txt(ctx.repo->name); 111 | html("\n"); 112 | html(""); 113 | html_txt(ctx.repo->desc); 114 | html("\n"); 115 | if (host) { 116 | html("\n"); 120 | } 121 | while ((commit = get_revision(&rev)) != NULL) { 122 | add_entry(commit, host); 123 | free(commit->buffer); 124 | commit->buffer = NULL; 125 | free_commit_list(commit->parents); 126 | commit->parents = NULL; 127 | } 128 | html("\n"); 129 | } 130 | -------------------------------------------------------------------------------- /ui-atom.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_ATOM_H 2 | #define UI_ATOM_H 3 | 4 | extern void cgit_print_atom(char *tip, char *path, int max_count); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /ui-blob.c: -------------------------------------------------------------------------------- 1 | /* ui-blob.c: show blob content 2 | * 3 | * Copyright (C) 2008 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | static char *match_path; 14 | static unsigned char *matched_sha1; 15 | 16 | static int walk_tree(const unsigned char *sha1, const char *base,int baselen, 17 | const char *pathname, unsigned mode, int stage, void *cbdata) { 18 | if(strncmp(base,match_path,baselen) 19 | || strcmp(match_path+baselen,pathname) ) 20 | return READ_TREE_RECURSIVE; 21 | memmove(matched_sha1,sha1,20); 22 | return 0; 23 | } 24 | 25 | void cgit_print_blob(const char *hex, char *path, const char *head) 26 | { 27 | 28 | unsigned char sha1[20]; 29 | enum object_type type; 30 | unsigned char *buf; 31 | unsigned long size; 32 | struct commit *commit; 33 | const char *paths[] = {path, NULL}; 34 | 35 | if (hex) { 36 | if (get_sha1_hex(hex, sha1)){ 37 | cgit_print_error(fmt("Bad hex value: %s", hex)); 38 | return; 39 | } 40 | } else { 41 | if (get_sha1(head,sha1)) { 42 | cgit_print_error(fmt("Bad ref: %s", head)); 43 | return; 44 | } 45 | } 46 | 47 | type = sha1_object_info(sha1, &size); 48 | 49 | if((!hex) && type == OBJ_COMMIT && path) { 50 | commit = lookup_commit_reference(sha1); 51 | match_path = path; 52 | matched_sha1 = sha1; 53 | read_tree_recursive(commit->tree, NULL, 0, 0, paths, walk_tree, NULL); 54 | type = sha1_object_info(sha1,&size); 55 | } 56 | 57 | if (type == OBJ_BAD) { 58 | cgit_print_error(fmt("Bad object name: %s", hex)); 59 | return; 60 | } 61 | 62 | buf = read_sha1_file(sha1, &type, &size); 63 | if (!buf) { 64 | cgit_print_error(fmt("Error reading object %s", hex)); 65 | return; 66 | } 67 | 68 | buf[size] = '\0'; 69 | ctx.page.mimetype = ctx.qry.mimetype; 70 | ctx.page.filename = path; 71 | cgit_print_http_headers(&ctx); 72 | write(htmlfd, buf, size); 73 | } 74 | -------------------------------------------------------------------------------- /ui-blob.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_BLOB_H 2 | #define UI_BLOB_H 3 | 4 | extern void cgit_print_blob(const char *hex, char *path, const char *head); 5 | 6 | #endif /* UI_BLOB_H */ 7 | -------------------------------------------------------------------------------- /ui-clone.c: -------------------------------------------------------------------------------- 1 | /* ui-clone.c: functions for http cloning, based on 2 | * git's http-backend.c by Shawn O. Pearce 3 | * 4 | * Copyright (C) 2008 Lars Hjemli 5 | * 6 | * Licensed under GNU General Public License v2 7 | * (see COPYING for full license text) 8 | */ 9 | 10 | #include "cgit.h" 11 | #include "html.h" 12 | #include "ui-shared.h" 13 | 14 | static int print_ref_info(const char *refname, const unsigned char *sha1, 15 | int flags, void *cb_data) 16 | { 17 | struct object *obj; 18 | 19 | if (!(obj = parse_object(sha1))) 20 | return 0; 21 | 22 | if (!strcmp(refname, "HEAD") || !prefixcmp(refname, "refs/heads/")) 23 | htmlf("%s\t%s\n", sha1_to_hex(sha1), refname); 24 | else if (!prefixcmp(refname, "refs/tags") && obj->type == OBJ_TAG) { 25 | if (!(obj = deref_tag(obj, refname, 0))) 26 | return 0; 27 | htmlf("%s\t%s\n", sha1_to_hex(sha1), refname); 28 | htmlf("%s\t%s^{}\n", sha1_to_hex(obj->sha1), refname); 29 | } 30 | return 0; 31 | } 32 | 33 | static void print_pack_info(struct cgit_context *ctx) 34 | { 35 | struct packed_git *pack; 36 | int ofs; 37 | 38 | ctx->page.mimetype = "text/plain"; 39 | ctx->page.filename = "objects/info/packs"; 40 | cgit_print_http_headers(ctx); 41 | ofs = strlen(ctx->repo->path) + strlen("/objects/pack/"); 42 | prepare_packed_git(); 43 | for (pack = packed_git; pack; pack = pack->next) 44 | if (pack->pack_local) 45 | htmlf("P %s\n", pack->pack_name + ofs); 46 | } 47 | 48 | static void send_file(struct cgit_context *ctx, char *path) 49 | { 50 | struct stat st; 51 | 52 | if (stat(path, &st)) { 53 | switch (errno) { 54 | case ENOENT: 55 | html_status(404, "Not found", 0); 56 | break; 57 | case EACCES: 58 | html_status(403, "Forbidden", 0); 59 | break; 60 | default: 61 | html_status(400, "Bad request", 0); 62 | } 63 | return; 64 | } 65 | ctx->page.mimetype = "application/octet-stream"; 66 | ctx->page.filename = path; 67 | if (prefixcmp(ctx->repo->path, path)) 68 | ctx->page.filename += strlen(ctx->repo->path) + 1; 69 | cgit_print_http_headers(ctx); 70 | html_include(path); 71 | } 72 | 73 | void cgit_clone_info(struct cgit_context *ctx) 74 | { 75 | if (!ctx->qry.path || strcmp(ctx->qry.path, "refs")) 76 | return; 77 | 78 | ctx->page.mimetype = "text/plain"; 79 | ctx->page.filename = "info/refs"; 80 | cgit_print_http_headers(ctx); 81 | for_each_ref(print_ref_info, ctx); 82 | } 83 | 84 | void cgit_clone_objects(struct cgit_context *ctx) 85 | { 86 | if (!ctx->qry.path) { 87 | html_status(400, "Bad request", 0); 88 | return; 89 | } 90 | 91 | if (!strcmp(ctx->qry.path, "info/packs")) { 92 | print_pack_info(ctx); 93 | return; 94 | } 95 | 96 | send_file(ctx, git_path("objects/%s", ctx->qry.path)); 97 | } 98 | 99 | void cgit_clone_head(struct cgit_context *ctx) 100 | { 101 | send_file(ctx, git_path("%s", "HEAD")); 102 | } 103 | -------------------------------------------------------------------------------- /ui-clone.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_CLONE_H 2 | #define UI_CLONE_H 3 | 4 | void cgit_clone_info(struct cgit_context *ctx); 5 | void cgit_clone_objects(struct cgit_context *ctx); 6 | void cgit_clone_head(struct cgit_context *ctx); 7 | 8 | #endif /* UI_CLONE_H */ 9 | -------------------------------------------------------------------------------- /ui-commit.c: -------------------------------------------------------------------------------- 1 | /* ui-commit.c: generate commit view 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | #include "ui-diff.h" 13 | 14 | void cgit_print_commit(char *hex) 15 | { 16 | struct commit *commit, *parent; 17 | struct commitinfo *info; 18 | struct commit_list *p; 19 | unsigned char sha1[20]; 20 | char *tmp; 21 | int parents = 0; 22 | 23 | if (!hex) 24 | hex = ctx.qry.head; 25 | 26 | if (get_sha1(hex, sha1)) { 27 | cgit_print_error(fmt("Bad object id: %s", hex)); 28 | return; 29 | } 30 | commit = lookup_commit_reference(sha1); 31 | if (!commit) { 32 | cgit_print_error(fmt("Bad commit reference: %s", hex)); 33 | return; 34 | } 35 | info = cgit_parse_commit(commit); 36 | 37 | html("
\n"); 38 | html("\n"); 45 | html("\n"); 52 | html("\n"); 58 | html("\n"); 63 | for (p = commit->parents; p ; p = p->next) { 64 | parent = lookup_commit_reference(p->item->object.sha1); 65 | if (!parent) { 66 | html(""); 69 | continue; 70 | } 71 | html("" 72 | ""); 79 | parents++; 80 | } 81 | if (ctx.repo->snapshots) { 82 | html(""); 86 | } 87 | html("
author"); 39 | html_txt(info->author); 40 | html(" "); 41 | html_txt(info->author_email); 42 | html(""); 43 | cgit_print_date(info->author_date, FMT_LONGDATE, ctx.cfg.local_time); 44 | html("
committer"); 46 | html_txt(info->committer); 47 | html(" "); 48 | html_txt(info->committer_email); 49 | html(""); 50 | cgit_print_date(info->committer_date, FMT_LONGDATE, ctx.cfg.local_time); 51 | html("
commit"); 53 | tmp = sha1_to_hex(commit->object.sha1); 54 | cgit_commit_link(tmp, NULL, NULL, ctx.qry.head, tmp); 55 | html(" ("); 56 | cgit_patch_link("patch", NULL, NULL, NULL, tmp); 57 | html(")
tree"); 59 | tmp = xstrdup(hex); 60 | cgit_tree_link(sha1_to_hex(commit->tree->object.sha1), NULL, NULL, 61 | ctx.qry.head, tmp, NULL); 62 | html("
"); 67 | cgit_print_error("Error reading parent commit"); 68 | html("
parent"); 73 | cgit_commit_link(sha1_to_hex(p->item->object.sha1), NULL, NULL, 74 | ctx.qry.head, sha1_to_hex(p->item->object.sha1)); 75 | html(" ("); 76 | cgit_diff_link("diff", NULL, NULL, ctx.qry.head, hex, 77 | sha1_to_hex(p->item->object.sha1), NULL); 78 | html(")
download"); 83 | cgit_print_snapshot_links(ctx.qry.repo, ctx.qry.head, 84 | hex, ctx.repo->snapshots); 85 | html("
\n"); 88 | html("
"); 89 | html_txt(info->subject); 90 | html("
"); 91 | html("
"); 92 | html_txt(info->msg); 93 | html("
"); 94 | if (parents < 3) { 95 | if (parents) 96 | tmp = sha1_to_hex(commit->parents->item->object.sha1); 97 | else 98 | tmp = NULL; 99 | cgit_print_diff(ctx.qry.sha1, tmp, NULL); 100 | } 101 | cgit_free_commitinfo(info); 102 | } 103 | -------------------------------------------------------------------------------- /ui-commit.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_COMMIT_H 2 | #define UI_COMMIT_H 3 | 4 | extern void cgit_print_commit(char *hex); 5 | 6 | #endif /* UI_COMMIT_H */ 7 | -------------------------------------------------------------------------------- /ui-diff.c: -------------------------------------------------------------------------------- 1 | /* ui-diff.c: show diff between two blobs 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | unsigned char old_rev_sha1[20]; 14 | unsigned char new_rev_sha1[20]; 15 | 16 | static int files, slots; 17 | static int total_adds, total_rems, max_changes; 18 | static int lines_added, lines_removed; 19 | 20 | static struct fileinfo { 21 | char status; 22 | unsigned char old_sha1[20]; 23 | unsigned char new_sha1[20]; 24 | unsigned short old_mode; 25 | unsigned short new_mode; 26 | char *old_path; 27 | char *new_path; 28 | unsigned int added; 29 | unsigned int removed; 30 | } *items; 31 | 32 | 33 | static void print_fileinfo(struct fileinfo *info) 34 | { 35 | char *class; 36 | 37 | switch (info->status) { 38 | case DIFF_STATUS_ADDED: 39 | class = "add"; 40 | break; 41 | case DIFF_STATUS_COPIED: 42 | class = "cpy"; 43 | break; 44 | case DIFF_STATUS_DELETED: 45 | class = "del"; 46 | break; 47 | case DIFF_STATUS_MODIFIED: 48 | class = "upd"; 49 | break; 50 | case DIFF_STATUS_RENAMED: 51 | class = "mov"; 52 | break; 53 | case DIFF_STATUS_TYPE_CHANGED: 54 | class = "typ"; 55 | break; 56 | case DIFF_STATUS_UNKNOWN: 57 | class = "unk"; 58 | break; 59 | case DIFF_STATUS_UNMERGED: 60 | class = "stg"; 61 | break; 62 | default: 63 | die("bug: unhandled diff status %c", info->status); 64 | } 65 | 66 | html(""); 67 | htmlf(""); 68 | if (is_null_sha1(info->new_sha1)) { 69 | cgit_print_filemode(info->old_mode); 70 | } else { 71 | cgit_print_filemode(info->new_mode); 72 | } 73 | 74 | if (info->old_mode != info->new_mode && 75 | !is_null_sha1(info->old_sha1) && 76 | !is_null_sha1(info->new_sha1)) { 77 | html("["); 78 | cgit_print_filemode(info->old_mode); 79 | html("]"); 80 | } 81 | htmlf("", class); 82 | cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.sha1, 83 | ctx.qry.sha2, info->new_path); 84 | if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED) 85 | htmlf(" (%s from %s)", 86 | info->status == DIFF_STATUS_COPIED ? "copied" : "renamed", 87 | info->old_path); 88 | html(""); 89 | htmlf("%d", info->added + info->removed); 90 | html(""); 91 | htmlf("", (max_changes > 100 ? 100 : max_changes)); 92 | htmlf("
", 93 | info->added * 100.0 / max_changes); 94 | htmlf("", 95 | info->removed * 100.0 / max_changes); 96 | htmlf("", 97 | (max_changes - info->removed - info->added) * 100.0 / max_changes); 98 | html("
\n"); 99 | } 100 | 101 | static void count_diff_lines(char *line, int len) 102 | { 103 | if (line && (len > 0)) { 104 | if (line[0] == '+') 105 | lines_added++; 106 | else if (line[0] == '-') 107 | lines_removed++; 108 | } 109 | } 110 | 111 | static void inspect_filepair(struct diff_filepair *pair) 112 | { 113 | files++; 114 | lines_added = 0; 115 | lines_removed = 0; 116 | cgit_diff_files(pair->one->sha1, pair->two->sha1, count_diff_lines); 117 | if (files >= slots) { 118 | if (slots == 0) 119 | slots = 4; 120 | else 121 | slots = slots * 2; 122 | items = xrealloc(items, slots * sizeof(struct fileinfo)); 123 | } 124 | items[files-1].status = pair->status; 125 | hashcpy(items[files-1].old_sha1, pair->one->sha1); 126 | hashcpy(items[files-1].new_sha1, pair->two->sha1); 127 | items[files-1].old_mode = pair->one->mode; 128 | items[files-1].new_mode = pair->two->mode; 129 | items[files-1].old_path = xstrdup(pair->one->path); 130 | items[files-1].new_path = xstrdup(pair->two->path); 131 | items[files-1].added = lines_added; 132 | items[files-1].removed = lines_removed; 133 | if (lines_added + lines_removed > max_changes) 134 | max_changes = lines_added + lines_removed; 135 | total_adds += lines_added; 136 | total_rems += lines_removed; 137 | } 138 | 139 | void cgit_print_diffstat(const unsigned char *old_sha1, 140 | const unsigned char *new_sha1) 141 | { 142 | int i; 143 | 144 | html("
"); 145 | cgit_diff_link("Diffstat", NULL, NULL, ctx.qry.head, ctx.qry.sha1, 146 | ctx.qry.sha2, NULL); 147 | html("
"); 148 | html(""); 149 | max_changes = 0; 150 | cgit_diff_tree(old_sha1, new_sha1, inspect_filepair, NULL); 151 | for(i = 0; i"); 154 | html("
"); 155 | htmlf("%d files changed, %d insertions, %d deletions", 156 | files, total_adds, total_rems); 157 | html("
"); 158 | } 159 | 160 | 161 | /* 162 | * print a single line returned from xdiff 163 | */ 164 | static void print_line(char *line, int len) 165 | { 166 | char *class = "ctx"; 167 | char c = line[len-1]; 168 | 169 | if (line[0] == '+') 170 | class = "add"; 171 | else if (line[0] == '-') 172 | class = "del"; 173 | else if (line[0] == '@') 174 | class = "hunk"; 175 | 176 | htmlf("
", class); 177 | line[len-1] = '\0'; 178 | html_txt(line); 179 | html("
"); 180 | line[len-1] = c; 181 | } 182 | 183 | static void header(unsigned char *sha1, char *path1, int mode1, 184 | unsigned char *sha2, char *path2, int mode2) 185 | { 186 | char *abbrev1, *abbrev2; 187 | int subproject; 188 | 189 | subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2)); 190 | html("
"); 191 | html("diff --git a/"); 192 | html_txt(path1); 193 | html(" b/"); 194 | html_txt(path2); 195 | 196 | if (is_null_sha1(sha1)) 197 | path1 = "dev/null"; 198 | if (is_null_sha1(sha2)) 199 | path2 = "dev/null"; 200 | 201 | if (mode1 == 0) 202 | htmlf("
new file mode %.6o", mode2); 203 | 204 | if (mode2 == 0) 205 | htmlf("
deleted file mode %.6o", mode1); 206 | 207 | if (!subproject) { 208 | abbrev1 = xstrdup(find_unique_abbrev(sha1, DEFAULT_ABBREV)); 209 | abbrev2 = xstrdup(find_unique_abbrev(sha2, DEFAULT_ABBREV)); 210 | htmlf("
index %s..%s", abbrev1, abbrev2); 211 | free(abbrev1); 212 | free(abbrev2); 213 | if (mode1 != 0 && mode2 != 0) { 214 | htmlf(" %.6o", mode1); 215 | if (mode2 != mode1) 216 | htmlf("..%.6o", mode2); 217 | } 218 | html("
--- a/"); 219 | if (mode1 != 0) 220 | cgit_tree_link(path1, NULL, NULL, ctx.qry.head, 221 | sha1_to_hex(old_rev_sha1), path1); 222 | else 223 | html_txt(path1); 224 | html("
+++ b/"); 225 | if (mode2 != 0) 226 | cgit_tree_link(path2, NULL, NULL, ctx.qry.head, 227 | sha1_to_hex(new_rev_sha1), path2); 228 | else 229 | html_txt(path2); 230 | } 231 | html("
"); 232 | } 233 | 234 | static void filepair_cb(struct diff_filepair *pair) 235 | { 236 | header(pair->one->sha1, pair->one->path, pair->one->mode, 237 | pair->two->sha1, pair->two->path, pair->two->mode); 238 | if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) { 239 | if (S_ISGITLINK(pair->one->mode)) 240 | print_line(fmt("-Subproject %s", sha1_to_hex(pair->one->sha1)), 52); 241 | if (S_ISGITLINK(pair->two->mode)) 242 | print_line(fmt("+Subproject %s", sha1_to_hex(pair->two->sha1)), 52); 243 | return; 244 | } 245 | if (cgit_diff_files(pair->one->sha1, pair->two->sha1, print_line)) 246 | cgit_print_error("Error running diff"); 247 | } 248 | 249 | void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefix) 250 | { 251 | enum object_type type; 252 | unsigned long size; 253 | struct commit *commit, *commit2; 254 | 255 | if (!new_rev) 256 | new_rev = ctx.qry.head; 257 | get_sha1(new_rev, new_rev_sha1); 258 | type = sha1_object_info(new_rev_sha1, &size); 259 | if (type == OBJ_BAD) { 260 | cgit_print_error(fmt("Bad object name: %s", new_rev)); 261 | return; 262 | } 263 | commit = lookup_commit_reference(new_rev_sha1); 264 | if (!commit || parse_commit(commit)) 265 | cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(new_rev_sha1))); 266 | 267 | if (old_rev) 268 | get_sha1(old_rev, old_rev_sha1); 269 | else if (commit->parents && commit->parents->item) 270 | hashcpy(old_rev_sha1, commit->parents->item->object.sha1); 271 | else 272 | hashclr(old_rev_sha1); 273 | 274 | if (!is_null_sha1(old_rev_sha1)) { 275 | type = sha1_object_info(old_rev_sha1, &size); 276 | if (type == OBJ_BAD) { 277 | cgit_print_error(fmt("Bad object name: %s", sha1_to_hex(old_rev_sha1))); 278 | return; 279 | } 280 | commit2 = lookup_commit_reference(old_rev_sha1); 281 | if (!commit2 || parse_commit(commit2)) 282 | cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(old_rev_sha1))); 283 | } 284 | cgit_print_diffstat(old_rev_sha1, new_rev_sha1); 285 | 286 | html("
"); 287 | html(""); 290 | html("
"); 288 | cgit_diff_tree(old_rev_sha1, new_rev_sha1, filepair_cb, prefix); 289 | html("
"); 291 | } 292 | -------------------------------------------------------------------------------- /ui-diff.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_DIFF_H 2 | #define UI_DIFF_H 3 | 4 | extern void cgit_print_diffstat(const unsigned char *old_sha1, 5 | const unsigned char *new_sha1); 6 | 7 | extern void cgit_print_diff(const char *new_hex, const char *old_hex, 8 | const char *prefix); 9 | 10 | #endif /* UI_DIFF_H */ 11 | -------------------------------------------------------------------------------- /ui-log.c: -------------------------------------------------------------------------------- 1 | /* ui-log.c: functions for log output 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | int files, add_lines, rem_lines; 14 | 15 | void count_lines(char *line, int size) 16 | { 17 | if (size <= 0) 18 | return; 19 | 20 | if (line[0] == '+') 21 | add_lines++; 22 | 23 | else if (line[0] == '-') 24 | rem_lines++; 25 | } 26 | 27 | void inspect_files(struct diff_filepair *pair) 28 | { 29 | files++; 30 | if (ctx.repo->enable_log_linecount) 31 | cgit_diff_files(pair->one->sha1, pair->two->sha1, count_lines); 32 | } 33 | 34 | void print_commit(struct commit *commit) 35 | { 36 | struct commitinfo *info; 37 | char *tmp; 38 | 39 | info = cgit_parse_commit(commit); 40 | html(""); 41 | tmp = fmt("id=%s", sha1_to_hex(commit->object.sha1)); 42 | tmp = cgit_pageurl(ctx.repo->url, "commit", tmp); 43 | html_link_open(tmp, NULL, NULL); 44 | cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE); 45 | html_link_close(); 46 | html(""); 47 | cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head, 48 | sha1_to_hex(commit->object.sha1)); 49 | html(""); 50 | html_txt(info->author); 51 | if (ctx.repo->enable_log_filecount) { 52 | files = 0; 53 | add_lines = 0; 54 | rem_lines = 0; 55 | cgit_diff_commit(commit, inspect_files); 56 | html(""); 57 | htmlf("%d", files); 58 | if (ctx.repo->enable_log_linecount) { 59 | html(""); 60 | htmlf("-%d/+%d", rem_lines, add_lines); 61 | } 62 | } 63 | html("\n"); 64 | cgit_free_commitinfo(info); 65 | } 66 | 67 | 68 | void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern, 69 | char *path, int pager) 70 | { 71 | struct rev_info rev; 72 | struct commit *commit; 73 | const char *argv[] = {NULL, tip, NULL, NULL, NULL}; 74 | int argc = 2; 75 | int i, columns = 3; 76 | 77 | if (!tip) 78 | argv[1] = ctx.qry.head; 79 | 80 | if (grep && pattern && (!strcmp(grep, "grep") || 81 | !strcmp(grep, "author") || 82 | !strcmp(grep, "committer"))) 83 | argv[argc++] = fmt("--%s=%s", grep, pattern); 84 | 85 | if (path) { 86 | argv[argc++] = "--"; 87 | argv[argc++] = path; 88 | } 89 | init_revisions(&rev, NULL); 90 | rev.abbrev = DEFAULT_ABBREV; 91 | rev.commit_format = CMIT_FMT_DEFAULT; 92 | rev.verbose_header = 1; 93 | rev.show_root_diff = 0; 94 | setup_revisions(argc, argv, &rev, NULL); 95 | rev.grep_filter.regflags |= REG_ICASE; 96 | compile_grep_patterns(&rev.grep_filter); 97 | prepare_revision_walk(&rev); 98 | 99 | if (pager) 100 | html(""); 101 | 102 | html("" 103 | "" 104 | ""); 105 | if (ctx.repo->enable_log_filecount) { 106 | html(""); 107 | columns++; 108 | if (ctx.repo->enable_log_linecount) { 109 | html(""); 110 | columns++; 111 | } 112 | } 113 | html("\n"); 114 | 115 | if (ofs<0) 116 | ofs = 0; 117 | 118 | for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; i++) { 119 | free(commit->buffer); 120 | commit->buffer = NULL; 121 | free_commit_list(commit->parents); 122 | commit->parents = NULL; 123 | } 124 | 125 | for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; i++) { 126 | print_commit(commit); 127 | free(commit->buffer); 128 | commit->buffer = NULL; 129 | free_commit_list(commit->parents); 130 | commit->parents = NULL; 131 | } 132 | if (pager) { 133 | htmlf("
AgeCommit messageAuthorFilesLines
", 134 | columns); 135 | if (ofs > 0) { 136 | cgit_log_link("[prev]", NULL, NULL, ctx.qry.head, 137 | ctx.qry.sha1, ctx.qry.path, 138 | ofs - cnt, ctx.qry.grep, 139 | ctx.qry.search); 140 | html(" "); 141 | } 142 | if ((commit = get_revision(&rev)) != NULL) { 143 | cgit_log_link("[next]", NULL, NULL, ctx.qry.head, 144 | ctx.qry.sha1, ctx.qry.path, 145 | ofs + cnt, ctx.qry.grep, 146 | ctx.qry.search); 147 | } 148 | html("
"); 149 | } else if ((commit = get_revision(&rev)) != NULL) { 150 | html(""); 151 | cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, NULL, 0, 152 | NULL, NULL); 153 | html("\n"); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /ui-log.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_LOG_H 2 | #define UI_LOG_H 3 | 4 | extern void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, 5 | char *pattern, char *path, int pager); 6 | 7 | #endif /* UI_LOG_H */ 8 | -------------------------------------------------------------------------------- /ui-patch.c: -------------------------------------------------------------------------------- 1 | /* ui-patch.c: generate patch view 2 | * 3 | * Copyright (C) 2007 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | static void print_line(char *line, int len) 14 | { 15 | char c = line[len-1]; 16 | 17 | line[len-1] = '\0'; 18 | htmlf("%s\n", line); 19 | line[len-1] = c; 20 | } 21 | 22 | static void header(unsigned char *sha1, char *path1, int mode1, 23 | unsigned char *sha2, char *path2, int mode2) 24 | { 25 | char *abbrev1, *abbrev2; 26 | int subproject; 27 | 28 | subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2)); 29 | htmlf("diff --git a/%s b/%s\n", path1, path2); 30 | 31 | if (is_null_sha1(sha1)) 32 | path1 = "dev/null"; 33 | if (is_null_sha1(sha2)) 34 | path2 = "dev/null"; 35 | 36 | if (mode1 == 0) 37 | htmlf("new file mode %.6o\n", mode2); 38 | 39 | if (mode2 == 0) 40 | htmlf("deleted file mode %.6o\n", mode1); 41 | 42 | if (!subproject) { 43 | abbrev1 = xstrdup(find_unique_abbrev(sha1, DEFAULT_ABBREV)); 44 | abbrev2 = xstrdup(find_unique_abbrev(sha2, DEFAULT_ABBREV)); 45 | htmlf("index %s..%s", abbrev1, abbrev2); 46 | free(abbrev1); 47 | free(abbrev2); 48 | if (mode1 != 0 && mode2 != 0) { 49 | htmlf(" %.6o", mode1); 50 | if (mode2 != mode1) 51 | htmlf("..%.6o", mode2); 52 | } 53 | htmlf("\n--- a/%s\n", path1); 54 | htmlf("+++ b/%s\n", path2); 55 | } 56 | } 57 | 58 | static void filepair_cb(struct diff_filepair *pair) 59 | { 60 | header(pair->one->sha1, pair->one->path, pair->one->mode, 61 | pair->two->sha1, pair->two->path, pair->two->mode); 62 | if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) { 63 | if (S_ISGITLINK(pair->one->mode)) 64 | print_line(fmt("-Subproject %s", sha1_to_hex(pair->one->sha1)), 52); 65 | if (S_ISGITLINK(pair->two->mode)) 66 | print_line(fmt("+Subproject %s", sha1_to_hex(pair->two->sha1)), 52); 67 | return; 68 | } 69 | if (cgit_diff_files(pair->one->sha1, pair->two->sha1, print_line)) 70 | html("Error running diff"); 71 | } 72 | 73 | void cgit_print_patch(char *hex) 74 | { 75 | struct commit *commit; 76 | struct commitinfo *info; 77 | unsigned char sha1[20], old_sha1[20]; 78 | char *patchname; 79 | 80 | if (!hex) 81 | hex = ctx.qry.head; 82 | 83 | if (get_sha1(hex, sha1)) { 84 | cgit_print_error(fmt("Bad object id: %s", hex)); 85 | return; 86 | } 87 | commit = lookup_commit_reference(sha1); 88 | if (!commit) { 89 | cgit_print_error(fmt("Bad commit reference: %s", hex)); 90 | return; 91 | } 92 | info = cgit_parse_commit(commit); 93 | 94 | if (commit->parents && commit->parents->item) 95 | hashcpy(old_sha1, commit->parents->item->object.sha1); 96 | else 97 | hashclr(old_sha1); 98 | 99 | patchname = fmt("%s.patch", sha1_to_hex(sha1)); 100 | ctx.page.mimetype = "text/plain"; 101 | ctx.page.filename = patchname; 102 | cgit_print_http_headers(&ctx); 103 | htmlf("From %s Mon Sep 17 00:00:00 2001\n", sha1_to_hex(sha1)); 104 | htmlf("From: %s%s\n", info->author, info->author_email); 105 | html("Date: "); 106 | cgit_print_date(info->author_date, "%a, %d %b %Y %H:%M:%S %z%n", ctx.cfg.local_time); 107 | htmlf("Subject: %s\n\n", info->subject); 108 | if (info->msg && *info->msg) { 109 | htmlf("%s", info->msg); 110 | if (info->msg[strlen(info->msg) - 1] != '\n') 111 | html("\n"); 112 | } 113 | html("---\n"); 114 | cgit_diff_tree(old_sha1, sha1, filepair_cb, NULL); 115 | html("--\n"); 116 | htmlf("cgit %s\n", CGIT_VERSION); 117 | cgit_free_commitinfo(info); 118 | } 119 | -------------------------------------------------------------------------------- /ui-patch.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_PATCH_H 2 | #define UI_PATCH_H 3 | 4 | extern void cgit_print_patch(char *hex); 5 | 6 | #endif /* UI_PATCH_H */ 7 | -------------------------------------------------------------------------------- /ui-plain.c: -------------------------------------------------------------------------------- 1 | /* ui-plain.c: functions for output of plain blobs by path 2 | * 3 | * Copyright (C) 2008 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | char *curr_rev; 14 | char *match_path; 15 | int match; 16 | 17 | static void print_object(const unsigned char *sha1, const char *path) 18 | { 19 | enum object_type type; 20 | char *buf; 21 | unsigned long size; 22 | 23 | type = sha1_object_info(sha1, &size); 24 | if (type == OBJ_BAD) { 25 | html_status(404, "Not found", 0); 26 | return; 27 | } 28 | 29 | buf = read_sha1_file(sha1, &type, &size); 30 | if (!buf) { 31 | html_status(404, "Not found", 0); 32 | return; 33 | } 34 | ctx.page.mimetype = "text/plain"; 35 | ctx.page.filename = fmt("%s", path); 36 | ctx.page.size = size; 37 | cgit_print_http_headers(&ctx); 38 | html_raw(buf, size); 39 | match = 1; 40 | } 41 | 42 | static int walk_tree(const unsigned char *sha1, const char *base, int baselen, 43 | const char *pathname, unsigned mode, int stage, 44 | void *cbdata) 45 | { 46 | if (S_ISDIR(mode)) 47 | return READ_TREE_RECURSIVE; 48 | 49 | if (S_ISREG(mode)) 50 | print_object(sha1, pathname); 51 | 52 | return 0; 53 | } 54 | 55 | void cgit_print_plain(struct cgit_context *ctx) 56 | { 57 | const char *rev = ctx->qry.sha1; 58 | unsigned char sha1[20]; 59 | struct commit *commit; 60 | const char *paths[] = {ctx->qry.path, NULL}; 61 | 62 | if (!rev) 63 | rev = ctx->qry.head; 64 | 65 | curr_rev = xstrdup(rev); 66 | if (get_sha1(rev, sha1)) { 67 | html_status(404, "Not found", 0); 68 | return; 69 | } 70 | commit = lookup_commit_reference(sha1); 71 | if (!commit || parse_commit(commit)) { 72 | html_status(404, "Not found", 0); 73 | return; 74 | } 75 | match_path = ctx->qry.path; 76 | read_tree_recursive(commit->tree, NULL, 0, 0, paths, walk_tree, NULL); 77 | if (!match) 78 | html_status(404, "Not found", 0); 79 | } 80 | -------------------------------------------------------------------------------- /ui-plain.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_PLAIN_H 2 | #define UI_PLAIN_H 3 | 4 | extern void cgit_print_plain(struct cgit_context *ctx); 5 | 6 | #endif /* UI_PLAIN_H */ 7 | -------------------------------------------------------------------------------- /ui-refs.c: -------------------------------------------------------------------------------- 1 | /* ui-refs.c: browse symbolic refs 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | static int header; 14 | 15 | static int cmp_age(int age1, int age2) 16 | { 17 | if (age1 != 0 && age2 != 0) 18 | return age2 - age1; 19 | 20 | if (age1 == 0 && age2 == 0) 21 | return 0; 22 | 23 | if (age1 == 0) 24 | return +1; 25 | 26 | return -1; 27 | } 28 | 29 | static int cmp_ref_name(const void *a, const void *b) 30 | { 31 | struct refinfo *r1 = *(struct refinfo **)a; 32 | struct refinfo *r2 = *(struct refinfo **)b; 33 | 34 | return strcmp(r1->refname, r2->refname); 35 | } 36 | 37 | static int cmp_branch_age(const void *a, const void *b) 38 | { 39 | struct refinfo *r1 = *(struct refinfo **)a; 40 | struct refinfo *r2 = *(struct refinfo **)b; 41 | 42 | return cmp_age(r1->commit->committer_date, r2->commit->committer_date); 43 | } 44 | 45 | static int cmp_tag_age(const void *a, const void *b) 46 | { 47 | struct refinfo *r1 = *(struct refinfo **)a; 48 | struct refinfo *r2 = *(struct refinfo **)b; 49 | 50 | return cmp_age(r1->tag->tagger_date, r2->tag->tagger_date); 51 | } 52 | 53 | static int print_branch(struct refinfo *ref) 54 | { 55 | struct commitinfo *info = ref->commit; 56 | char *name = (char *)ref->refname; 57 | 58 | if (!info) 59 | return 1; 60 | html(""); 61 | cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL); 62 | html(""); 63 | 64 | if (ref->object->type == OBJ_COMMIT) { 65 | cgit_commit_link(info->subject, NULL, NULL, name, NULL); 66 | html(""); 67 | html_txt(info->author); 68 | html(""); 69 | cgit_print_age(info->commit->date, -1, NULL); 70 | } else { 71 | html(""); 72 | cgit_object_link(ref->object); 73 | } 74 | html("\n"); 75 | return 0; 76 | } 77 | 78 | static void print_tag_header() 79 | { 80 | html("Tag" 81 | "Reference" 82 | "Author" 83 | "Age\n"); 84 | header = 1; 85 | } 86 | 87 | static int print_tag(struct refinfo *ref) 88 | { 89 | struct tag *tag; 90 | struct taginfo *info; 91 | char *name = (char *)ref->refname; 92 | 93 | if (ref->object->type == OBJ_TAG) { 94 | tag = (struct tag *)ref->object; 95 | info = ref->tag; 96 | if (!tag || !info) 97 | return 1; 98 | html(""); 99 | cgit_tag_link(name, NULL, NULL, ctx.qry.head, name); 100 | html(""); 101 | cgit_object_link(tag->tagged); 102 | html(""); 103 | if (info->tagger) 104 | html(info->tagger); 105 | html(""); 106 | if (info->tagger_date > 0) 107 | cgit_print_age(info->tagger_date, -1, NULL); 108 | html("\n"); 109 | } else { 110 | if (!header) 111 | print_tag_header(); 112 | html(""); 113 | html_txt(name); 114 | html(""); 115 | cgit_object_link(ref->object); 116 | html("\n"); 117 | } 118 | return 0; 119 | } 120 | 121 | static void print_refs_link(char *path) 122 | { 123 | html(""); 124 | cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path); 125 | html(""); 126 | } 127 | 128 | void cgit_print_branches(int maxcount) 129 | { 130 | struct reflist list; 131 | int i; 132 | 133 | html("Branch" 134 | "Commit message" 135 | "Author" 136 | "Age\n"); 137 | 138 | list.refs = NULL; 139 | list.alloc = list.count = 0; 140 | for_each_branch_ref(cgit_refs_cb, &list); 141 | 142 | if (maxcount == 0 || maxcount > list.count) 143 | maxcount = list.count; 144 | 145 | if (maxcount < list.count) { 146 | qsort(list.refs, list.count, sizeof(*list.refs), cmp_branch_age); 147 | qsort(list.refs, maxcount, sizeof(*list.refs), cmp_ref_name); 148 | } 149 | 150 | for(i=0; i list.count) 172 | maxcount = list.count; 173 | print_tag_header(); 174 | for(i=0; i"); 185 | 186 | if (ctx.qry.path && !strncmp(ctx.qry.path, "heads", 5)) 187 | cgit_print_branches(0); 188 | else if (ctx.qry.path && !strncmp(ctx.qry.path, "tags", 4)) 189 | cgit_print_tags(0); 190 | else { 191 | cgit_print_branches(0); 192 | html(" "); 193 | cgit_print_tags(0); 194 | } 195 | html(""); 196 | } 197 | -------------------------------------------------------------------------------- /ui-refs.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_REFS_H 2 | #define UI_REFS_H 3 | 4 | extern void cgit_print_branches(int maxcount); 5 | extern void cgit_print_tags(int maxcount); 6 | extern void cgit_print_refs(); 7 | 8 | #endif /* UI_REFS_H */ 9 | -------------------------------------------------------------------------------- /ui-repolist.c: -------------------------------------------------------------------------------- 1 | /* ui-repolist.c: functions for generating the repolist page 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include 10 | 11 | #include "cgit.h" 12 | #include "html.h" 13 | #include "ui-shared.h" 14 | 15 | time_t read_agefile(char *path) 16 | { 17 | FILE *f; 18 | static char buf[64], buf2[64]; 19 | 20 | if (!(f = fopen(path, "r"))) 21 | return -1; 22 | if (fgets(buf, sizeof(buf), f) == NULL) 23 | return -1; 24 | fclose(f); 25 | if (parse_date(buf, buf2, sizeof(buf2))) 26 | return strtoul(buf2, NULL, 10); 27 | else 28 | return 0; 29 | } 30 | 31 | static void print_modtime(struct cgit_repo *repo) 32 | { 33 | char *path; 34 | struct stat s; 35 | 36 | path = fmt("%s/%s", repo->path, ctx.cfg.agefile); 37 | if (stat(path, &s) == 0) { 38 | cgit_print_age(read_agefile(path), -1, NULL); 39 | return; 40 | } 41 | 42 | path = fmt("%s/refs/heads/%s", repo->path, repo->defbranch); 43 | if (stat(path, &s) != 0) 44 | return; 45 | cgit_print_age(s.st_mtime, -1, NULL); 46 | } 47 | 48 | int is_match(struct cgit_repo *repo) 49 | { 50 | if (!ctx.qry.search) 51 | return 1; 52 | if (repo->url && strcasestr(repo->url, ctx.qry.search)) 53 | return 1; 54 | if (repo->name && strcasestr(repo->name, ctx.qry.search)) 55 | return 1; 56 | if (repo->desc && strcasestr(repo->desc, ctx.qry.search)) 57 | return 1; 58 | if (repo->owner && strcasestr(repo->owner, ctx.qry.search)) 59 | return 1; 60 | return 0; 61 | } 62 | 63 | int is_in_url(struct cgit_repo *repo) 64 | { 65 | if (!ctx.qry.url) 66 | return 1; 67 | if (repo->url && !prefixcmp(repo->url, ctx.qry.url)) 68 | return 1; 69 | return 0; 70 | } 71 | 72 | void print_header(int columns) 73 | { 74 | html("" 75 | "Name" 76 | "Description" 77 | "Owner" 78 | "Idle"); 79 | if (ctx.cfg.enable_index_links) 80 | html("Links"); 81 | html("\n"); 82 | } 83 | 84 | 85 | void print_pager(int items, int pagelen, char *search) 86 | { 87 | int i; 88 | html("
"); 89 | for(i = 0; i * pagelen < items; i++) 90 | cgit_index_link(fmt("[%d]", i+1), fmt("Page %d", i+1), NULL, 91 | search, i * pagelen); 92 | html("
"); 93 | } 94 | 95 | void cgit_print_repolist() 96 | { 97 | int i, columns = 4, hits = 0, header = 0; 98 | char *last_group = NULL; 99 | 100 | if (ctx.cfg.enable_index_links) 101 | columns++; 102 | 103 | ctx.page.title = ctx.cfg.root_title; 104 | cgit_print_http_headers(&ctx); 105 | cgit_print_docstart(&ctx); 106 | cgit_print_pageheader(&ctx); 107 | 108 | if (ctx.cfg.index_header) 109 | html_include(ctx.cfg.index_header); 110 | 111 | html(""); 112 | for (i=0; i ctx.qry.ofs + ctx.cfg.max_repo_count) 120 | continue; 121 | if (!header++) 122 | print_header(columns); 123 | if ((last_group == NULL && ctx.repo->group != NULL) || 124 | (last_group != NULL && ctx.repo->group == NULL) || 125 | (last_group != NULL && ctx.repo->group != NULL && 126 | strcmp(ctx.repo->group, last_group))) { 127 | htmlf(""); 131 | last_group = ctx.repo->group; 132 | } 133 | htmlf(""); 145 | if (ctx.cfg.enable_index_links) { 146 | html(""); 152 | } 153 | html("\n"); 154 | } 155 | html("
", 128 | columns); 129 | html_txt(ctx.repo->group); 130 | html("
", 134 | ctx.repo->group ? "sublevel-repo" : "toplevel-repo"); 135 | cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL); 136 | html(""); 137 | html_link_open(cgit_repourl(ctx.repo->url), NULL, NULL); 138 | html_ntxt(ctx.cfg.max_repodesc_len, ctx.repo->desc); 139 | html_link_close(); 140 | html(""); 141 | html_txt(ctx.repo->owner); 142 | html(""); 143 | print_modtime(ctx.repo); 144 | html(""); 147 | cgit_summary_link("summary", NULL, "button", NULL); 148 | cgit_log_link("log", NULL, "button", NULL, NULL, NULL, 149 | 0, NULL, NULL); 150 | cgit_tree_link("tree", NULL, "button", NULL, NULL, NULL); 151 | html("
"); 156 | if (!hits) 157 | cgit_print_error("No repositories found"); 158 | else if (hits > ctx.cfg.max_repo_count) 159 | print_pager(hits, ctx.cfg.max_repo_count, ctx.qry.search); 160 | cgit_print_docend(); 161 | } 162 | 163 | void cgit_print_site_readme() 164 | { 165 | if (ctx.cfg.root_readme) 166 | html_include(ctx.cfg.root_readme); 167 | } 168 | -------------------------------------------------------------------------------- /ui-repolist.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_REPOLIST_H 2 | #define UI_REPOLIST_H 3 | 4 | extern void cgit_print_repolist(); 5 | extern void cgit_print_site_readme(); 6 | 7 | #endif /* UI_REPOLIST_H */ 8 | -------------------------------------------------------------------------------- /ui-shared.c: -------------------------------------------------------------------------------- 1 | /* ui-shared.c: common web output functions 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "cmd.h" 11 | #include "html.h" 12 | 13 | const char cgit_doctype[] = 14 | "\n"; 16 | 17 | static char *http_date(time_t t) 18 | { 19 | static char day[][4] = 20 | {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; 21 | static char month[][4] = 22 | {"Jan", "Feb", "Mar", "Apr", "May", "Jun", 23 | "Jul", "Aug", "Sep", "Oct", "Now", "Dec"}; 24 | struct tm *tm = gmtime(&t); 25 | return fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm->tm_wday], 26 | tm->tm_mday, month[tm->tm_mon], 1900+tm->tm_year, 27 | tm->tm_hour, tm->tm_min, tm->tm_sec); 28 | } 29 | 30 | void cgit_print_error(char *msg) 31 | { 32 | html("
"); 33 | html_txt(msg); 34 | html("
\n"); 35 | } 36 | 37 | char *cgit_hosturl() 38 | { 39 | char *host, *port; 40 | 41 | host = getenv("HTTP_HOST"); 42 | if (host) { 43 | host = xstrdup(host); 44 | } else { 45 | host = getenv("SERVER_NAME"); 46 | if (!host) 47 | return NULL; 48 | port = getenv("SERVER_PORT"); 49 | if (port && atoi(port) != 80) 50 | host = xstrdup(fmt("%s:%d", host, atoi(port))); 51 | else 52 | host = xstrdup(host); 53 | } 54 | return host; 55 | } 56 | 57 | char *cgit_rooturl() 58 | { 59 | if (ctx.cfg.virtual_root) 60 | return fmt("%s/", ctx.cfg.virtual_root); 61 | else 62 | return ctx.cfg.script_name; 63 | } 64 | 65 | char *cgit_repourl(const char *reponame) 66 | { 67 | if (ctx.cfg.virtual_root) { 68 | return fmt("%s/%s/", ctx.cfg.virtual_root, reponame); 69 | } else { 70 | return fmt("?r=%s", reponame); 71 | } 72 | } 73 | 74 | char *cgit_fileurl(const char *reponame, const char *pagename, 75 | const char *filename, const char *query) 76 | { 77 | char *tmp; 78 | char *delim; 79 | 80 | if (ctx.cfg.virtual_root) { 81 | tmp = fmt("%s/%s/%s/%s", ctx.cfg.virtual_root, reponame, 82 | pagename, (filename ? filename:"")); 83 | delim = "?"; 84 | } else { 85 | tmp = fmt("?url=%s/%s/%s", reponame, pagename, 86 | (filename ? filename : "")); 87 | delim = "&"; 88 | } 89 | if (query) 90 | tmp = fmt("%s%s%s", tmp, delim, query); 91 | return tmp; 92 | } 93 | 94 | char *cgit_pageurl(const char *reponame, const char *pagename, 95 | const char *query) 96 | { 97 | return cgit_fileurl(reponame,pagename,0,query); 98 | } 99 | 100 | const char *cgit_repobasename(const char *reponame) 101 | { 102 | /* I assume we don't need to store more than one repo basename */ 103 | static char rvbuf[1024]; 104 | int p; 105 | const char *rv; 106 | strncpy(rvbuf,reponame,sizeof(rvbuf)); 107 | if(rvbuf[sizeof(rvbuf)-1]) 108 | die("cgit_repobasename: truncated repository name '%s'", reponame); 109 | p = strlen(rvbuf)-1; 110 | /* strip trailing slashes */ 111 | while(p && rvbuf[p]=='/') rvbuf[p--]=0; 112 | /* strip trailing .git */ 113 | if(p>=3 && !strncmp(&rvbuf[p-3],".git",4)) { 114 | p -= 3; rvbuf[p--] = 0; 115 | } 116 | /* strip more trailing slashes if any */ 117 | while( p && rvbuf[p]=='/') rvbuf[p--]=0; 118 | /* find last slash in the remaining string */ 119 | rv = strrchr(rvbuf,'/'); 120 | if(rv) 121 | return ++rv; 122 | return rvbuf; 123 | } 124 | 125 | char *cgit_currurl() 126 | { 127 | if (!ctx.cfg.virtual_root) 128 | return ctx.cfg.script_name; 129 | else if (ctx.qry.page) 130 | return fmt("%s/%s/%s/", ctx.cfg.virtual_root, ctx.qry.repo, ctx.qry.page); 131 | else if (ctx.qry.repo) 132 | return fmt("%s/%s/", ctx.cfg.virtual_root, ctx.qry.repo); 133 | else 134 | return fmt("%s/", ctx.cfg.virtual_root); 135 | } 136 | 137 | static void site_url(char *page, char *search, int ofs) 138 | { 139 | char *delim = "?"; 140 | 141 | if (ctx.cfg.virtual_root) { 142 | html_attr(ctx.cfg.virtual_root); 143 | if (ctx.cfg.virtual_root[strlen(ctx.cfg.virtual_root) - 1] != '/') 144 | html("/"); 145 | } else 146 | html(ctx.cfg.script_name); 147 | 148 | if (page) { 149 | htmlf("?p=%s", page); 150 | delim = "&"; 151 | } 152 | if (search) { 153 | html(delim); 154 | html("q="); 155 | html_attr(search); 156 | delim = "&"; 157 | } 158 | if (ofs) { 159 | html(delim); 160 | htmlf("ofs=%d", ofs); 161 | } 162 | } 163 | 164 | static void site_link(char *page, char *name, char *title, char *class, 165 | char *search, int ofs) 166 | { 167 | html(""); 181 | html_txt(name); 182 | html("
"); 183 | } 184 | 185 | void cgit_index_link(char *name, char *title, char *class, char *pattern, 186 | int ofs) 187 | { 188 | site_link(NULL, name, title, class, pattern, ofs); 189 | } 190 | 191 | static char *repolink(char *title, char *class, char *page, char *head, 192 | char *path) 193 | { 194 | char *delim = "?"; 195 | 196 | html("url); 213 | if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/') 214 | html("/"); 215 | if (page) { 216 | html_url_path(page); 217 | html("/"); 218 | if (path) 219 | html_url_path(path); 220 | } 221 | } else { 222 | html(ctx.cfg.script_name); 223 | html("?url="); 224 | html_url_arg(ctx.repo->url); 225 | if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/') 226 | html("/"); 227 | if (page) { 228 | html_url_arg(page); 229 | html("/"); 230 | if (path) 231 | html_url_arg(path); 232 | } 233 | delim = "&"; 234 | } 235 | if (head && strcmp(head, ctx.repo->defbranch)) { 236 | html(delim); 237 | html("h="); 238 | html_url_arg(head); 239 | delim = "&"; 240 | } 241 | return fmt("%s", delim); 242 | } 243 | 244 | static void reporevlink(char *page, char *name, char *title, char *class, 245 | char *head, char *rev, char *path) 246 | { 247 | char *delim; 248 | 249 | delim = repolink(title, class, page, head, path); 250 | if (rev && strcmp(rev, ctx.qry.head)) { 251 | html(delim); 252 | html("id="); 253 | html_url_arg(rev); 254 | } 255 | html("'>"); 256 | html_txt(name); 257 | html(""); 258 | } 259 | 260 | void cgit_summary_link(char *name, char *title, char *class, char *head) 261 | { 262 | reporevlink(NULL, name, title, class, head, NULL, NULL); 263 | } 264 | 265 | void cgit_tag_link(char *name, char *title, char *class, char *head, 266 | char *rev) 267 | { 268 | reporevlink("tag", name, title, class, head, rev, NULL); 269 | } 270 | 271 | void cgit_tree_link(char *name, char *title, char *class, char *head, 272 | char *rev, char *path) 273 | { 274 | reporevlink("tree", name, title, class, head, rev, path); 275 | } 276 | 277 | void cgit_plain_link(char *name, char *title, char *class, char *head, 278 | char *rev, char *path) 279 | { 280 | reporevlink("plain", name, title, class, head, rev, path); 281 | } 282 | 283 | void cgit_log_link(char *name, char *title, char *class, char *head, 284 | char *rev, char *path, int ofs, char *grep, char *pattern) 285 | { 286 | char *delim; 287 | 288 | delim = repolink(title, class, "log", head, path); 289 | if (rev && strcmp(rev, ctx.qry.head)) { 290 | html(delim); 291 | html("id="); 292 | html_url_arg(rev); 293 | delim = "&"; 294 | } 295 | if (grep && pattern) { 296 | html(delim); 297 | html("qt="); 298 | html_url_arg(grep); 299 | delim = "&"; 300 | html(delim); 301 | html("q="); 302 | html_url_arg(pattern); 303 | } 304 | if (ofs > 0) { 305 | html(delim); 306 | html("ofs="); 307 | htmlf("%d", ofs); 308 | } 309 | html("'>"); 310 | html_txt(name); 311 | html(""); 312 | } 313 | 314 | void cgit_commit_link(char *name, char *title, char *class, char *head, 315 | char *rev) 316 | { 317 | if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) { 318 | name[ctx.cfg.max_msg_len] = '\0'; 319 | name[ctx.cfg.max_msg_len - 1] = '.'; 320 | name[ctx.cfg.max_msg_len - 2] = '.'; 321 | name[ctx.cfg.max_msg_len - 3] = '.'; 322 | } 323 | reporevlink("commit", name, title, class, head, rev, NULL); 324 | } 325 | 326 | void cgit_refs_link(char *name, char *title, char *class, char *head, 327 | char *rev, char *path) 328 | { 329 | reporevlink("refs", name, title, class, head, rev, path); 330 | } 331 | 332 | void cgit_snapshot_link(char *name, char *title, char *class, char *head, 333 | char *rev, char *archivename) 334 | { 335 | reporevlink("snapshot", name, title, class, head, rev, archivename); 336 | } 337 | 338 | void cgit_diff_link(char *name, char *title, char *class, char *head, 339 | char *new_rev, char *old_rev, char *path) 340 | { 341 | char *delim; 342 | 343 | delim = repolink(title, class, "diff", head, path); 344 | if (new_rev && strcmp(new_rev, ctx.qry.head)) { 345 | html(delim); 346 | html("id="); 347 | html_url_arg(new_rev); 348 | delim = "&"; 349 | } 350 | if (old_rev) { 351 | html(delim); 352 | html("id2="); 353 | html_url_arg(old_rev); 354 | } 355 | html("'>"); 356 | html_txt(name); 357 | html(""); 358 | } 359 | 360 | void cgit_patch_link(char *name, char *title, char *class, char *head, 361 | char *rev) 362 | { 363 | reporevlink("patch", name, title, class, head, rev, NULL); 364 | } 365 | 366 | void cgit_object_link(struct object *obj) 367 | { 368 | char *page, *rev, *name; 369 | 370 | if (obj->type == OBJ_COMMIT) { 371 | cgit_commit_link(fmt("commit %s", sha1_to_hex(obj->sha1)), NULL, NULL, 372 | ctx.qry.head, sha1_to_hex(obj->sha1)); 373 | return; 374 | } else if (obj->type == OBJ_TREE) 375 | page = "tree"; 376 | else if (obj->type == OBJ_TAG) 377 | page = "tag"; 378 | else 379 | page = "blob"; 380 | rev = sha1_to_hex(obj->sha1); 381 | name = fmt("%s %s", typename(obj->type), rev); 382 | reporevlink(page, name, NULL, NULL, ctx.qry.head, rev, NULL); 383 | } 384 | 385 | void cgit_print_date(time_t secs, char *format, int local_time) 386 | { 387 | char buf[64]; 388 | struct tm *time; 389 | 390 | if (!secs) 391 | return; 392 | if(local_time) 393 | time = localtime(&secs); 394 | else 395 | time = gmtime(&secs); 396 | strftime(buf, sizeof(buf)-1, format, time); 397 | html_txt(buf); 398 | } 399 | 400 | void cgit_print_age(time_t t, time_t max_relative, char *format) 401 | { 402 | time_t now, secs; 403 | 404 | if (!t) 405 | return; 406 | time(&now); 407 | secs = now - t; 408 | 409 | if (secs > max_relative && max_relative >= 0) { 410 | cgit_print_date(t, format, ctx.cfg.local_time); 411 | return; 412 | } 413 | 414 | if (secs < TM_HOUR * 2) { 415 | htmlf("%.0f min.", 416 | secs * 1.0 / TM_MIN); 417 | return; 418 | } 419 | if (secs < TM_DAY * 2) { 420 | htmlf("%.0f hours", 421 | secs * 1.0 / TM_HOUR); 422 | return; 423 | } 424 | if (secs < TM_WEEK * 2) { 425 | htmlf("%.0f days", 426 | secs * 1.0 / TM_DAY); 427 | return; 428 | } 429 | if (secs < TM_MONTH * 2) { 430 | htmlf("%.0f weeks", 431 | secs * 1.0 / TM_WEEK); 432 | return; 433 | } 434 | if (secs < TM_YEAR * 2) { 435 | htmlf("%.0f months", 436 | secs * 1.0 / TM_MONTH); 437 | return; 438 | } 439 | htmlf("%.0f years", 440 | secs * 1.0 / TM_YEAR); 441 | } 442 | 443 | void cgit_print_http_headers(struct cgit_context *ctx) 444 | { 445 | if (ctx->page.mimetype && ctx->page.charset) 446 | htmlf("Content-Type: %s; charset=%s\n", ctx->page.mimetype, 447 | ctx->page.charset); 448 | else if (ctx->page.mimetype) 449 | htmlf("Content-Type: %s\n", ctx->page.mimetype); 450 | if (ctx->page.size) 451 | htmlf("Content-Length: %ld\n", ctx->page.size); 452 | if (ctx->page.filename) 453 | htmlf("Content-Disposition: inline; filename=\"%s\"\n", 454 | ctx->page.filename); 455 | htmlf("Last-Modified: %s\n", http_date(ctx->page.modified)); 456 | htmlf("Expires: %s\n", http_date(ctx->page.expires)); 457 | html("\n"); 458 | } 459 | 460 | void cgit_print_docstart(struct cgit_context *ctx) 461 | { 462 | char *host = cgit_hosturl(); 463 | html(cgit_doctype); 464 | html("\n"); 465 | html("\n"); 466 | html(""); 467 | html_txt(ctx->page.title); 468 | html("\n"); 469 | htmlf("\n", cgit_version); 470 | if (ctx->cfg.robots && *ctx->cfg.robots) 471 | htmlf("\n", ctx->cfg.robots); 472 | html("\n"); 475 | if (ctx->cfg.favicon) { 476 | html("\n"); 479 | } 480 | if (host && ctx->repo) { 481 | html(""); 486 | } 487 | html("\n"); 488 | html("\n"); 489 | } 490 | 491 | void cgit_print_docend() 492 | { 493 | html(""); 494 | if (ctx.cfg.footer) 495 | html_include(ctx.cfg.footer); 496 | else { 497 | htmlf("\n"); 501 | } 502 | html("\n\n"); 503 | } 504 | 505 | int print_branch_option(const char *refname, const unsigned char *sha1, 506 | int flags, void *cb_data) 507 | { 508 | char *name = (char *)refname; 509 | html_option(name, name, ctx.qry.head); 510 | return 0; 511 | } 512 | 513 | int print_archive_ref(const char *refname, const unsigned char *sha1, 514 | int flags, void *cb_data) 515 | { 516 | struct tag *tag; 517 | struct taginfo *info; 518 | struct object *obj; 519 | char buf[256], *url; 520 | unsigned char fileid[20]; 521 | int *header = (int *)cb_data; 522 | 523 | if (prefixcmp(refname, "refs/archives")) 524 | return 0; 525 | strncpy(buf, refname+14, sizeof(buf)); 526 | obj = parse_object(sha1); 527 | if (!obj) 528 | return 1; 529 | if (obj->type == OBJ_TAG) { 530 | tag = lookup_tag(sha1); 531 | if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) 532 | return 0; 533 | hashcpy(fileid, tag->tagged->sha1); 534 | } else if (obj->type != OBJ_BLOB) { 535 | return 0; 536 | } else { 537 | hashcpy(fileid, sha1); 538 | } 539 | if (!*header) { 540 | html("

download

\n"); 541 | *header = 1; 542 | } 543 | url = cgit_pageurl(ctx.qry.repo, "blob", 544 | fmt("id=%s&path=%s", sha1_to_hex(fileid), 545 | buf)); 546 | html_link_open(url, NULL, "menu"); 547 | html_txt(strlpart(buf, 20)); 548 | html_link_close(); 549 | return 0; 550 | } 551 | 552 | void add_hidden_formfields(int incl_head, int incl_search, char *page) 553 | { 554 | char *url; 555 | 556 | if (!ctx.cfg.virtual_root) { 557 | url = fmt("%s/%s", ctx.qry.repo, page); 558 | if (ctx.qry.path) 559 | url = fmt("%s/%s", url, ctx.qry.path); 560 | html_hidden("url", url); 561 | } 562 | 563 | if (incl_head && ctx.qry.head && ctx.repo->defbranch && 564 | strcmp(ctx.qry.head, ctx.repo->defbranch)) 565 | html_hidden("h", ctx.qry.head); 566 | 567 | if (ctx.qry.sha1) 568 | html_hidden("id", ctx.qry.sha1); 569 | if (ctx.qry.sha2) 570 | html_hidden("id2", ctx.qry.sha2); 571 | 572 | if (incl_search) { 573 | if (ctx.qry.grep) 574 | html_hidden("qt", ctx.qry.grep); 575 | if (ctx.qry.search) 576 | html_hidden("q", ctx.qry.search); 577 | } 578 | } 579 | 580 | char *hc(struct cgit_cmd *cmd, const char *page) 581 | { 582 | return (strcmp(cmd->name, page) ? NULL : "active"); 583 | } 584 | 585 | void cgit_print_pageheader(struct cgit_context *ctx) 586 | { 587 | struct cgit_cmd *cmd = cgit_get_cmd(ctx); 588 | 589 | html("\n"); 590 | html("\n"); 591 | html("\n"); 599 | 600 | html("\n"); 616 | 617 | html("\n"); 629 | 630 | html("
\n"); 631 | if (ctx->repo) { 632 | cgit_summary_link("summary", NULL, hc(cmd, "summary"), 633 | ctx->qry.head); 634 | cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head, 635 | ctx->qry.sha1, NULL); 636 | cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head, 637 | NULL, NULL, 0, NULL, NULL); 638 | cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 639 | ctx->qry.sha1, NULL); 640 | cgit_commit_link("commit", NULL, hc(cmd, "commit"), 641 | ctx->qry.head, ctx->qry.sha1); 642 | cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 643 | ctx->qry.sha1, ctx->qry.sha2, NULL); 644 | if (ctx->repo->readme) 645 | reporevlink("about", "about", NULL, 646 | hc(cmd, "about"), ctx->qry.head, NULL, 647 | NULL); 648 | html(""); 649 | html("
\n"); 654 | add_hidden_formfields(1, 0, "log"); 655 | html("\n"); 660 | html("\n"); 663 | html("\n"); 664 | html("
\n"); 665 | } else { 666 | site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0); 667 | if (ctx->cfg.root_readme) 668 | site_link("about", "about", NULL, hc(cmd, "about"), 669 | NULL, 0); 670 | html("
"); 671 | html("
\n"); 674 | html("\n"); 677 | html("\n"); 678 | html("
"); 679 | } 680 | html("
\n"); 681 | html("
"); 682 | } 683 | 684 | void cgit_print_filemode(unsigned short mode) 685 | { 686 | if (S_ISDIR(mode)) 687 | html("d"); 688 | else if (S_ISLNK(mode)) 689 | html("l"); 690 | else if (S_ISGITLINK(mode)) 691 | html("m"); 692 | else 693 | html("-"); 694 | html_fileperm(mode >> 6); 695 | html_fileperm(mode >> 3); 696 | html_fileperm(mode); 697 | } 698 | 699 | void cgit_print_snapshot_links(const char *repo, const char *head, 700 | const char *hex, int snapshots) 701 | { 702 | const struct cgit_snapshot_format* f; 703 | char *filename; 704 | 705 | for (f = cgit_snapshot_formats; f->suffix; f++) { 706 | if (!(snapshots & f->bit)) 707 | continue; 708 | filename = fmt("%s-%s%s", cgit_repobasename(repo), hex, 709 | f->suffix); 710 | cgit_snapshot_link(filename, NULL, NULL, (char *)head, 711 | (char *)hex, filename); 712 | html("
"); 713 | } 714 | } 715 | -------------------------------------------------------------------------------- /ui-shared.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_SHARED_H 2 | #define UI_SHARED_H 3 | 4 | extern char *cgit_hosturl(); 5 | extern char *cgit_repourl(const char *reponame); 6 | extern char *cgit_fileurl(const char *reponame, const char *pagename, 7 | const char *filename, const char *query); 8 | extern char *cgit_pageurl(const char *reponame, const char *pagename, 9 | const char *query); 10 | 11 | extern void cgit_index_link(char *name, char *title, char *class, 12 | char *pattern, int ofs); 13 | extern void cgit_summary_link(char *name, char *title, char *class, char *head); 14 | extern void cgit_tag_link(char *name, char *title, char *class, char *head, 15 | char *rev); 16 | extern void cgit_tree_link(char *name, char *title, char *class, char *head, 17 | char *rev, char *path); 18 | extern void cgit_plain_link(char *name, char *title, char *class, char *head, 19 | char *rev, char *path); 20 | extern void cgit_log_link(char *name, char *title, char *class, char *head, 21 | char *rev, char *path, int ofs, char *grep, 22 | char *pattern); 23 | extern void cgit_commit_link(char *name, char *title, char *class, char *head, 24 | char *rev); 25 | extern void cgit_patch_link(char *name, char *title, char *class, char *head, 26 | char *rev); 27 | extern void cgit_refs_link(char *name, char *title, char *class, char *head, 28 | char *rev, char *path); 29 | extern void cgit_snapshot_link(char *name, char *title, char *class, 30 | char *head, char *rev, char *archivename); 31 | extern void cgit_diff_link(char *name, char *title, char *class, char *head, 32 | char *new_rev, char *old_rev, char *path); 33 | extern void cgit_object_link(struct object *obj); 34 | 35 | extern void cgit_print_error(char *msg); 36 | extern void cgit_print_date(time_t secs, char *format, int local_time); 37 | extern void cgit_print_age(time_t t, time_t max_relative, char *format); 38 | extern void cgit_print_http_headers(struct cgit_context *ctx); 39 | extern void cgit_print_docstart(struct cgit_context *ctx); 40 | extern void cgit_print_docend(); 41 | extern void cgit_print_pageheader(struct cgit_context *ctx); 42 | extern void cgit_print_filemode(unsigned short mode); 43 | extern void cgit_print_snapshot_links(const char *repo, const char *head, 44 | const char *hex, int snapshots); 45 | 46 | #endif /* UI_SHARED_H */ 47 | -------------------------------------------------------------------------------- /ui-snapshot.c: -------------------------------------------------------------------------------- 1 | /* ui-snapshot.c: generate snapshot of a commit 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | static int write_compressed_tar_archive(struct archiver_args *args,const char *filter) 14 | { 15 | int rw[2]; 16 | pid_t gzpid; 17 | int stdout2; 18 | int status; 19 | int rv; 20 | 21 | stdout2 = chk_non_negative(dup(STDIN_FILENO), "Preserving STDOUT before compressing"); 22 | chk_zero(pipe(rw), "Opening pipe from compressor subprocess"); 23 | gzpid = chk_non_negative(fork(), "Forking compressor subprocess"); 24 | if(gzpid==0) { 25 | /* child */ 26 | chk_zero(close(rw[1]), "Closing write end of pipe in child"); 27 | chk_zero(close(STDIN_FILENO), "Closing STDIN"); 28 | chk_non_negative(dup2(rw[0],STDIN_FILENO), "Redirecting compressor input to stdin"); 29 | execlp(filter,filter,NULL); 30 | _exit(-1); 31 | } 32 | /* parent */ 33 | chk_zero(close(rw[0]), "Closing read end of pipe"); 34 | chk_non_negative(dup2(rw[1],STDOUT_FILENO), "Redirecting output to compressor"); 35 | 36 | rv = write_tar_archive(args); 37 | 38 | chk_zero(close(STDOUT_FILENO), "Closing STDOUT redirected to compressor"); 39 | chk_non_negative(dup2(stdout2,STDOUT_FILENO), "Restoring uncompressed STDOUT"); 40 | chk_zero(close(stdout2), "Closing uncompressed STDOUT"); 41 | chk_zero(close(rw[1]), "Closing write end of pipe in parent"); 42 | chk_positive(waitpid(gzpid,&status,0), "Waiting on compressor process"); 43 | if(! ( WIFEXITED(status) && WEXITSTATUS(status)==0 ) ) 44 | cgit_print_error("Failed to compress archive"); 45 | 46 | return rv; 47 | } 48 | 49 | static int write_tar_gzip_archive(struct archiver_args *args) 50 | { 51 | return write_compressed_tar_archive(args,"gzip"); 52 | } 53 | 54 | static int write_tar_bzip2_archive(struct archiver_args *args) 55 | { 56 | return write_compressed_tar_archive(args,"bzip2"); 57 | } 58 | 59 | const struct cgit_snapshot_format cgit_snapshot_formats[] = { 60 | { ".zip", "application/x-zip", write_zip_archive, 0x1 }, 61 | { ".tar.gz", "application/x-tar", write_tar_gzip_archive, 0x2 }, 62 | { ".tar.bz2", "application/x-tar", write_tar_bzip2_archive, 0x4 }, 63 | { ".tar", "application/x-tar", write_tar_archive, 0x8 }, 64 | {} 65 | }; 66 | 67 | static const struct cgit_snapshot_format *get_format(const char *filename) 68 | { 69 | const struct cgit_snapshot_format *fmt; 70 | int fl, sl; 71 | 72 | fl = strlen(filename); 73 | for(fmt = cgit_snapshot_formats; fmt->suffix; fmt++) { 74 | sl = strlen(fmt->suffix); 75 | if (sl >= fl) 76 | continue; 77 | if (!strcmp(fmt->suffix, filename + fl - sl)) 78 | return fmt; 79 | } 80 | return NULL; 81 | } 82 | 83 | static int make_snapshot(const struct cgit_snapshot_format *format, 84 | const char *hex, const char *prefix, 85 | const char *filename) 86 | { 87 | struct archiver_args args; 88 | struct commit *commit; 89 | unsigned char sha1[20]; 90 | 91 | if(get_sha1(hex, sha1)) { 92 | cgit_print_error(fmt("Bad object id: %s", hex)); 93 | return 1; 94 | } 95 | commit = lookup_commit_reference(sha1); 96 | if(!commit) { 97 | cgit_print_error(fmt("Not a commit reference: %s", hex)); 98 | return 1; 99 | } 100 | memset(&args, 0, sizeof(args)); 101 | if (prefix) { 102 | args.base = fmt("%s/", prefix); 103 | args.baselen = strlen(prefix) + 1; 104 | } else { 105 | args.base = ""; 106 | args.baselen = 0; 107 | } 108 | args.tree = commit->tree; 109 | args.time = commit->date; 110 | ctx.page.mimetype = xstrdup(format->mimetype); 111 | ctx.page.filename = xstrdup(filename); 112 | cgit_print_http_headers(&ctx); 113 | format->write_func(&args); 114 | return 0; 115 | } 116 | 117 | char *dwim_filename = NULL; 118 | const char *dwim_refname = NULL; 119 | 120 | static int ref_cb(const char *refname, const unsigned char *sha1, int flags, 121 | void *cb_data) 122 | { 123 | const char *r = refname; 124 | while (r && *r) { 125 | fprintf(stderr, " cmp %s with %s:", dwim_filename, r); 126 | if (!strcmp(dwim_filename, r)) { 127 | fprintf(stderr, "MATCH!\n"); 128 | dwim_refname = refname; 129 | return 1; 130 | } 131 | fprintf(stderr, "no match\n"); 132 | if (isdigit(*r)) 133 | break; 134 | r++; 135 | } 136 | return 0; 137 | } 138 | 139 | /* Try to guess the requested revision by combining repo name and tag name 140 | * and comparing this to the requested snapshot name. E.g. the requested 141 | * snapshot is "cgit-0.7.2.tar.gz" while repo name is "cgit" and tag name 142 | * is "v0.7.2". First, the reponame is stripped off, leaving "-0.7.2.tar.gz". 143 | * Next, any '-' and '_' characters are stripped, leaving "0.7.2.tar.gz". 144 | * Finally, the requested format suffix is removed and we end up with "0.7.2". 145 | * Then we test each tag against this dwimmed filename, and for each tag 146 | * we even try to remove any leading characters which are non-digits. I.e. 147 | * we first compare with "v0.7.2", then with "0.7.2" and we've got a match. 148 | */ 149 | static const char *get_ref_from_filename(const char *url, const char *filename, 150 | const struct cgit_snapshot_format *fmt) 151 | { 152 | const char *reponame = cgit_repobasename(url); 153 | fprintf(stderr, "reponame=%s, filename=%s\n", reponame, filename); 154 | if (prefixcmp(filename, reponame)) 155 | return NULL; 156 | filename += strlen(reponame); 157 | while (filename && (*filename == '-' || *filename == '_')) 158 | filename++; 159 | dwim_filename = xstrdup(filename); 160 | dwim_filename[strlen(filename) - strlen(fmt->suffix)] = '\0'; 161 | for_each_tag_ref(ref_cb, NULL); 162 | return dwim_refname; 163 | } 164 | 165 | void cgit_print_snapshot(const char *head, const char *hex, const char *prefix, 166 | const char *filename, int snapshots, int dwim) 167 | { 168 | const struct cgit_snapshot_format* f; 169 | 170 | f = get_format(filename); 171 | if (!f) { 172 | ctx.page.mimetype = "text/html"; 173 | cgit_print_http_headers(&ctx); 174 | cgit_print_docstart(&ctx); 175 | cgit_print_pageheader(&ctx); 176 | cgit_print_error(fmt("Unsupported snapshot format: %s", filename)); 177 | cgit_print_docend(); 178 | return; 179 | } 180 | 181 | if (!hex && dwim) 182 | hex = get_ref_from_filename(ctx.repo->url, filename, f); 183 | 184 | if (!hex) 185 | hex = head; 186 | 187 | make_snapshot(f, hex, prefix, filename); 188 | } 189 | -------------------------------------------------------------------------------- /ui-snapshot.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_SNAPSHOT_H 2 | #define UI_SNAPSHOT_H 3 | 4 | extern void cgit_print_snapshot(const char *head, const char *hex, 5 | const char *prefix, const char *filename, 6 | int snapshot, int dwim); 7 | 8 | #endif /* UI_SNAPSHOT_H */ 9 | -------------------------------------------------------------------------------- /ui-summary.c: -------------------------------------------------------------------------------- 1 | /* ui-summary.c: functions for generating repo summary page 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-log.h" 12 | #include "ui-refs.h" 13 | 14 | int urls = 0; 15 | 16 | static void print_url(char *base, char *suffix) 17 | { 18 | if (!base || !*base) 19 | return; 20 | if (urls++ == 0) { 21 | html(" "); 22 | html("Clone\n"); 23 | } 24 | if (suffix && *suffix) 25 | base = fmt("%s/%s", base, suffix); 26 | html(""); 29 | html_txt(base); 30 | html("\n"); 31 | } 32 | 33 | static void print_urls(char *txt, char *suffix) 34 | { 35 | char *h = txt, *t, c; 36 | 37 | while (h && *h) { 38 | while (h && *h == ' ') 39 | h++; 40 | t = h; 41 | while (t && *t && *t != ' ') 42 | t++; 43 | c = *t; 44 | *t = 0; 45 | print_url(h, suffix); 46 | *t = c; 47 | h = t; 48 | } 49 | } 50 | 51 | void cgit_print_summary() 52 | { 53 | html(""); 54 | cgit_print_branches(ctx.cfg.summary_branches); 55 | html(""); 56 | cgit_print_tags(ctx.cfg.summary_tags); 57 | if (ctx.cfg.summary_log > 0) { 58 | html(""); 59 | cgit_print_log(ctx.qry.head, 0, ctx.cfg.summary_log, NULL, 60 | NULL, NULL, 0); 61 | } 62 | if (ctx.repo->clone_url) 63 | print_urls(ctx.repo->clone_url, NULL); 64 | else if (ctx.cfg.clone_prefix) 65 | print_urls(ctx.cfg.clone_prefix, ctx.repo->url); 66 | html("
 
 
"); 67 | } 68 | 69 | void cgit_print_repo_readme() 70 | { 71 | if (ctx.repo->readme) { 72 | html("
"); 73 | html_include(ctx.repo->readme); 74 | html("
"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ui-summary.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_SUMMARY_H 2 | #define UI_SUMMARY_H 3 | 4 | extern void cgit_print_summary(); 5 | extern void cgit_print_repo_readme(); 6 | 7 | #endif /* UI_SUMMARY_H */ 8 | -------------------------------------------------------------------------------- /ui-tag.c: -------------------------------------------------------------------------------- 1 | /* ui-tag.c: display a tag 2 | * 3 | * Copyright (C) 2007 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | static void print_tag_content(char *buf) 14 | { 15 | char *p; 16 | 17 | if (!buf) 18 | return; 19 | 20 | html("
"); 21 | p = strchr(buf, '\n'); 22 | if (p) 23 | *p = '\0'; 24 | html_txt(buf); 25 | html("
"); 26 | if (p) { 27 | html("
"); 28 | html_txt(++p); 29 | html("
"); 30 | } 31 | } 32 | 33 | void cgit_print_tag(char *revname) 34 | { 35 | unsigned char sha1[20]; 36 | struct object *obj; 37 | struct tag *tag; 38 | struct taginfo *info; 39 | 40 | if (get_sha1(revname, sha1)) { 41 | cgit_print_error(fmt("Bad tag reference: %s", revname)); 42 | return; 43 | } 44 | obj = parse_object(sha1); 45 | if (!obj) { 46 | cgit_print_error(fmt("Bad object id: %s", sha1_to_hex(sha1))); 47 | return; 48 | } 49 | if (obj->type == OBJ_TAG) { 50 | tag = lookup_tag(sha1); 51 | if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) { 52 | cgit_print_error(fmt("Bad tag object: %s", revname)); 53 | return; 54 | } 55 | html("\n"); 56 | htmlf("\n", 57 | revname, sha1_to_hex(sha1)); 58 | if (info->tagger_date > 0) { 59 | html("\n"); 62 | } 63 | if (info->tagger) { 64 | html("\n"); 71 | } 72 | html("\n"); 75 | html("
Tag name%s (%s)
Tag date"); 60 | cgit_print_date(info->tagger_date, FMT_LONGDATE, ctx.cfg.local_time); 61 | html("
Tagged by"); 65 | html_txt(info->tagger); 66 | if (info->tagger_email) { 67 | html(" "); 68 | html_txt(info->tagger_email); 69 | } 70 | html("
Tagged object"); 73 | cgit_object_link(tag->tagged); 74 | html("
\n"); 76 | print_tag_content(info->msg); 77 | } 78 | return; 79 | } 80 | -------------------------------------------------------------------------------- /ui-tag.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_TAG_H 2 | #define UI_TAG_H 3 | 4 | extern void cgit_print_tag(char *revname); 5 | 6 | #endif /* UI_TAG_H */ 7 | -------------------------------------------------------------------------------- /ui-tree.c: -------------------------------------------------------------------------------- 1 | /* ui-tree.c: functions for tree output 2 | * 3 | * Copyright (C) 2006 Lars Hjemli 4 | * 5 | * Licensed under GNU General Public License v2 6 | * (see COPYING for full license text) 7 | */ 8 | 9 | #include "cgit.h" 10 | #include "html.h" 11 | #include "ui-shared.h" 12 | 13 | char *curr_rev; 14 | char *match_path; 15 | int header = 0; 16 | 17 | static void print_object(const unsigned char *sha1, char *path) 18 | { 19 | enum object_type type; 20 | char *buf; 21 | unsigned long size, lineno, start, idx; 22 | const char *linefmt = "%1$d"; 23 | 24 | type = sha1_object_info(sha1, &size); 25 | if (type == OBJ_BAD) { 26 | cgit_print_error(fmt("Bad object name: %s", 27 | sha1_to_hex(sha1))); 28 | return; 29 | } 30 | 31 | buf = read_sha1_file(sha1, &type, &size); 32 | if (!buf) { 33 | cgit_print_error(fmt("Error reading object %s", 34 | sha1_to_hex(sha1))); 35 | return; 36 | } 37 | 38 | html(" ("); 39 | cgit_plain_link("plain", NULL, NULL, ctx.qry.head, 40 | curr_rev, path); 41 | htmlf(")
blob: %s", sha1_to_hex(sha1)); 42 | 43 | html("\n"); 44 | idx = 0; 45 | start = 0; 46 | lineno = 0; 47 | while(idx < size) { 48 | if (buf[idx] == '\n') { 49 | buf[idx] = '\0'; 50 | htmlf(linefmt, ++lineno); 51 | html_txt(buf + start); 52 | html("\n"); 53 | start = idx + 1; 54 | } 55 | idx++; 56 | } 57 | htmlf(linefmt, ++lineno); 58 | html_txt(buf + start); 59 | html("\n"); 60 | html("
\n"); 61 | } 62 | 63 | 64 | static int ls_item(const unsigned char *sha1, const char *base, int baselen, 65 | const char *pathname, unsigned int mode, int stage, 66 | void *cbdata) 67 | { 68 | char *name; 69 | char *fullpath; 70 | enum object_type type; 71 | unsigned long size = 0; 72 | 73 | name = xstrdup(pathname); 74 | fullpath = fmt("%s%s%s", ctx.qry.path ? ctx.qry.path : "", 75 | ctx.qry.path ? "/" : "", name); 76 | 77 | if (!S_ISGITLINK(mode)) { 78 | type = sha1_object_info(sha1, &size); 79 | if (type == OBJ_BAD) { 80 | htmlf("Bad object: %s %s", 81 | name, 82 | sha1_to_hex(sha1)); 83 | return 0; 84 | } 85 | } 86 | 87 | html(""); 88 | cgit_print_filemode(mode); 89 | html(""); 90 | if (S_ISGITLINK(mode)) { 91 | htmlf(""); 96 | html_txt(name); 97 | html(""); 98 | } else if (S_ISDIR(mode)) { 99 | cgit_tree_link(name, NULL, "ls-dir", ctx.qry.head, 100 | curr_rev, fullpath); 101 | } else { 102 | cgit_tree_link(name, NULL, "ls-blob", ctx.qry.head, 103 | curr_rev, fullpath); 104 | } 105 | htmlf("%li", size); 106 | 107 | html(""); 108 | cgit_log_link("log", NULL, "button", ctx.qry.head, curr_rev, 109 | fullpath, 0, NULL, NULL); 110 | html("\n"); 111 | free(name); 112 | return 0; 113 | } 114 | 115 | static void ls_head() 116 | { 117 | html("\n"); 118 | html(""); 119 | html(""); 120 | html(""); 121 | html(""); 122 | html("\n"); 124 | header = 1; 125 | } 126 | 127 | static void ls_tail() 128 | { 129 | if (!header) 130 | return; 131 | html("
ModeNameSize"); 123 | html("
\n"); 132 | header = 0; 133 | } 134 | 135 | static void ls_tree(const unsigned char *sha1, char *path) 136 | { 137 | struct tree *tree; 138 | 139 | tree = parse_tree_indirect(sha1); 140 | if (!tree) { 141 | cgit_print_error(fmt("Not a tree object: %s", 142 | sha1_to_hex(sha1))); 143 | return; 144 | } 145 | 146 | ls_head(); 147 | read_tree_recursive(tree, "", 0, 1, NULL, ls_item, NULL); 148 | ls_tail(); 149 | } 150 | 151 | 152 | static int walk_tree(const unsigned char *sha1, const char *base, int baselen, 153 | const char *pathname, unsigned mode, int stage, 154 | void *cbdata) 155 | { 156 | static int state; 157 | static char buffer[PATH_MAX]; 158 | char *url; 159 | 160 | if (state == 0) { 161 | memcpy(buffer, base, baselen); 162 | strcpy(buffer+baselen, pathname); 163 | url = cgit_pageurl(ctx.qry.repo, "tree", 164 | fmt("h=%s&path=%s", curr_rev, buffer)); 165 | html("/"); 166 | cgit_tree_link(xstrdup(pathname), NULL, NULL, ctx.qry.head, 167 | curr_rev, buffer); 168 | 169 | if (strcmp(match_path, buffer)) 170 | return READ_TREE_RECURSIVE; 171 | 172 | if (S_ISDIR(mode)) { 173 | state = 1; 174 | ls_head(); 175 | return READ_TREE_RECURSIVE; 176 | } else { 177 | print_object(sha1, buffer); 178 | return 0; 179 | } 180 | } 181 | ls_item(sha1, base, baselen, pathname, mode, stage, NULL); 182 | return 0; 183 | } 184 | 185 | 186 | /* 187 | * Show a tree or a blob 188 | * rev: the commit pointing at the root tree object 189 | * path: path to tree or blob 190 | */ 191 | void cgit_print_tree(const char *rev, char *path) 192 | { 193 | unsigned char sha1[20]; 194 | struct commit *commit; 195 | const char *paths[] = {path, NULL}; 196 | 197 | if (!rev) 198 | rev = ctx.qry.head; 199 | 200 | curr_rev = xstrdup(rev); 201 | if (get_sha1(rev, sha1)) { 202 | cgit_print_error(fmt("Invalid revision name: %s", rev)); 203 | return; 204 | } 205 | commit = lookup_commit_reference(sha1); 206 | if (!commit || parse_commit(commit)) { 207 | cgit_print_error(fmt("Invalid commit reference: %s", rev)); 208 | return; 209 | } 210 | 211 | html("path: root"); 214 | 215 | if (path == NULL) { 216 | ls_tree(commit->tree->object.sha1, NULL); 217 | return; 218 | } 219 | 220 | match_path = path; 221 | read_tree_recursive(commit->tree, NULL, 0, 0, paths, walk_tree, NULL); 222 | ls_tail(); 223 | } 224 | -------------------------------------------------------------------------------- /ui-tree.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_TREE_H 2 | #define UI_TREE_H 3 | 4 | extern void cgit_print_tree(const char *rev, char *path); 5 | 6 | #endif /* UI_TREE_H */ 7 | --------------------------------------------------------------------------------